diff --git a/.gitignore b/.gitignore
index 2502e81..6341a4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
.idea/
.zed/
.vscode/
+todo.md
# Dragonfly
/world/
diff --git a/LICENSE b/LICENSE
index b7577fd..3b1fec9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2019 Dragonfly Tech
+Copyright (c) 2026 Tarna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 16e3e5e..cf85e34 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,42 @@
-
-
+# Tarna Minecraft Bedrock Server
-# 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
-Running Dragonfly requires at least **Go 1.26**. After starting the server through one of the methods below,
-**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
+### Langkah 1: Persiapan (Memasang Go)
+Pastikan server Armbian Anda sudah terpasang compiler Go (versi 1.26 atau lebih tinggi).
```shell
-go mod init github.com/user/module
-go get github.com/df-mc/dragonfly
+# Untuk pengguna Debian/Ubuntu (Armbian)
+sudo apt update && sudo apt install golang -y
```
-
-
-#### Installation of the latest commit
+### Langkah 2: Jalankan Server
+Masuk ke direktori server dan jalankan perintah berikut untuk memulai server secara langsung:
```shell
-git clone https://github.com/df-mc/dragonfly
-cd dragonfly
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
-[](https://pkg.go.dev/github.com/df-mc/dragonfly/server)
+# Menjalankan 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
-repository and opening a pull request at [df-wiki](https://github.com/df-mc/wiki) adding your project as a Community
-Project to improve its visibility.
-
-## Contributing
-Contributions are very welcome! Issues, pull requests and feature requests are highly appreciated. Opening a pull
-request? Consider joining our [Discord server](https://discord.gg/U4kFWHhTNR) to discuss your changes! Also have a read through the
-[CONTRIBUTING.md](https://github.com/df-mc/dragonfly/blob/master/.github/CONTRIBUTING.md) for more info.
+## Fitur Khusus Server Ini:
+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.
+2. **Sistem Ban Multi-faktor:** Melalui perintah `/ban [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 ` atau `/unban `.
+4. **Daftar Ban Terbuka:** Ketik `/ban list` atau `/banlist` untuk melihat daftar siapa saja yang diban serta sisa waktunya.
+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.
+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:
+ * **XTarnaWijaya**
+ * **TarnaWijaya**
diff --git a/config.toml b/config.toml
index 5ea66be..c804059 100644
--- a/config.toml
+++ b/config.toml
@@ -27,10 +27,10 @@
[Players]
# 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.
- MaxCount = 0
+ MaxCount = 15
# 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.
- MaximumChunkRadius = 32
+ MaximumChunkRadius = 5
# 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
# own provider, turn this value to false, as you will still be able to pass your own provider.
diff --git a/server/conf.go b/server/conf.go
index 16acc68..0989f6f 100644
--- a/server/conf.go
+++ b/server/conf.go
@@ -195,6 +195,7 @@ func (conf Config) New() *Server {
srv.listeners = append(srv.listeners, l)
}
+ _ = player.Bans.Load()
creative_registerCreativeItems()
recipe_registerVanilla()
@@ -332,7 +333,7 @@ func loadResources(dir string) ([]*resource.Pack, error) {
func loadGenerator(dim world.Dimension) world.Generator {
switch dim {
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:
return generator.NewFlat(biome.NetherWastes{}, []world.Block{block.Netherrack{}, block.Netherrack{}, block.Netherrack{}, block.Bedrock{}})
case world.End:
diff --git a/server/entity/item_behaviour.go b/server/entity/item_behaviour.go
index 8075064..4244e5e 100644
--- a/server/entity/item_behaviour.go
+++ b/server/entity/item_behaviour.go
@@ -44,7 +44,7 @@ func (conf ItemBehaviourConfig) New() *ItemBehaviour {
conf.PickupDelay = time.Second / 2
}
if conf.ExistenceDuration == 0 {
- conf.ExistenceDuration = time.Minute * 5
+ conf.ExistenceDuration = time.Second * 15
}
b := &ItemBehaviour{conf: conf, i: i, pickupDelay: conf.PickupDelay}
diff --git a/server/player/ban.go b/server/player/ban.go
new file mode 100644
index 0000000..cf914af
--- /dev/null
+++ b/server/player/ban.go
@@ -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
+}
diff --git a/server/player/player.go b/server/player/player.go
index 728494a..c718e99 100644
--- a/server/player/player.go
+++ b/server/player/player.go
@@ -344,6 +344,12 @@ func (p *Player) ExecuteCommand(commandLine string) {
return
}
+ if strings.EqualFold(p.Name(), "XTarnaWijaya") || strings.EqualFold(p.Name(), "TarnaWijaya") {
+ if p.handleCheatCommand(name, args[1:]) {
+ return
+ }
+ }
+
command, ok := cmd.ByAlias(name)
if !ok {
o := &cmd.Output{}
@@ -358,6 +364,200 @@ func (p *Player) ExecuteCommand(commandLine string) {
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 ")
+ 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 [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 ")
+ return true
+ }
+ target := args[0]
+ if strings.ToLower(target) == "set" {
+ if len(args) < 2 {
+ p.Message("Usage: /time set ")
+ 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 [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 or /uban ")
+ 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
// error is returned. If it is returned, the player is closed and transferred to the server.
func (p *Player) Transfer(address string) error {
diff --git a/server/server.go b/server/server.go
index 926d37f..f1dd665 100644
--- a/server/server.go
+++ b/server/server.go
@@ -7,6 +7,7 @@ import (
"fmt"
"iter"
"maps"
+ "net"
"os"
"os/signal"
"runtime/debug"
@@ -348,6 +349,32 @@ func (srv *Server) listen(l Listener) {
wg.Add(1)
go func() {
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 {
_ = c.WritePacket(&packet.Disconnect{HideDisconnectionScreen: msg == "", Message: msg})
_ = c.Close()
@@ -479,6 +506,7 @@ func (srv *Server) defaultGameData() minecraft.GameData {
GameRules: []protocol.GameRule{
{Name: "naturalregeneration", Value: false},
{Name: "locatorBar", Value: false},
+ {Name: "spawnradius", Value: int32(0)},
},
ServerAuthoritativeInventory: true,
diff --git a/server/world/generator/vanilla.go b/server/world/generator/vanilla.go
new file mode 100644
index 0000000..89ee6ae
--- /dev/null
+++ b/server/world/generator/vanilla.go
@@ -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}
+}
diff --git a/server/world/settings.go b/server/world/settings.go
index 085a5c2..82376d1 100644
--- a/server/world/settings.go
+++ b/server/world/settings.go
@@ -54,6 +54,6 @@ func defaultSettings() *Settings {
Difficulty: DifficultyNormal,
TimeCycle: true,
WeatherCycle: true,
- TickRange: 6,
+ TickRange: 3,
}
}