Add basic entity tracking, mob spawning and syncing
This commit is contained in:
parent
7e285e8068
commit
7af0acd3a3
6 changed files with 238 additions and 7 deletions
|
|
@ -46,6 +46,8 @@ func main() {
|
||||||
saveCtx, saveStop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
saveCtx, saveStop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
autosaveDone := srv.Chunks().StartAutosave(saveCtx, log, 30*time.Second)
|
autosaveDone := srv.Chunks().StartAutosave(saveCtx, log, 30*time.Second)
|
||||||
|
|
||||||
|
srv.StartSpawning()
|
||||||
|
|
||||||
ln := network.NewListener(srv, log)
|
ln := network.NewListener(srv, log)
|
||||||
|
|
||||||
// ListenAndServe blocks until the listener stops (on the same signals).
|
// ListenAndServe blocks until the listener stops (on the same signals).
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ func (h *handler) beginPlay() error {
|
||||||
go h.streamer.run(h.ctx)
|
go h.streamer.run(h.ctx)
|
||||||
h.streamer.requestRecenter(0, 0)
|
h.streamer.requestRecenter(0, 0)
|
||||||
go h.keepAliveLoop()
|
go h.keepAliveLoop()
|
||||||
|
go h.entitySyncLoop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +134,74 @@ func (h *handler) keepAliveLoop() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// entitySyncLoop periodically sends all entities in the world to the client.
|
||||||
|
// In a real server this would track which entities the player can see and send updates.
|
||||||
|
func (h *handler) entitySyncLoop() {
|
||||||
|
ticker := time.NewTicker(200 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
known := make(map[int32]bool)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-h.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
all := h.srv.Entities().All()
|
||||||
|
current := make(map[int32]bool)
|
||||||
|
for _, e := range all {
|
||||||
|
current[e.ID] = true
|
||||||
|
if !known[e.ID] {
|
||||||
|
h.sendAddEntity(e)
|
||||||
|
known[e.ID] = true
|
||||||
|
} else {
|
||||||
|
h.sendEntityTeleport(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove entities that disappeared
|
||||||
|
for id := range known {
|
||||||
|
if !current[id] {
|
||||||
|
h.sendRemoveEntity(id)
|
||||||
|
delete(known, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendAddEntity sends the minecraft:add_entity packet.
|
||||||
|
func (h *handler) sendAddEntity(e *world.Entity) error {
|
||||||
|
w := protocol.NewWriter(64)
|
||||||
|
w.VarInt(e.ID)
|
||||||
|
w.UUID(e.UUID)
|
||||||
|
w.VarInt(int32(e.TypeID))
|
||||||
|
w.Float64(e.X).Float64(e.Y).Float64(e.Z)
|
||||||
|
w.Byte(byte(e.Pitch * 256.0 / 360.0))
|
||||||
|
w.Byte(byte(e.Yaw * 256.0 / 360.0))
|
||||||
|
w.Byte(byte(e.HeadYaw * 256.0 / 360.0))
|
||||||
|
w.VarInt(0) // Data
|
||||||
|
w.Uint16(uint16(e.VelocityX))
|
||||||
|
w.Uint16(uint16(e.VelocityY))
|
||||||
|
w.Uint16(uint16(e.VelocityZ))
|
||||||
|
return h.conn.SendWriter(protocol.PlayAddEntity, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) sendEntityTeleport(e *world.Entity) error {
|
||||||
|
w := protocol.NewWriter(64)
|
||||||
|
w.VarInt(e.ID)
|
||||||
|
w.Float64(e.X).Float64(e.Y).Float64(e.Z)
|
||||||
|
w.Byte(byte(e.Yaw * 256.0 / 360.0))
|
||||||
|
w.Byte(byte(e.Pitch * 256.0 / 360.0))
|
||||||
|
w.Bool(true) // On ground
|
||||||
|
return h.conn.SendWriter(protocol.PlayTeleportEntity, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) sendRemoveEntity(id int32) error {
|
||||||
|
w := protocol.NewWriter(16)
|
||||||
|
w.VarInt(1) // count
|
||||||
|
w.VarInt(id)
|
||||||
|
return h.conn.SendWriter(protocol.PlayRemoveEntities, w)
|
||||||
|
}
|
||||||
|
|
||||||
// handlePlay dispatches serverbound play packets. Most are tolerated for now;
|
// handlePlay dispatches serverbound play packets. Most are tolerated for now;
|
||||||
// teleport and keep-alive are acknowledged/logged.
|
// teleport and keep-alive are acknowledged/logged.
|
||||||
func (h *handler) handlePlay(pkt protocol.Packet) error {
|
func (h *handler) handlePlay(pkt protocol.Packet) error {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,17 @@ const (
|
||||||
PlayBlockUpdate = 0x08
|
PlayBlockUpdate = 0x08
|
||||||
PlayBlockChangedAck = 0x04
|
PlayBlockChangedAck = 0x04
|
||||||
PlaySystemChat = 0x79
|
PlaySystemChat = 0x79
|
||||||
|
|
||||||
|
// Entity packets
|
||||||
|
PlayAddEntity = 0x01
|
||||||
|
PlayTeleportEntity = 0x7D
|
||||||
|
PlayMoveEntityPos = 0x35
|
||||||
|
PlayMoveEntityPosRot = 0x36
|
||||||
|
PlayMoveEntityRot = 0x38
|
||||||
|
PlaySetEntityMotion = 0x65
|
||||||
|
PlayRemoveEntities = 0x4D
|
||||||
|
PlaySetEntityData = 0x63
|
||||||
|
PlaySetEquipment = 0x66
|
||||||
)
|
)
|
||||||
|
|
||||||
// Play, serverbound (protocol 775).
|
// Play, serverbound (protocol 775).
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ type Server struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
chunks *world.Cache
|
chunks *world.Cache
|
||||||
store *world.Store // nil when persistence is disabled
|
store *world.Store // nil when persistence is disabled
|
||||||
|
entities *world.EntityManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// New constructs a Server from cfg. When cfg.WorldDir is set, the world is
|
// New constructs a Server from cfg. When cfg.WorldDir is set, the world is
|
||||||
|
|
@ -61,10 +62,11 @@ type Server struct {
|
||||||
// only. A returned error (e.g. the world dir cannot be created) is fatal.
|
// only. A returned error (e.g. the world dir cannot be created) is fatal.
|
||||||
func New(cfg Config) (*Server, error) {
|
func New(cfg Config) (*Server, error) {
|
||||||
gen := world.NewVanillaGenerator(cfg.WorldSeed)
|
gen := world.NewVanillaGenerator(cfg.WorldSeed)
|
||||||
|
em := world.NewEntityManager()
|
||||||
if cfg.WorldDir == "" {
|
if cfg.WorldDir == "" {
|
||||||
// No persistence; keep eviction off too (flat/test worlds expect full
|
// No persistence; keep eviction off too (flat/test worlds expect full
|
||||||
// presence). Real servers set WorldDir and MaxCachedChunks together.
|
// presence). Real servers set WorldDir and MaxCachedChunks together.
|
||||||
return &Server{cfg: cfg, chunks: world.NewCache(int32(cfg.CompressionThreshold), gen)}, nil
|
return &Server{cfg: cfg, chunks: world.NewCache(int32(cfg.CompressionThreshold), gen), entities: em}, nil
|
||||||
}
|
}
|
||||||
store, err := world.NewStore(cfg.WorldDir)
|
store, err := world.NewStore(cfg.WorldDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -74,6 +76,7 @@ func New(cfg Config) (*Server, error) {
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
chunks: world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, store, cfg.MaxCachedChunks),
|
chunks: world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, store, cfg.MaxCachedChunks),
|
||||||
store: store,
|
store: store,
|
||||||
|
entities: em,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +86,9 @@ func (s *Server) Config() Config { return s.cfg }
|
||||||
// Chunks returns the shared chunk cache.
|
// Chunks returns the shared chunk cache.
|
||||||
func (s *Server) Chunks() *world.Cache { return s.chunks }
|
func (s *Server) Chunks() *world.Cache { return s.chunks }
|
||||||
|
|
||||||
|
// Entities returns the shared entity manager.
|
||||||
|
func (s *Server) Entities() *world.EntityManager { return s.entities }
|
||||||
|
|
||||||
// Store returns the on-disk world store, or nil if persistence is disabled.
|
// Store returns the on-disk world store, or nil if persistence is disabled.
|
||||||
func (s *Server) Store() *world.Store { return s.store }
|
func (s *Server) Store() *world.Store { return s.store }
|
||||||
|
|
||||||
|
|
|
||||||
66
internal/server/spawner.go
Normal file
66
internal/server/spawner.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"regionio/internal/registry"
|
||||||
|
"regionio/internal/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartSpawning begins the entity tick and spawn loops.
|
||||||
|
func (s *Server) StartSpawning() {
|
||||||
|
go s.entityTickLoop()
|
||||||
|
go s.mobSpawnLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) entityTickLoop() {
|
||||||
|
ticker := time.NewTicker(50 * time.Millisecond) // 20 TPS
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
all := s.entities.All()
|
||||||
|
for _, e := range all {
|
||||||
|
// Basic random wandering
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) mobSpawnLoop() {
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
pigType := registry.Index("minecraft:entity_type", "minecraft:pig")
|
||||||
|
zombieType := registry.Index("minecraft:entity_type", "minecraft:zombie")
|
||||||
|
if pigType < 0 || zombieType < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
all := s.entities.All()
|
||||||
|
if len(all) > 50 {
|
||||||
|
continue // limit to 50 entities
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, // They float for now since there's no gravity
|
||||||
|
Z: z + 8.5,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
77
internal/world/entity.go
Normal file
77
internal/world/entity.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Entity represents an in-game movable entity (mob, animal, etc).
|
||||||
|
type Entity struct {
|
||||||
|
ID int32
|
||||||
|
UUID [16]byte
|
||||||
|
TypeID int // Network ID from the minecraft:entity_type registry
|
||||||
|
TypeName string
|
||||||
|
|
||||||
|
X, Y, Z float64
|
||||||
|
Pitch, Yaw float32
|
||||||
|
HeadYaw float32
|
||||||
|
VelocityX int16
|
||||||
|
VelocityY int16
|
||||||
|
VelocityZ int16
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntityManager tracks active entities in the server and manages thread-safe access.
|
||||||
|
type EntityManager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
entities map[int32]*Entity
|
||||||
|
nextID int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEntityManager returns a fresh manager starting at a high ID.
|
||||||
|
func NewEntityManager() *EntityManager {
|
||||||
|
return &EntityManager{
|
||||||
|
entities: make(map[int32]*Entity),
|
||||||
|
nextID: 1000, // keep IDs above early players
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add assigns an ID and UUID (if missing) and tracks the entity.
|
||||||
|
func (em *EntityManager) Add(e *Entity) int32 {
|
||||||
|
em.mu.Lock()
|
||||||
|
defer em.mu.Unlock()
|
||||||
|
e.ID = atomic.AddInt32(&em.nextID, 1)
|
||||||
|
if e.UUID == [16]byte{} {
|
||||||
|
rand.Read(e.UUID[:])
|
||||||
|
// Version 4 UUID
|
||||||
|
e.UUID[6] = (e.UUID[6] & 0x0f) | 0x40
|
||||||
|
e.UUID[8] = (e.UUID[8] & 0x3f) | 0x80
|
||||||
|
}
|
||||||
|
em.entities[e.ID] = e
|
||||||
|
return e.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove drops the entity by its ID.
|
||||||
|
func (em *EntityManager) Remove(id int32) {
|
||||||
|
em.mu.Lock()
|
||||||
|
defer em.mu.Unlock()
|
||||||
|
delete(em.entities, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves an entity by ID, or nil if not found.
|
||||||
|
func (em *EntityManager) Get(id int32) *Entity {
|
||||||
|
em.mu.RLock()
|
||||||
|
defer em.mu.RUnlock()
|
||||||
|
return em.entities[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// All returns a snapshot slice of all active entities.
|
||||||
|
func (em *EntityManager) All() []*Entity {
|
||||||
|
em.mu.RLock()
|
||||||
|
defer em.mu.RUnlock()
|
||||||
|
list := make([]*Entity, 0, len(em.entities))
|
||||||
|
for _, e := range em.entities {
|
||||||
|
list = append(list, e)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue