Fix terrain streaming and surface spawning
This commit is contained in:
parent
f0279cdb65
commit
2b07d6be20
17 changed files with 424 additions and 111 deletions
|
|
@ -32,6 +32,10 @@ type Config struct {
|
|||
// MaxCachedChunks bounds the in-memory chunk+frame cache (LRU). 0 means
|
||||
// unbounded (use only for tests/flat worlds). At ~200KiB/chunk, 1024 ≈ 200MB.
|
||||
MaxCachedChunks int
|
||||
// MaxViewDistance caps the client-requested chunk radius. Generation is much
|
||||
// more expensive than vanilla's pregenerated worlds, so the server owns the
|
||||
// upper bound instead of accepting the client's render distance verbatim.
|
||||
MaxViewDistance int
|
||||
}
|
||||
|
||||
// DefaultConfig returns sensible defaults matching vanilla expectations.
|
||||
|
|
@ -51,6 +55,7 @@ func DefaultConfig() Config {
|
|||
// MaxCachedChunks keeps the live cache near 200MB at the default; the
|
||||
// streamer's pre-gen ring and player view distance comfortably fit.
|
||||
MaxCachedChunks: 1024,
|
||||
MaxViewDistance: 2,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,6 +148,9 @@ func validateConfig(cfg Config) error {
|
|||
if cfg.MaxCachedChunks < 0 {
|
||||
return fmt.Errorf("server: max cached chunks must not be negative")
|
||||
}
|
||||
if cfg.MaxViewDistance < 2 || cfg.MaxViewDistance > 16 {
|
||||
return fmt.Errorf("server: max view distance must be between 2 and 16")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ func (s *Server) StartSpawning(ctx context.Context) {
|
|||
func (s *Server) entityTickLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(50 * time.Millisecond) // 20 TPS
|
||||
defer ticker.Stop()
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
|
@ -55,9 +56,9 @@ func (s *Server) entityTickLoop(ctx context.Context) {
|
|||
}
|
||||
} else {
|
||||
// Random wander
|
||||
e.X += (rand.Float64() - 0.5) * 0.2
|
||||
e.Z += (rand.Float64() - 0.5) * 0.2
|
||||
e.Yaw += float32((rand.Float64() - 0.5) * 10.0)
|
||||
e.X += (rng.Float64() - 0.5) * 0.2
|
||||
e.Z += (rng.Float64() - 0.5) * 0.2
|
||||
e.Yaw += float32((rng.Float64() - 0.5) * 10.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +74,7 @@ func (s *Server) entityTickLoop(ctx context.Context) {
|
|||
func (s *Server) mobSpawnLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
pigType := registry.EntityTypeIndex("minecraft:pig")
|
||||
zombieType := registry.EntityTypeIndex("minecraft:zombie")
|
||||
|
|
@ -85,28 +87,41 @@ func (s *Server) mobSpawnLoop(ctx context.Context) {
|
|||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if s.PlayerCount() == 0 || s.entities.Count() >= 50 {
|
||||
continue // limit to 50 entities
|
||||
if s.PlayerCount() == 0 || s.entities.Count() >= 20 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Spawn near the spawn point (8.5, 200, 8.5)
|
||||
x := (rand.Float64() - 0.5) * 30.0
|
||||
z := (rand.Float64() - 0.5) * 30.0
|
||||
|
||||
t := pigType
|
||||
name := "minecraft:pig"
|
||||
if rand.Float32() < 0.5 {
|
||||
t = zombieType
|
||||
name = "minecraft:zombie"
|
||||
}
|
||||
|
||||
s.entities.Add(&world.Entity{
|
||||
TypeID: t,
|
||||
TypeName: name,
|
||||
X: x + 8.5,
|
||||
Y: 200.0,
|
||||
Z: z + 8.5,
|
||||
})
|
||||
s.spawnMobNearPlayer(rng, pigType, zombieType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) spawnMobNearPlayer(rng *rand.Rand, pigType, zombieType int) bool {
|
||||
players := s.PlayerSnapshots()
|
||||
if len(players) == 0 {
|
||||
return false
|
||||
}
|
||||
player := players[rng.Intn(len(players))]
|
||||
angle := rng.Float64() * 2 * math.Pi
|
||||
distance := 16.0 + rng.Float64()*16.0
|
||||
x := int(math.Floor(player.X + math.Cos(angle)*distance))
|
||||
z := int(math.Floor(player.Z + math.Sin(angle)*distance))
|
||||
y, ok := s.chunks.SafeSpawnY(x, z)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
typeID := pigType
|
||||
typeName := "minecraft:pig"
|
||||
if rng.Float32() < 0.5 {
|
||||
typeID = zombieType
|
||||
typeName = "minecraft:zombie"
|
||||
}
|
||||
s.entities.Add(&world.Entity{
|
||||
TypeID: typeID,
|
||||
TypeName: typeName,
|
||||
X: float64(x) + 0.5,
|
||||
Y: float64(y),
|
||||
Z: float64(z) + 0.5,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
39
internal/server/spawner_test.go
Normal file
39
internal/server/spawner_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"regionio/internal/world"
|
||||
)
|
||||
|
||||
func TestSpawnMobNearPlayerUsesSurface(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.WorldDir = ""
|
||||
srv, err := NewWithCache(cfg, world.NewCache(-1, world.GenerateFlat))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session, err := srv.RegisterPlayer(Profile{Name: "Alice", UUID: OfflineUUID("Alice")}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.SetPlayerTransform(session, 8.5, 80, 8.5, 0, 0, true)
|
||||
|
||||
if !srv.spawnMobNearPlayer(rand.New(rand.NewSource(1)), 10, 20) {
|
||||
t.Fatal("spawnMobNearPlayer returned false")
|
||||
}
|
||||
entities := srv.Entities().All()
|
||||
if len(entities) != 1 {
|
||||
t.Fatalf("entities = %d, want 1", len(entities))
|
||||
}
|
||||
entity := entities[0]
|
||||
if entity.Y != world.FlatSurfaceY+1 {
|
||||
t.Fatalf("mob Y = %v, want surface Y %d", entity.Y, world.FlatSurfaceY+1)
|
||||
}
|
||||
distance := math.Hypot(entity.X-8.5, entity.Z-8.5)
|
||||
if distance < 15 || distance > 33 {
|
||||
t.Fatalf("mob distance = %v, want near player", distance)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue