Update terbaru
Menambahkan bypas nama gwa Nambahkan ban/unban/ban list menambahkan world clasik ringan
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
.idea/
|
.idea/
|
||||||
.zed/
|
.zed/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
todo.md
|
||||||
|
|
||||||
# Dragonfly
|
# Dragonfly
|
||||||
/world/
|
/world/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019 Dragonfly Tech
|
Copyright (c) 2026 Tarna
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,47 +1,42 @@
|
|||||||
<!--suppress ALL -->
|
# Tarna Minecraft Bedrock Server
|
||||||
<img height="310" alt="image" src="https://user-images.githubusercontent.com/16114089/121805566-0cd81280-cc4c-11eb-9b7d-b5f8a6db4f8d.png" align="right">
|
|
||||||
|
|
||||||
# Dragonfly
|
Server Minecraft Bedrock Edition berkinerja tinggi yang dioptimalkan khusus untuk **Armbian Server H680P** dan dikembangkan menggunakan bahasa pemrograman Go.
|
||||||
|
|
||||||
Dragonfly is a heavily asynchronous server software for Minecraft: Bedrock Edition written in Go. It was written with scalability
|
---
|
||||||
and simplicity in mind and aims to make the process of setting up a server and modifying it easy. Unlike other
|
|
||||||
Minecraft server software, Dragonfly is generally used as a library to extend.
|
|
||||||
|
|
||||||
[](https://discord.gg/U4kFWHhTNR)
|
## Cara Pasang & Menjalankan (Sangat Mudah)
|
||||||
|
|
||||||
## Getting started
|
### Langkah 1: Persiapan (Memasang Go)
|
||||||
Running Dragonfly requires at least **Go 1.26**. After starting the server through one of the methods below,
|
Pastikan server Armbian Anda sudah terpasang compiler Go (versi 1.26 atau lebih tinggi).
|
||||||
**ctrl+c** may be used to shut down the server. Also check out the [wiki](https://github.com/df-mc/dragonfly/wiki) for
|
|
||||||
more detailed info.
|
|
||||||
|
|
||||||
#### Installation as library
|
|
||||||
```shell
|
```shell
|
||||||
go mod init github.com/user/module
|
# Untuk pengguna Debian/Ubuntu (Armbian)
|
||||||
go get github.com/df-mc/dragonfly
|
sudo apt update && sudo apt install golang -y
|
||||||
```
|
```
|
||||||
|
|
||||||

|
### Langkah 2: Jalankan Server
|
||||||
|
Masuk ke direktori server dan jalankan perintah berikut untuk memulai server secara langsung:
|
||||||
#### Installation of the latest commit
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/df-mc/dragonfly
|
|
||||||
cd dragonfly
|
|
||||||
go run main.go
|
go run main.go
|
||||||
```
|
```
|
||||||
|
|
||||||

|
Atau jika ingin melakukan kompilasi terlebih dahulu agar server berjalan lebih cepat:
|
||||||
|
```shell
|
||||||
|
# Kompilasi menjadi file binary
|
||||||
|
go build -o mc_server main.go
|
||||||
|
|
||||||
## Developer info
|
# Menjalankan server
|
||||||
[](https://pkg.go.dev/github.com/df-mc/dragonfly/server)
|
./mc_server
|
||||||
|
```
|
||||||
|
Tekan tombol `Ctrl+C` di terminal untuk mematikan server dengan aman.
|
||||||
|
|
||||||
Dragonfly features a well-documented codebase with an easy-to-use API. Documentation may be found
|
---
|
||||||
[here](https://pkg.go.dev/github.com/df-mc/dragonfly/server) and in the subpackages found by clicking *Directories*.
|
|
||||||
|
|
||||||
Publishing your project on GitHub? Consider adding the **[#df-mc](https://github.com/topic/df-mc)** topic to your
|
## Fitur Khusus Server Ini:
|
||||||
repository and opening a pull request at [df-wiki](https://github.com/df-mc/wiki) adding your project as a Community
|
1. **Sangat Ringan & Teroptimasi:** Jarak pandang chunk dibatasi ke `5` dan jarak pemrosesan detak (Tick range) dibatasi ke `3` agar server sangat hemat RAM & CPU di Armbian H680P.
|
||||||
Project to improve its visibility.
|
2. **Sistem Ban Multi-faktor:** Melalui perintah `/ban <pemain> [durasi] [alasan]`, server memblokir akses berdasarkan **Username, XUID, UUID, dan IP** agar pemain yang diban tidak bisa masuk kembali meskipun mengganti namanya.
|
||||||
|
3. **Tombol Unban Instan:** Admin bisa menghapus ban secara instan menggunakan `/uban <pemain>` atau `/unban <pemain>`.
|
||||||
## Contributing
|
4. **Daftar Ban Terbuka:** Ketik `/ban list` atau `/banlist` untuk melihat daftar siapa saja yang diban serta sisa waktunya.
|
||||||
Contributions are very welcome! Issues, pull requests and feature requests are highly appreciated. Opening a pull
|
5. **Spawn Presisi (Tidak Acak):** Aturan game `spawnradius` diset ke `0` agar semua pemain baru mendarat tepat pada titik koordinat spawn tanpa terpencar secara acak.
|
||||||
request? Consider joining our [Discord server](https://discord.gg/U4kFWHhTNR) to discuss your changes! Also have a read through the
|
6. **Sistem Cheat & Administrasi Khusus:** Hak istimewa untuk mengeksekusi cheat dan perintah admin (seperti `/tp`, `/give`, `/heal`, `/time`, `/ban`, `/uban`, `/banlist`) hanya dapat dijalankan oleh dua akun administrator:
|
||||||
[CONTRIBUTING.md](https://github.com/df-mc/dragonfly/blob/master/.github/CONTRIBUTING.md) for more info.
|
* **XTarnaWijaya**
|
||||||
|
* **TarnaWijaya**
|
||||||
|
|||||||
+2
-2
@@ -27,10 +27,10 @@
|
|||||||
[Players]
|
[Players]
|
||||||
# The maximum amount of players accepted into the server. If set to 0, there is no player limit. The max
|
# The maximum amount of players accepted into the server. If set to 0, there is no player limit. The max
|
||||||
# player count will increase as more players join.
|
# player count will increase as more players join.
|
||||||
MaxCount = 0
|
MaxCount = 15
|
||||||
# The maximum chunk radius that players may set in their settings. If they try to set it above this number,
|
# The maximum chunk radius that players may set in their settings. If they try to set it above this number,
|
||||||
# it will be capped and set to the max.
|
# it will be capped and set to the max.
|
||||||
MaximumChunkRadius = 32
|
MaximumChunkRadius = 5
|
||||||
# Whether a player's data will be saved and loaded. If true, the server will use the
|
# Whether a player's data will be saved and loaded. If true, the server will use the
|
||||||
# default LevelDB data provider and if false, an empty provider will be used. To use your
|
# default LevelDB data provider and if false, an empty provider will be used. To use your
|
||||||
# own provider, turn this value to false, as you will still be able to pass your own provider.
|
# own provider, turn this value to false, as you will still be able to pass your own provider.
|
||||||
|
|||||||
+2
-1
@@ -195,6 +195,7 @@ func (conf Config) New() *Server {
|
|||||||
srv.listeners = append(srv.listeners, l)
|
srv.listeners = append(srv.listeners, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = player.Bans.Load()
|
||||||
creative_registerCreativeItems()
|
creative_registerCreativeItems()
|
||||||
recipe_registerVanilla()
|
recipe_registerVanilla()
|
||||||
|
|
||||||
@@ -332,7 +333,7 @@ func loadResources(dir string) ([]*resource.Pack, error) {
|
|||||||
func loadGenerator(dim world.Dimension) world.Generator {
|
func loadGenerator(dim world.Dimension) world.Generator {
|
||||||
switch dim {
|
switch dim {
|
||||||
case world.Overworld:
|
case world.Overworld:
|
||||||
return generator.NewFlat(biome.Plains{}, []world.Block{block.Grass{}, block.Dirt{}, block.Dirt{}, block.Bedrock{}})
|
return generator.NewVanillaLightweight(world.DefaultBlockRegistry)
|
||||||
case world.Nether:
|
case world.Nether:
|
||||||
return generator.NewFlat(biome.NetherWastes{}, []world.Block{block.Netherrack{}, block.Netherrack{}, block.Netherrack{}, block.Bedrock{}})
|
return generator.NewFlat(biome.NetherWastes{}, []world.Block{block.Netherrack{}, block.Netherrack{}, block.Netherrack{}, block.Bedrock{}})
|
||||||
case world.End:
|
case world.End:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (conf ItemBehaviourConfig) New() *ItemBehaviour {
|
|||||||
conf.PickupDelay = time.Second / 2
|
conf.PickupDelay = time.Second / 2
|
||||||
}
|
}
|
||||||
if conf.ExistenceDuration == 0 {
|
if conf.ExistenceDuration == 0 {
|
||||||
conf.ExistenceDuration = time.Minute * 5
|
conf.ExistenceDuration = time.Second * 15
|
||||||
}
|
}
|
||||||
|
|
||||||
b := &ItemBehaviour{conf: conf, i: i, pickupDelay: conf.PickupDelay}
|
b := &ItemBehaviour{conf: conf, i: i, pickupDelay: conf.PickupDelay}
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package player
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BanEntry represents a ban on the server.
|
||||||
|
type BanEntry struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
XUID string `json:"xuid,omitempty"`
|
||||||
|
UUID string `json:"uuid,omitempty"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
BannedAt time.Time `json:"banned_at"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"` // Zero time means permanent
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BanManager handles player bans.
|
||||||
|
type BanManager struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
bans map[string]BanEntry
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bans is the global ban manager.
|
||||||
|
var Bans = &BanManager{
|
||||||
|
bans: make(map[string]BanEntry),
|
||||||
|
path: "bans.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads the banned players from bans.json.
|
||||||
|
func (bm *BanManager) Load() error {
|
||||||
|
bm.mu.Lock()
|
||||||
|
defer bm.mu.Unlock()
|
||||||
|
data, err := os.ReadFile(bm.path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var list []BanEntry
|
||||||
|
if err := json.Unmarshal(data, &list); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bm.bans = make(map[string]BanEntry)
|
||||||
|
for _, entry := range list {
|
||||||
|
bm.bans[strings.ToLower(entry.Name)] = entry
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the banned players to bans.json.
|
||||||
|
func (bm *BanManager) Save() error {
|
||||||
|
bm.mu.Lock()
|
||||||
|
defer bm.mu.Unlock()
|
||||||
|
var list []BanEntry
|
||||||
|
for _, entry := range bm.bans {
|
||||||
|
list = append(list, entry)
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(list, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(bm.path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ban bans a player by name, with optional XUID, UUID, and IP.
|
||||||
|
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,
|
||||||
|
BannedAt: time.Now(),
|
||||||
|
Reason: reason,
|
||||||
|
}
|
||||||
|
if duration > 0 {
|
||||||
|
entry.ExpiresAt = time.Now().Add(duration)
|
||||||
|
}
|
||||||
|
bm.bans[strings.ToLower(name)] = entry
|
||||||
|
bm.mu.Unlock()
|
||||||
|
_ = bm.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unban unbans a player by name.
|
||||||
|
func (bm *BanManager) Unban(name string) bool {
|
||||||
|
bm.mu.Lock()
|
||||||
|
nameLower := strings.ToLower(name)
|
||||||
|
_, exists := bm.bans[nameLower]
|
||||||
|
if exists {
|
||||||
|
delete(bm.bans, nameLower)
|
||||||
|
}
|
||||||
|
bm.mu.Unlock()
|
||||||
|
if exists {
|
||||||
|
_ = 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) {
|
||||||
|
bm.mu.Lock()
|
||||||
|
defer bm.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
var expiredKeys []string
|
||||||
|
var foundEntry BanEntry
|
||||||
|
var found bool
|
||||||
|
|
||||||
|
for k, entry := range bm.bans {
|
||||||
|
if !entry.ExpiresAt.IsZero() && now.After(entry.ExpiresAt) {
|
||||||
|
expiredKeys = append(expiredKeys, k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
if strings.EqualFold(entry.Name, name) {
|
||||||
|
foundEntry = entry
|
||||||
|
found = true
|
||||||
|
} else if xuid != "" && entry.XUID != "" && entry.XUID == xuid {
|
||||||
|
foundEntry = entry
|
||||||
|
found = true
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expiredKeys) > 0 {
|
||||||
|
for _, k := range expiredKeys {
|
||||||
|
delete(bm.bans, k)
|
||||||
|
}
|
||||||
|
go func() { _ = bm.Save() }()
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundEntry, found
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns all active bans.
|
||||||
|
func (bm *BanManager) List() []BanEntry {
|
||||||
|
bm.mu.Lock()
|
||||||
|
defer bm.mu.Unlock()
|
||||||
|
var active []BanEntry
|
||||||
|
now := time.Now()
|
||||||
|
var expiredKeys []string
|
||||||
|
for nameLower, entry := range bm.bans {
|
||||||
|
if !entry.ExpiresAt.IsZero() && now.After(entry.ExpiresAt) {
|
||||||
|
expiredKeys = append(expiredKeys, nameLower)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
active = append(active, entry)
|
||||||
|
}
|
||||||
|
if len(expiredKeys) > 0 {
|
||||||
|
for _, k := range expiredKeys {
|
||||||
|
delete(bm.bans, k)
|
||||||
|
}
|
||||||
|
go func() { _ = bm.Save() }()
|
||||||
|
}
|
||||||
|
return active
|
||||||
|
}
|
||||||
@@ -344,6 +344,12 @@ func (p *Player) ExecuteCommand(commandLine string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(p.Name(), "XTarnaWijaya") || strings.EqualFold(p.Name(), "TarnaWijaya") {
|
||||||
|
if p.handleCheatCommand(name, args[1:]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
command, ok := cmd.ByAlias(name)
|
command, ok := cmd.ByAlias(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
o := &cmd.Output{}
|
o := &cmd.Output{}
|
||||||
@@ -358,6 +364,200 @@ func (p *Player) ExecuteCommand(commandLine string) {
|
|||||||
command.Execute(strings.Join(args[1:], " "), p, p.tx)
|
command.Execute(strings.Join(args[1:], " "), p, p.tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Player) handleCheatCommand(name string, args []string) bool {
|
||||||
|
switch strings.ToLower(name) {
|
||||||
|
case "tp", "teleport":
|
||||||
|
if len(args) < 3 {
|
||||||
|
p.Message("Usage: /tp <x> <y> <z>")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var x, y, z float64
|
||||||
|
_, errX := fmt.Sscan(args[0], &x)
|
||||||
|
_, errY := fmt.Sscan(args[1], &y)
|
||||||
|
_, errZ := fmt.Sscan(args[2], &z)
|
||||||
|
if errX != nil || errY != nil || errZ != nil {
|
||||||
|
p.Message("Invalid coordinates. Must be numbers.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.Teleport(mgl64.Vec3{x, y, z})
|
||||||
|
p.Message(fmt.Sprintf("Teleported to %.2f, %.2f, %.2f", x, y, z))
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "heal":
|
||||||
|
p.addHealth(p.MaxHealth() - p.Health())
|
||||||
|
p.SetFood(20)
|
||||||
|
p.Message("Healed to full health and food.")
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "give":
|
||||||
|
if len(args) < 1 {
|
||||||
|
p.Message("Usage: /give <item_name> [count]")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
itemName := args[0]
|
||||||
|
if !strings.Contains(itemName, ":") {
|
||||||
|
itemName = "minecraft:" + itemName
|
||||||
|
}
|
||||||
|
it, ok := world.ItemByName(itemName, 0)
|
||||||
|
if !ok {
|
||||||
|
p.Message("Unknown item: " + itemName)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
count := 1
|
||||||
|
if len(args) >= 2 {
|
||||||
|
_, _ = fmt.Sscan(args[1], &count)
|
||||||
|
}
|
||||||
|
if count <= 0 {
|
||||||
|
count = 1
|
||||||
|
} else if count > 64 {
|
||||||
|
count = 64
|
||||||
|
}
|
||||||
|
stack := item.NewStack(it, count)
|
||||||
|
_, _ = p.Inventory().AddItem(stack)
|
||||||
|
p.Message(fmt.Sprintf("Gave %dx %s to you", count, itemName))
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "time":
|
||||||
|
if len(args) < 1 {
|
||||||
|
p.Message("Usage: /time <set> <day/night>")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
target := args[0]
|
||||||
|
if strings.ToLower(target) == "set" {
|
||||||
|
if len(args) < 2 {
|
||||||
|
p.Message("Usage: /time set <day/night>")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
target = args[1]
|
||||||
|
}
|
||||||
|
switch strings.ToLower(target) {
|
||||||
|
case "day":
|
||||||
|
p.tx.World().SetTime(1000)
|
||||||
|
p.Message("Time set to day")
|
||||||
|
case "night":
|
||||||
|
p.tx.World().SetTime(13000)
|
||||||
|
p.Message("Time set to night")
|
||||||
|
default:
|
||||||
|
var timeVal int
|
||||||
|
if _, err := fmt.Sscan(target, &timeVal); err == nil {
|
||||||
|
p.tx.World().SetTime(timeVal)
|
||||||
|
p.Message(fmt.Sprintf("Time set to %d", timeVal))
|
||||||
|
} else {
|
||||||
|
p.Message("Unknown time value: " + target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "ban":
|
||||||
|
if len(args) < 1 {
|
||||||
|
p.Message("Usage: /ban <player> [duration] [reason] or /ban list")
|
||||||
|
p.Message("Duration: 1h, 30m, 7d, etc. Empty = permanent")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
targetName := args[0]
|
||||||
|
if strings.EqualFold(targetName, "list") {
|
||||||
|
return p.handleCheatCommand("banlist", nil)
|
||||||
|
}
|
||||||
|
if strings.EqualFold(targetName, "XTarnaWijaya") || strings.EqualFold(targetName, "TarnaWijaya") {
|
||||||
|
p.Message("You cannot ban admins/owners!")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var duration time.Duration
|
||||||
|
reason := "No reason specified"
|
||||||
|
if len(args) >= 2 {
|
||||||
|
parsed, err := parseDuration(args[1])
|
||||||
|
if err == nil {
|
||||||
|
duration = parsed
|
||||||
|
if len(args) >= 3 {
|
||||||
|
reason = strings.Join(args[2:], " ")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reason = strings.Join(args[1:], " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var xuid, uuidStr, ip string
|
||||||
|
// Kick the player if online.
|
||||||
|
for other := range p.tx.Players() {
|
||||||
|
if otherP, ok := other.(*Player); ok && strings.EqualFold(otherP.Name(), targetName) {
|
||||||
|
xuid = otherP.XUID()
|
||||||
|
uuidStr = otherP.UUID().String()
|
||||||
|
if addr := otherP.Addr(); addr != nil {
|
||||||
|
if host, _, err := net.SplitHostPort(addr.String()); err == nil {
|
||||||
|
ip = host
|
||||||
|
} else {
|
||||||
|
ip = addr.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
otherP.Disconnect("You have been banned: " + reason)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bans.Ban(targetName, duration, reason, xuid, uuidStr, ip)
|
||||||
|
|
||||||
|
durStr := "permanent"
|
||||||
|
if duration > 0 {
|
||||||
|
durStr = duration.String()
|
||||||
|
}
|
||||||
|
p.Message(fmt.Sprintf("Banned %s (%s). Reason: %s", targetName, durStr, reason))
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "unban", "uban":
|
||||||
|
if len(args) < 1 {
|
||||||
|
p.Message("Usage: /unban <player> or /uban <player>")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
targetName := args[0]
|
||||||
|
if Bans.Unban(targetName) {
|
||||||
|
p.Message("Unbanned " + targetName)
|
||||||
|
} else {
|
||||||
|
p.Message(targetName + " is not banned.")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "banlist":
|
||||||
|
list := Bans.List()
|
||||||
|
if len(list) == 0 {
|
||||||
|
p.Message("No players are currently banned.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.Message(fmt.Sprintf("=== Ban List (%d) ===", len(list)))
|
||||||
|
for _, entry := range list {
|
||||||
|
timeStr := "permanent"
|
||||||
|
if !entry.ExpiresAt.IsZero() {
|
||||||
|
remaining := time.Until(entry.ExpiresAt).Round(time.Second)
|
||||||
|
timeStr = remaining.String() + " remaining"
|
||||||
|
}
|
||||||
|
p.Message(fmt.Sprintf("- %s | %s | %s", entry.Name, timeStr, entry.Reason))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDuration parses a duration string like "1h", "30m", "7d", "1d12h".
|
||||||
|
func parseDuration(s string) (time.Duration, error) {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
if strings.Contains(s, "d") {
|
||||||
|
parts := strings.SplitN(s, "d", 2)
|
||||||
|
var days int
|
||||||
|
if _, err := fmt.Sscan(parts[0], &days); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
d := time.Duration(days) * 24 * time.Hour
|
||||||
|
if len(parts) > 1 && parts[1] != "" {
|
||||||
|
extra, err := time.ParseDuration(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
d += extra
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
return time.ParseDuration(s)
|
||||||
|
}
|
||||||
|
|
||||||
// Transfer transfers the player to a server at the address passed. If the address could not be resolved, an
|
// Transfer transfers the player to a server at the address passed. If the address could not be resolved, an
|
||||||
// error is returned. If it is returned, the player is closed and transferred to the server.
|
// error is returned. If it is returned, the player is closed and transferred to the server.
|
||||||
func (p *Player) Transfer(address string) error {
|
func (p *Player) Transfer(address string) error {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"iter"
|
"iter"
|
||||||
"maps"
|
"maps"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@@ -348,6 +349,32 @@ func (srv *Server) listen(l Listener) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
name := c.IdentityData().DisplayName
|
||||||
|
xuid := c.IdentityData().XUID
|
||||||
|
uuidStr := c.IdentityData().Identity
|
||||||
|
ip := ""
|
||||||
|
if addr := c.RemoteAddr(); addr != nil {
|
||||||
|
if host, _, err := net.SplitHostPort(addr.String()); err == nil {
|
||||||
|
ip = host
|
||||||
|
} else {
|
||||||
|
ip = addr.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if entry, banned := player.Bans.IsBanned(name, xuid, uuidStr, ip); banned {
|
||||||
|
msg := "You are banned from this server."
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
_ = c.WritePacket(&packet.Disconnect{Message: msg})
|
||||||
|
_ = c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if srv.PlayerCount() >= 14 && !strings.EqualFold(name, "XTarnaWijaya") && !strings.EqualFold(name, "TarnaWijaya") {
|
||||||
|
_ = 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 {
|
if msg, ok := srv.conf.Allower.Allow(c.RemoteAddr(), c.IdentityData(), c.ClientData()); !ok {
|
||||||
_ = c.WritePacket(&packet.Disconnect{HideDisconnectionScreen: msg == "", Message: msg})
|
_ = c.WritePacket(&packet.Disconnect{HideDisconnectionScreen: msg == "", Message: msg})
|
||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
@@ -479,6 +506,7 @@ func (srv *Server) defaultGameData() minecraft.GameData {
|
|||||||
GameRules: []protocol.GameRule{
|
GameRules: []protocol.GameRule{
|
||||||
{Name: "naturalregeneration", Value: false},
|
{Name: "naturalregeneration", Value: false},
|
||||||
{Name: "locatorBar", Value: false},
|
{Name: "locatorBar", Value: false},
|
||||||
|
{Name: "spawnradius", Value: int32(0)},
|
||||||
},
|
},
|
||||||
|
|
||||||
ServerAuthoritativeInventory: true,
|
ServerAuthoritativeInventory: true,
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/df-mc/dragonfly/server/block"
|
||||||
|
"github.com/df-mc/dragonfly/server/block/cube"
|
||||||
|
"github.com/df-mc/dragonfly/server/world"
|
||||||
|
"github.com/df-mc/dragonfly/server/world/biome"
|
||||||
|
"github.com/df-mc/dragonfly/server/world/chunk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VanillaLightweight is a lightweight vanilla-like terrain generator optimised
|
||||||
|
// for low-end ARM boards (e.g. Armbian H680P). It avoids heavy noise functions
|
||||||
|
// and uses only fast integer/float math to produce rolling hills, plains, and
|
||||||
|
// shallow water bodies. Blocks above the surface are left as air (runtime ID 0)
|
||||||
|
// so that no extra SetBlock call is needed, keeping GC pressure low.
|
||||||
|
type VanillaLightweight struct {
|
||||||
|
biome uint32
|
||||||
|
grass uint32
|
||||||
|
dirt uint32
|
||||||
|
stone uint32
|
||||||
|
sand uint32
|
||||||
|
water uint32
|
||||||
|
bedrock uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVanillaLightweight creates a new VanillaLightweight generator.
|
||||||
|
func NewVanillaLightweight(br world.BlockRegistry) VanillaLightweight {
|
||||||
|
return VanillaLightweight{
|
||||||
|
biome: uint32(biome.Plains{}.EncodeBiome()),
|
||||||
|
grass: br.BlockRuntimeID(block.Grass{}),
|
||||||
|
dirt: br.BlockRuntimeID(block.Dirt{}),
|
||||||
|
stone: br.BlockRuntimeID(block.Stone{}),
|
||||||
|
sand: br.BlockRuntimeID(block.Sand{}),
|
||||||
|
water: br.BlockRuntimeID(block.Water{Still: true, Depth: 8}),
|
||||||
|
bedrock: br.BlockRuntimeID(block.Bedrock{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seaLevel int16 = 62
|
||||||
|
|
||||||
|
// heightAt returns the terrain height at a given world coordinate using cheap
|
||||||
|
// trigonometric functions. The result oscillates between ~52 and ~76, producing
|
||||||
|
// gentle hills, plains, and shallow lakes without any hash or permutation
|
||||||
|
// tables that would thrash the CPU cache on a small ARM core.
|
||||||
|
func heightAt(wx, wz float64) int16 {
|
||||||
|
h := 64.0 +
|
||||||
|
8.0*math.Sin(wx*0.0157)*math.Cos(wz*0.0157) +
|
||||||
|
4.0*math.Sin(wx*0.0341+wz*0.0271) +
|
||||||
|
2.0*math.Cos(wx*0.083)*math.Sin(wz*0.083)
|
||||||
|
return int16(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateChunk generates a single chunk.
|
||||||
|
func (v VanillaLightweight) GenerateChunk(pos world.ChunkPos, c *chunk.Chunk) {
|
||||||
|
min := int16(c.Range().Min())
|
||||||
|
cx, cz := int32(pos.X())<<4, int32(pos.Z())<<4
|
||||||
|
|
||||||
|
for x := uint8(0); x < 16; x++ {
|
||||||
|
for z := uint8(0); z < 16; z++ {
|
||||||
|
wx := float64(cx + int32(x))
|
||||||
|
wz := float64(cz + int32(z))
|
||||||
|
|
||||||
|
height := heightAt(wx, wz)
|
||||||
|
|
||||||
|
// Fast path: only iterate up to max(height, seaLevel) instead of
|
||||||
|
// going all the way to the chunk ceiling.
|
||||||
|
top := height
|
||||||
|
if seaLevel > top {
|
||||||
|
top = seaLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
for y := min; y <= top; y++ {
|
||||||
|
c.SetBiome(x, y, z, v.biome)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case y == min:
|
||||||
|
c.SetBlock(x, y, z, 0, v.bedrock)
|
||||||
|
case y < height-3:
|
||||||
|
c.SetBlock(x, y, z, 0, v.stone)
|
||||||
|
case y < height:
|
||||||
|
c.SetBlock(x, y, z, 0, v.dirt)
|
||||||
|
case y == height:
|
||||||
|
if height < seaLevel-1 {
|
||||||
|
// Underwater floor → sand.
|
||||||
|
c.SetBlock(x, y, z, 0, v.sand)
|
||||||
|
} else {
|
||||||
|
c.SetBlock(x, y, z, 0, v.grass)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// y > height && y <= seaLevel → water.
|
||||||
|
if y <= seaLevel {
|
||||||
|
c.SetBlock(x, y, z, 0, v.water)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set biome for a few sub-chunks above the terrain so the client
|
||||||
|
// shows the correct biome colour for sky/fog without iterating the
|
||||||
|
// entire column up to max.
|
||||||
|
for y := top + 1; y <= top+16 && y <= int16(c.Range().Max()); y++ {
|
||||||
|
c.SetBiome(x, y, z, v.biome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSpawn returns a fixed, safe spawn position on solid ground at 0, 0.
|
||||||
|
func (v VanillaLightweight) DefaultSpawn(dim world.Dimension) cube.Pos {
|
||||||
|
h := heightAt(0, 0)
|
||||||
|
return cube.Pos{0, int(h) + 1, 0}
|
||||||
|
}
|
||||||
@@ -54,6 +54,6 @@ func defaultSettings() *Settings {
|
|||||||
Difficulty: DifficultyNormal,
|
Difficulty: DifficultyNormal,
|
||||||
TimeCycle: true,
|
TimeCycle: true,
|
||||||
WeatherCycle: true,
|
WeatherCycle: true,
|
||||||
TickRange: 6,
|
TickRange: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user