From 7f878f40a1514f93110ceb013977ba6056acf39c Mon Sep 17 00:00:00 2001 From: adman Date: Thu, 16 Jul 2026 16:05:42 +0200 Subject: [PATCH] add world module for in-memory block storage and integrate getblock command in messenger --- cmd/strom/messenger/main.go | 58 +++++++++++- mc/bot/world/mod.go | 178 ++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 mc/bot/world/mod.go diff --git a/cmd/strom/messenger/main.go b/cmd/strom/messenger/main.go index 3db3528..d309cde 100644 --- a/cmd/strom/messenger/main.go +++ b/cmd/strom/messenger/main.go @@ -2,11 +2,16 @@ package messenger import ( "flag" + "fmt" + "strconv" + "strings" "github.com/admin-else/strom/cmd/strom/cmd_util" "github.com/admin-else/strom/mc/bot/chat" "github.com/admin-else/strom/mc/bot/keepalive" + "github.com/admin-else/strom/mc/bot/world" "github.com/admin-else/strom/mc/client" + "github.com/admin-else/strom/mc/data" "github.com/admin-else/strom/mc/event" "github.com/admin-else/strom/mc/proto" "github.com/admin-else/strom/mc/proto_generated/v1_21_11" @@ -20,7 +25,8 @@ var ( type Messenger struct { *proto.Conn - chat *chat.Module + chat *chat.Module + world *world.Module } func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) { @@ -39,7 +45,46 @@ func (m *Messenger) OnChatSystem(e *v1_21_11.PlayToClientPacketSystemChat) (err } func (m *Messenger) OnStdin(e event.Stdin) (err error) { - return m.chat.SendMessage(e.Val) + input := strings.TrimSpace(e.Val) + if strings.HasPrefix(input, ">say ") { + return m.chat.SendMessage(strings.TrimSpace(strings.TrimPrefix(input, ">say"))) + } + if strings.HasPrefix(input, ">getblock ") { + return m.handleGetBlock(strings.TrimSpace(strings.TrimPrefix(input, ">getblock"))) + } + return m.chat.SendMessage(input) +} + +func (m *Messenger) handleGetBlock(args string) (err error) { + fields := strings.Fields(args) + if len(fields) != 3 { + m.Log.Info("getblock", "error", "expected >getblock ") + return nil + } + + coords := make([]int64, 3) + for i, f := range fields { + coords[i], err = strconv.ParseInt(f, 10, 32) + if err != nil { + m.Log.Info("getblock", "error", fmt.Sprintf("bad coordinate %q", f)) + return nil + } + } + + stateId, err := m.world.GetBlock(int32(coords[0]), int32(coords[1]), int32(coords[2])) + if err != nil { + m.Log.Info("getblock", "error", err.Error()) + return nil + } + + block, ok := data.LookupBlockByStateId(m.Version, stateId) + if !ok { + m.Log.Info("getblock", "stateId", stateId, "name", "unknown") + return nil + } + + m.Log.Info("getblock", "x", coords[0], "y", coords[1], "z", coords[2], "name", block.Name, "stateId", stateId) + return nil } func Run(args []string) (err error) { @@ -62,9 +107,14 @@ func Run(args []string) (err error) { if err != nil { return } + + // Shareable world storage. Other bots can subscribe to the same World. + w := world.NewWorld(c.Version, -64, 384) + m := &Messenger{ - Conn: c, - chat: chatMod, + Conn: c, + chat: chatMod, + world: world.Start(c, w), } event.StartListingStdin(m.Loop) diff --git a/mc/bot/world/mod.go b/mc/bot/world/mod.go new file mode 100644 index 0000000..176d341 --- /dev/null +++ b/mc/bot/world/mod.go @@ -0,0 +1,178 @@ +package world + +import ( + "bytes" + "errors" + "sync" + + "github.com/admin-else/strom/mc/level" + "github.com/admin-else/strom/mc/proto" + "github.com/admin-else/strom/mc/proto_generated/v1_21_11" +) + +var ( + ChunkNotLoadedErr = errors.New("chunk not loaded") + OutOfBoundsErr = errors.New("coordinates out of bounds") +) + +// ChunkPos identifies a chunk column. +type ChunkPos struct { + X, Z int32 +} + +// World stores a shareable in-memory view of the Minecraft world. +// Multiple bots can subscribe to the same World and update it from their +// respective connections. +type World struct { + mu sync.RWMutex + chunks map[ChunkPos]*level.Chunk + version string + minY int + height int +} + +// NewWorld creates a new World for the given protocol version and vertical +// bounds. For a 1.21 overworld use minY=-64 and height=384. +func NewWorld(version string, minY, height int) *World { + return &World{ + chunks: make(map[ChunkPos]*level.Chunk), + version: version, + minY: minY, + height: height, + } +} + +// GetBlock returns the global block state ID at the given world coordinates. +func (w *World) GetBlock(x, y, z int32) (stateId int32, err error) { + w.mu.RLock() + defer w.mu.RUnlock() + return w.getBlockLocked(x, y, z) +} + +func (w *World) getBlockLocked(x, y, z int32) (stateId int32, err error) { + chunkX := floorDiv(x, level.ChunkWidth) + chunkZ := floorDiv(z, level.ChunkWidth) + chunk, ok := w.chunks[ChunkPos{chunkX, chunkZ}] + if !ok { + err = ChunkNotLoadedErr + return + } + + sectionIndex := (int(y) - w.minY) / level.ChunkWidth + if sectionIndex < 0 || sectionIndex >= len(chunk.Sections) { + err = OutOfBoundsErr + return + } + + lx, ly, lz := blockToLocal(x, y, z) + index := ly*level.ChunkWidth*level.ChunkWidth + lz*level.ChunkWidth + lx + return chunk.Sections[sectionIndex].Blocks.Get(index) +} + +// SetBlock updates the global block state ID at the given world coordinates. +func (w *World) SetBlock(x, y, z int32, stateId int32) (err error) { + w.mu.Lock() + defer w.mu.Unlock() + + chunkX := floorDiv(x, level.ChunkWidth) + chunkZ := floorDiv(z, level.ChunkWidth) + chunk, ok := w.chunks[ChunkPos{chunkX, chunkZ}] + if !ok { + return ChunkNotLoadedErr + } + + sectionIndex := (int(y) - w.minY) / level.ChunkWidth + if sectionIndex < 0 || sectionIndex >= len(chunk.Sections) { + return OutOfBoundsErr + } + + lx, ly, lz := blockToLocal(x, y, z) + index := ly*level.ChunkWidth*level.ChunkWidth + lz*level.ChunkWidth + lx + return chunk.Sections[sectionIndex].Blocks.Set(index, stateId) +} + +// Module subscribes to chunk and block-change packets and updates a shared +// World. Multiple Modules can point to the same World. +type Module struct { + *proto.Conn + world *World +} + +// GetBlock returns the block state ID at the given world coordinates via the +// shared World. +func (m *Module) GetBlock(x, y, z int32) (int32, error) { + return m.world.GetBlock(x, y, z) +} + +// Start registers packet handlers on c that update the shared World w. +func Start(c *proto.Conn, w *World) *Module { + m := &Module{Conn: c, world: w} + m.RegisterUntilLatest(m.onMapChunk) + m.RegisterUntilLatest(m.onBlockChange) + m.RegisterUntilLatest(m.onMultiBlockChange) + return m +} + +func (m *Module) onMapChunk(p *v1_21_11.PlayToClientPacketMapChunk) (err error) { + chunk, err := level.ReadChunkFromChunkPacketData( + bytes.NewReader(p.ChunkData.Val), + m.world.version, + m.world.height, + ) + if err != nil { + return + } + + m.world.mu.Lock() + m.world.chunks[ChunkPos{p.X, p.Z}] = chunk + m.world.mu.Unlock() + return nil +} + +func (m *Module) onBlockChange(p *v1_21_11.PlayToClientPacketBlockChange) (err error) { + return m.world.SetBlock(p.Location.X, int32(p.Location.Y), p.Location.Z, p.Type) +} + +func (m *Module) onMultiBlockChange(p *v1_21_11.PlayToClientPacketMultiBlockChange) (err error) { + chunkX := p.ChunkCoordinates.X + chunkZ := p.ChunkCoordinates.Z + sectionY := p.ChunkCoordinates.Y + + for _, record := range p.Records { + packedPos := record & 0xFFF + stateId := record >> 12 + + localX := (packedPos >> 8) & 0xF + localY := (packedPos >> 4) & 0xF + localZ := packedPos & 0xF + + x := chunkX*level.ChunkWidth + int32(localX) + y := sectionY*level.ChunkWidth + int32(localY) + z := chunkZ*level.ChunkWidth + int32(localZ) + + if err = m.world.SetBlock(x, y, z, stateId); err != nil { + return + } + } + return nil +} + +func floorDiv(a, b int32) int32 { + return (a - ((a%b + b) % b)) / b +} + +func blockToLocal(x, y, z int32) (lx, ly, lz int32) { + lx = x % level.ChunkWidth + if lx < 0 { + lx += level.ChunkWidth + } + ly = y % level.ChunkWidth + if ly < 0 { + ly += level.ChunkWidth + } + lz = z % level.ChunkWidth + if lz < 0 { + lz += level.ChunkWidth + } + return +}