5 Commits

9 changed files with 123 additions and 254 deletions
-66
View File
@@ -1,66 +0,0 @@
# Contributing to Dragonfly
First, thank you for your interest in contributing to Dragonfly. :tada:
The following is a set of guidelines for contributing to Dragonfly. These are guidelines and in
general it is recommended to stick to them when contributing, but you should use your best
judgement. Feel free to propose changes to this document in our Discord.
### Issues
Issues are very welcome, both in the form of bug reports and feature requests.
When opening a bug report, please try to provide as much information as you can to reproduce the issue. When opening a
feature request, please be as clear as you can and provide concrete examples of how you think the proposed feature
should work. The more precise you are, the easier it is for someone to pick up your request. Don't be afraid to propose
any ideas you might have, they may help people looking to pick up your feature request!
As soon as a maintainer sees your issue and has enough information, your issue will be added to a milestone that
indicates when we hope to have your issue resolved.
### Pull Requests
In general, it is recommended to discuss any changes you would like to make in our Discord to
before making changes, unless the change-set is otherwise small or limited to a specific part of
the code base.
When reviewing pull requests, we aim to reach the following goals with the code proposed:
* Maintain the quality of the source code.
* Stick to the standard formatting of Go (go fmt).
* Provide a well-documented, simple and clean codebase.
To make sure your pull request satisfies those points, we recommend you do the following before
opening a pull request:
* Run `go fmt` on any files you have changed to ensure the formatting is correct. Some IDEs have
integration with this tool to run it automatically when committing. GoLand has a box that may be
checked in the bottom-right corner when creating a commit.
* Make sure to provide documentation for symbols where adequate. We generally follow the following
conventions for documentation in pull requests:
- Exported symbols (TypeName, FunctionName) should always have documentation over them, but if
the function exists merely to satisfy an interface, the documentation may read
`// FunctionName ...` or be completely absent if the method has no functional behaviour.
- Unexported symbols (typeName, functionName) _should_ have documentation, but doing so is not
mandatory if the function is very simple and needs no clarification.
* Make sure to use British English and proper punctuation throughout symbol names, variables and
documentation.
* Where possible, try to expose as few exported symbols (functions, types) as possible, unless
strictly necessary. This makes it easier for us to change code in the future and ensures that
users cannot use functions not suitable for the API.
* In big functions it can become difficult to track the execution flow. Try to `return` as quickly
possible in functions so that the main code flow is minimally indented and therefore easy to
track and understand.
* In places where there are three or more sequential variable declarations, these should be grouped
into a single `var ( )` block.
* Try to be conservative with the usage of generics. While these are often useful, they can quickly
pollute code if used in excess. Consider carefully if generics are needed, particularly for
exported types and functions. Don't hesitate on using them if they are able to clean up the code
significantly.
* We strive to have only completely functional features in the codebase. While we recognise that
it is not always possible to provide full functionality for a feature in a single pull request,
you should attempt to do so to the extent that you can. Specific smaller features part of the
pull request that cannot be implemented yet should be marked with a `// TODO: ...` comment so
that we can implement these when the required functionality is present.
* When you open a PR, we assume you have tested your code and made sure it is working as intended.
As a general recommendation, you should enable the Minecraft Content Log in the Profile settings
so that it becomes obvious when invalid data is sent to the client.
If you run into a problem or otherwise need help with your pull request, please feel free to reach
out to us on Discord, so we can work towards a complete pull request together.
-22
View File
@@ -1,22 +0,0 @@
name: Build
on: [pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Lint
run: make lint
- name: Test
run: go test ./...
-41
View File
@@ -1,41 +0,0 @@
name: Deploy
on:
pull_request_target:
types: [opened, synchronize, reopened, closed]
jobs:
deploy:
name: Deploy
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v6
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Build
run: go build -o dragonfly_exe -v .
- name: Deploy test server
run: |
curl -X POST https://df-mc.dev/pullrequest \
-H "X-API-Key: ${{ secrets.API_KEY }}" \
-F "pr=${{ github.event.pull_request.number }}" \
-F "binary=@dragonfly_exe"
cleanup:
name: Cleanup
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Delete test server
run: |
curl -X DELETE https://df-mc.dev/pullrequest/${{ github.event.pull_request.number }} \
-H "X-API-Key: ${{ secrets.API_KEY }}"
-93
View File
@@ -1,93 +0,0 @@
name: Build and deploy
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Lint
run: make lint
- name: Test
run: go test ./...
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
if: github.repository == 'df-mc/dragonfly'
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Build
run: go build -o dragonfly_exe -v .
- name: Set SSH info
env:
SSH_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
SSH_PRIVATE_KEY: ${{ secrets.VPS_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh/
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Stop server
env:
HOST: ${{ secrets.VPS_HOST }}
run: |
ssh -i ~/.ssh/id_rsa $HOST screen -d -R -S dragonfly -X stuff '^C'
ssh -i ~/.ssh/id_rsa $HOST rm -f dragonfly_exe
- name: Transfer executable
env:
HOST: ${{ secrets.VPS_HOST }}
run: |
scp -i ~/.ssh/id_rsa dragonfly_exe $HOST:/home/dragonfly_exe
- name: Restart server
env:
HOST: ${{ secrets.VPS_HOST }}
run: |
ssh -i ~/.ssh/id_rsa $HOST "screen -d -R -S dragonfly -X stuff '/home/dragonfly_exe\n'"
update_contributors:
name: Update Contributors
runs-on: ubuntu-latest
if: github.repository == 'df-mc/dragonfly' && github.ref == 'refs/heads/master'
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.PUSH_CLIENT_ID }}
private-key: ${{ secrets.PUSH_SECRET }}
- name: Checkout Repository
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
- name: Fetch Contributors
run: |
CONTRIBUTOR_DATA=$(curl -H "Accept: application/vnd.github+json" https://api.github.com/repos/df-mc/dragonfly/contributors?per_page=9999)
echo $CONTRIBUTOR_DATA
if [ $(echo $CONTRIBUTOR_DATA | jq type) == '"array"' ]; then echo -e "// Code generated by .github/workflows/push.yml; DO NOT EDIT\n\npackage session\n\n// enchantNames are names translated to the 'Standard Galactic Alphabet' client-side. The names generally have no meaning\n// on the vanilla server implementation, so we can sneak some easter eggs in here without anyone noticing.\nvar enchantNames = []string{"$(echo $CONTRIBUTOR_DATA | jq .[].login | sed -r -e "s/([^A-Z\"])([A-Z])/\1 \2/g" | sed "s/./\L&/g" | sort | sed -z "s/\n/,/g")"}" > server/session/enchantment_texts.go && gofmt -w server/session/enchantment_texts.go; fi
- name: Push Changes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "updated contributor list"
+10
View File
@@ -29,6 +29,16 @@ go build -o mc_server main.go
```
Tekan tombol `Ctrl+C` di terminal untuk mematikan server dengan aman.
---
Dan ini cara install dengan simple
```bash
apt update && apt upgrade -y
apt install git golang -y
git clone https://nt-server.quoll-karat.ts.net/TarnaWijaya/mc.git ||
cd mc
go run .
```
---
## Fitur Khusus Server Ini:
+48 -19
View File
@@ -21,7 +21,7 @@ type BanEntry struct {
// BanManager handles player bans.
type BanManager struct {
mu sync.Mutex
mu sync.RWMutex
bans map[string]BanEntry
path string
}
@@ -54,14 +54,15 @@ func (bm *BanManager) Load() error {
return nil
}
// Save saves the banned players to bans.json.
func (bm *BanManager) Save() error {
bm.mu.Lock()
defer bm.mu.Unlock()
// save writes bans to disk. Must be called WITHOUT holding bm.mu.
func (bm *BanManager) save() error {
bm.mu.RLock()
var list []BanEntry
for _, entry := range bm.bans {
list = append(list, entry)
}
bm.mu.RUnlock()
data, err := json.MarshalIndent(list, "", " ")
if err != nil {
return err
@@ -69,14 +70,21 @@ func (bm *BanManager) Save() error {
return os.WriteFile(bm.path, data, 0644)
}
// Save saves the banned players to bans.json.
func (bm *BanManager) Save() error {
return bm.save()
}
// Ban bans a player by name, with optional XUID, UUID, and IP.
// IP bans are intentionally NOT stored to prevent blocking all
// players on the same local network (LAN/nethernet).
func (bm *BanManager) Ban(name string, duration time.Duration, reason string, xuid string, uuidStr string, ip string) {
bm.mu.Lock()
entry := BanEntry{
Name: name,
XUID: xuid,
UUID: uuidStr,
IP: ip,
IP: "", // Do NOT store IP to prevent LAN-wide bans
BannedAt: time.Now(),
Reason: reason,
}
@@ -85,7 +93,7 @@ func (bm *BanManager) Ban(name string, duration time.Duration, reason string, xu
}
bm.bans[strings.ToLower(name)] = entry
bm.mu.Unlock()
_ = bm.Save()
_ = bm.save()
}
// Unban unbans a player by name.
@@ -98,15 +106,28 @@ func (bm *BanManager) Unban(name string) bool {
}
bm.mu.Unlock()
if exists {
_ = bm.Save()
_ = bm.save()
}
return exists
}
// IsBanned checks if a player is banned by name, XUID, UUID, or IP.
func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool) {
// UnbanAll removes all bans. Useful for emergency reset.
func (bm *BanManager) UnbanAll() int {
bm.mu.Lock()
defer bm.mu.Unlock()
count := len(bm.bans)
bm.bans = make(map[string]BanEntry)
bm.mu.Unlock()
if count > 0 {
_ = bm.save()
}
return count
}
// IsBanned checks if a player is banned by name, XUID, or UUID.
// IP-based checking is intentionally excluded to prevent false
// positives on local networks where all players share the same IP.
func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool) {
bm.mu.RLock()
now := time.Now()
var expiredKeys []string
@@ -129,18 +150,23 @@ func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool)
} else if uuidStr != "" && entry.UUID != "" && strings.EqualFold(entry.UUID, uuidStr) {
foundEntry = entry
found = true
} else if ip != "" && entry.IP != "" && entry.IP == ip {
foundEntry = entry
found = true
}
}
// NOTE: IP matching is intentionally removed.
// On local networks (LAN/nethernet), all players share the
// same IP address. Matching by IP would block ALL players
// from the same network when only one should be banned.
}
}
bm.mu.RUnlock()
// Clean up expired entries with a separate write lock.
if len(expiredKeys) > 0 {
bm.mu.Lock()
for _, k := range expiredKeys {
delete(bm.bans, k)
}
go func() { _ = bm.Save() }()
bm.mu.Unlock()
go func() { _ = bm.save() }()
}
return foundEntry, found
@@ -148,8 +174,7 @@ func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool)
// List returns all active bans.
func (bm *BanManager) List() []BanEntry {
bm.mu.Lock()
defer bm.mu.Unlock()
bm.mu.RLock()
var active []BanEntry
now := time.Now()
var expiredKeys []string
@@ -160,11 +185,15 @@ func (bm *BanManager) List() []BanEntry {
}
active = append(active, entry)
}
bm.mu.RUnlock()
if len(expiredKeys) > 0 {
bm.mu.Lock()
for _, k := range expiredKeys {
delete(bm.bans, k)
}
go func() { _ = bm.Save() }()
bm.mu.Unlock()
go func() { _ = bm.save() }()
}
return active
}
+9
View File
@@ -533,6 +533,15 @@ func (p *Player) handleCheatCommand(name string, args []string) bool {
}
return true
case "unbanall":
count := Bans.UnbanAll()
if count > 0 {
p.Message(fmt.Sprintf("Successfully unbanned all %d players.", count))
} else {
p.Message("No players were banned.")
}
return true
case "gamemode", "gm":
if len(args) < 1 {
p.Message("Usage: /gamemode <survival/creative/adventure/spectator/0/1/2/3> [player]")
+45 -6
View File
@@ -349,33 +349,64 @@ func (srv *Server) listen(l Listener) {
wg.Add(1)
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
srv.conf.Log.Error(fmt.Sprintf("panic handling connection: %v", r))
_ = c.Close()
}
}()
name := c.IdentityData().DisplayName
xuid := c.IdentityData().XUID
uuidStr := c.IdentityData().Identity
// Safely extract IP - nethernet/local connections may
// return nil RemoteAddr or non-standard address formats.
ip := ""
if addr := c.RemoteAddr(); addr != nil {
if host, _, err := net.SplitHostPort(addr.String()); err == nil {
addrStr := addr.String()
if addrStr != "" {
if host, _, err := net.SplitHostPort(addrStr); err == nil {
ip = host
} else {
ip = addr.String()
ip = addrStr
}
}
}
srv.conf.Log.Debug("Player connecting...", "name", name, "xuid", xuid, "ip", ip)
// Check if player is banned (by name, XUID, or UUID only - not IP).
if entry, banned := player.Bans.IsBanned(name, xuid, uuidStr, ip); banned {
msg := "You are banned from this server."
if entry.Reason != "" {
msg += " Reason: " + entry.Reason
}
if !entry.ExpiresAt.IsZero() {
timeLeft := time.Until(entry.ExpiresAt).Round(time.Second)
msg = fmt.Sprintf("You are banned from this server. Remaining time: %v", timeLeft)
if timeLeft > 0 {
msg += fmt.Sprintf(" Remaining: %v", timeLeft)
}
}
srv.conf.Log.Debug("Player banned, rejecting.", "name", name)
_ = c.WritePacket(&packet.Disconnect{Message: msg})
_ = c.Close()
return
}
if srv.PlayerCount() >= 14 && !strings.EqualFold(name, "XTarnaWijaya") && !strings.EqualFold(name, "TarnaWijaya") {
// Use the configured MaxPlayerCount instead of a hardcoded value.
// Admins bypass the player limit.
if srv.PlayerCount() >= srv.MaxPlayerCount() &&
!strings.EqualFold(name, "XTarnaWijaya") &&
!strings.EqualFold(name, "TarnaWijaya") {
srv.conf.Log.Debug("Server full, rejecting.", "name", name)
_ = c.WritePacket(&packet.Disconnect{Message: "Server is full."})
_ = c.Close()
return
}
if msg, ok := srv.conf.Allower.Allow(c.RemoteAddr(), c.IdentityData(), c.ClientData()); !ok {
srv.conf.Log.Debug("Player not allowed.", "name", name, "msg", msg)
_ = c.WritePacket(&packet.Disconnect{HideDisconnectionScreen: msg == "", Message: msg})
_ = c.Close()
return
@@ -469,12 +500,20 @@ func (srv *Server) finaliseConn(ctx context.Context, conn session.Conn, l Listen
if err := conn.StartGameContext(ctx, data); err != nil {
_ = l.Disconnect(conn, "Connection timeout.")
srv.conf.Log.Debug("spawn failed: "+err.Error(), "raddr", conn.RemoteAddr())
raddr := "nethernet"
if addr := conn.RemoteAddr(); addr != nil {
raddr = addr.String()
}
srv.conf.Log.Debug("spawn failed: "+err.Error(), "raddr", raddr)
return
}
if _, ok := srv.Player(id); ok {
_ = l.Disconnect(conn, "Already logged in.")
srv.conf.Log.Debug("spawn failed: already logged in", "raddr", conn.RemoteAddr())
raddr := "nethernet"
if addr := conn.RemoteAddr(); addr != nil {
raddr = addr.String()
}
srv.conf.Log.Debug("spawn failed: already logged in", "raddr", raddr)
return
}
_ = conn.WritePacket(&packet.ItemRegistry{Items: srv.customItems})
+5 -1
View File
@@ -176,7 +176,11 @@ func (conf Config) New(conn Conn) *Session {
if conf.Log == nil {
conf.Log = slog.Default()
}
conf.Log = conf.Log.With("name", conn.IdentityData().DisplayName, "uuid", conn.IdentityData().Identity, "raddr", conn.RemoteAddr().String())
raddr := "nethernet"
if addr := conn.RemoteAddr(); addr != nil {
raddr = addr.String()
}
conf.Log = conf.Log.With("name", conn.IdentityData().DisplayName, "uuid", conn.IdentityData().Identity, "raddr", raddr)
s := &Session{}
*s = Session{