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)
|
||||
autosaveDone := srv.Chunks().StartAutosave(saveCtx, log, 30*time.Second)
|
||||
|
||||
srv.StartSpawning()
|
||||
|
||||
ln := network.NewListener(srv, log)
|
||||
|
||||
// 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)
|
||||
h.streamer.requestRecenter(0, 0)
|
||||
go h.keepAliveLoop()
|
||||
go h.entitySyncLoop()
|
||||
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;
|
||||
// teleport and keep-alive are acknowledged/logged.
|
||||
func (h *handler) handlePlay(pkt protocol.Packet) error {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,17 @@ const (
|
|||
PlayBlockUpdate = 0x08
|
||||
PlayBlockChangedAck = 0x04
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -51,9 +51,10 @@ func DefaultConfig() Config {
|
|||
|
||||
// Server is the top-level core shared across all connections.
|
||||
type Server struct {
|
||||
cfg Config
|
||||
chunks *world.Cache
|
||||
store *world.Store // nil when persistence is disabled
|
||||
cfg Config
|
||||
chunks *world.Cache
|
||||
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
|
||||
|
|
@ -61,19 +62,21 @@ type Server struct {
|
|||
// only. A returned error (e.g. the world dir cannot be created) is fatal.
|
||||
func New(cfg Config) (*Server, error) {
|
||||
gen := world.NewVanillaGenerator(cfg.WorldSeed)
|
||||
em := world.NewEntityManager()
|
||||
if cfg.WorldDir == "" {
|
||||
// No persistence; keep eviction off too (flat/test worlds expect full
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
chunks: world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, store, cfg.MaxCachedChunks),
|
||||
store: store,
|
||||
cfg: cfg,
|
||||
chunks: world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, store, cfg.MaxCachedChunks),
|
||||
store: store,
|
||||
entities: em,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -83,6 +86,9 @@ func (s *Server) Config() Config { return s.cfg }
|
|||
// Chunks returns the shared chunk cache.
|
||||
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.
|
||||
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