diff --git a/.gitignore b/.gitignore index 37d0d29..68a3eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .devres/ .idea/ .failed_packets/ -tools/.*/ .vscode/ \ No newline at end of file diff --git a/cmd/strom/main.go b/cmd/strom/main.go index b5957c9..fdac7bf 100644 --- a/cmd/strom/main.go +++ b/cmd/strom/main.go @@ -12,11 +12,15 @@ import ( "github.com/admin-else/strom/cmd/strom/api" "github.com/admin-else/strom/cmd/strom/data" "github.com/admin-else/strom/cmd/strom/extract_mca" + "github.com/admin-else/strom/cmd/strom/mca_to_tmcpr" "github.com/admin-else/strom/cmd/strom/offline_uuid" "github.com/admin-else/strom/cmd/strom/packet_info" "github.com/admin-else/strom/cmd/strom/packet_inspector" "github.com/admin-else/strom/cmd/strom/print_nbt" "github.com/admin-else/strom/cmd/strom/print_replay" + "github.com/admin-else/strom/cmd/strom/registry_data_hunter" + "github.com/admin-else/strom/cmd/strom/serve_tmcpr" + "github.com/admin-else/strom/cmd/strom/serve_world" "github.com/admin-else/strom/cmd/strom/status" ) @@ -45,16 +49,20 @@ func Version(args []string) error { } var subcommands = map[string]func(args []string) error{ - "offline-uuid": offline_uuid.Run, - "packet-info": packet_info.Run, - "print-nbt": print_nbt.Run, - "status": status.Run, - "packet-spy": packet_inspector.Run, - "api": api.Run, - "version": Version, - "extract-mca": extract_mca.Run, - "data": data.Run, - "print-replay": print_replay.Run, + "offline-uuid": offline_uuid.Run, + "packet-info": packet_info.Run, + "print-nbt": print_nbt.Run, + "status": status.Run, + "packet-spy": packet_inspector.Run, + "api": api.Run, + "version": Version, + "extract-mca": extract_mca.Run, + "data": data.Run, + "print-replay": print_replay.Run, + "registry-data-hunter": registry_data_hunter.Run, + "serve-world": serve_world.Run, + "mca-to-tmcpr": mca_to_tmcpr.Run, + "serve-tmcpr": serve_tmcpr.Run, } func Help(args []string) (err error) { diff --git a/cmd/strom/mca_to_tmcpr/main.go b/cmd/strom/mca_to_tmcpr/main.go new file mode 100644 index 0000000..b2c6f35 --- /dev/null +++ b/cmd/strom/mca_to_tmcpr/main.go @@ -0,0 +1,211 @@ +package mca_to_tmcpr + +import ( + "bytes" + "flag" + "fmt" + "log/slog" + "os" + + "github.com/admin-else/strom/level" + "github.com/admin-else/strom/level/anvil" + "github.com/admin-else/strom/nbt" + "github.com/admin-else/strom/proto/replay" + "github.com/admin-else/strom/proto_generated/v1_21_11" + "github.com/admin-else/strom/util" +) + +var ( + cmd = flag.NewFlagSet("mca-to-tmcpr", flag.ContinueOnError) + worldPath = cmd.String("world", "", "Path to world directory") + output = cmd.String("output", "", "Output .tmcpr file path") + radius = cmd.Int("radius", 14, "Chunk radius around spawn") + spawnX = cmd.Float64("spawn-x", 0.5, "Spawn X coordinate") + spawnY = cmd.Float64("spawn-y", 100, "Spawn Y coordinate") + spawnZ = cmd.Float64("spawn-z", 0.5, "Spawn Z coordinate") +) + +var FullLight [][]uint8 + +func init() { + light := make([]uint8, 2048) + for i := range light { + light[i] = 0xFF + } + sectionsPerChunk := 384 / 16 + FullLight = make([][]uint8, sectionsPerChunk) + for i := range sectionsPerChunk { + FullLight[i] = light + } +} + +func GetChunkFromMca(x, z int32, version string) (b []byte, err error) { + xmca := util.FloorDiv(x, anvil.ChunksWidthRegionFile) + zmca := util.FloorDiv(z, anvil.ChunksWidthRegionFile) + x = x % anvil.ChunksWidthRegionFile + if x < 0 { + x += anvil.ChunksWidthRegionFile + } + z = z % anvil.ChunksWidthRegionFile + if z < 0 { + z += anvil.ChunksWidthRegionFile + } + + f, err := os.Open(fmt.Sprintf("./level/anvil/testdata/securechattestworld/region/r.%v.%v.mca", xmca, zmca)) + if err != nil { + return + } + defer f.Close() + mca, err := anvil.ReadChunkHeader(f) + if err != nil { + return + } + + var n *nbt.Tag + n, err = mca.ChunkAt(f, x, z) + if err != nil { + return + } + if n == nil { + return + } + + var chunk anvil.Chunk + err = nbt.Format.Decode(n.Value, &chunk) + if err != nil { + return + } + + var packetChunk *level.Chunk + packetChunk, err = chunk.ToStorage(version) + if err != nil { + return + } + buffer := bytes.NewBuffer(nil) + err = packetChunk.WriteChunkData(buffer) + if err != nil { + return + } + b = buffer.Bytes() + return +} + +func Run(args []string) error { + slog.SetLogLoggerLevel(slog.LevelDebug) + + err := cmd.Parse(args) + if err != nil { + return err + } + if *worldPath == "" { + slog.Error("no world path specified") + return nil + } + if *output == "" { + slog.Error("no output path specified") + return nil + } + + outFile, err := os.Create(*output) + if err != nil { + slog.Error("failed to create output file", "error", err) + return err + } + defer outFile.Close() + + w := replay.NewWriter(outFile) + + err = w.WritePacket(&v1_21_11.PlayToClientPacketLogin{ + EntityId: 73, + IsHardcore: false, + WorldNames: []string{"minecraft:overworld", "minecraft:the_end", "minecraft:the_nether"}, + MaxPlayers: 100, + ViewDistance: 10, + SimulationDistance: 10, + ReducedDebugInfo: false, + EnableRespawnScreen: true, + DoLimitedCrafting: false, + WorldState: v1_21_11.PlayToClientSpawnInfo{ + Dimension: 0, + Name: "minecraft:overworld", + HashedSeed: -8365436745629239539, + Gamemode: "survival", + PreviousGamemode: 0xff, + IsDebug: false, + IsFlat: false, + Death: nil, + PortalCooldown: 0, + SeaLevel: 63, + }, + EnforcesSecureChat: false, + }, 0) + if err != nil { + slog.Error("failed to write login packet", "error", err) + return err + } + + err = w.WritePacket(&v1_21_11.PlayToClientPacketPosition{ + TeleportId: 0, + X: *spawnX, + Y: *spawnY, + Z: *spawnZ, + Dx: 0, + Dy: 0, + Dz: 0, + Yaw: 0, + Pitch: 0, + Flags: v1_21_11.PlayToClientPositionUpdateRelatives{}, + }, 0) + if err != nil { + slog.Error("failed to write position packet", "error", err) + return err + } + + chunkX := int32(*spawnX) >> 4 + chunkZ := int32(*spawnZ) >> 4 + + err = w.WritePacket(&v1_21_11.PlayToClientPacketUpdateViewPosition{ + ChunkX: chunkX, + ChunkZ: chunkZ, + }, 0) + if err != nil { + slog.Error("failed to write view position packet", "error", err) + return err + } + + err = w.WritePacket(&v1_21_11.PlayToClientPacketGameStateChange{ + Reason: "level_chunks_load_start", + }, 0) + if err != nil { + slog.Error("failed to write game state packet", "error", err) + return err + } + + radius32 := int32(*radius) + + for x := -radius32; x < radius32; x++ { + for z := -radius32; z < radius32; z++ { + var chunkData []byte + chunkData, err = GetChunkFromMca(x, z, "1.21.11") + if err != nil { + return err + } + if chunkData == nil { + continue + } + OLDpacket := &v1_21_11.PlayToClientPacketMapChunk{} + OLDpacket.X = x + OLDpacket.Z = z + OLDpacket.ChunkData = v1_21_11.ByteArray{Val: chunkData} + OLDpacket.BlockLight = FullLight + OLDpacket.BlockLightMask = []int64{(1 << 24) - 1} + err = w.WritePacket(OLDpacket, 0) + if err != nil { + return err + } + } + } + + slog.Info("conversion complete", "output", *output) + return nil +} diff --git a/tools/registry_data_hunter/main.go b/cmd/strom/registry_data_hunter/main.go similarity index 93% rename from tools/registry_data_hunter/main.go rename to cmd/strom/registry_data_hunter/main.go index 2c98b7f..068cf6e 100644 --- a/tools/registry_data_hunter/main.go +++ b/cmd/strom/registry_data_hunter/main.go @@ -1,4 +1,4 @@ -package main +package registry_data_hunter import ( "encoding/json" @@ -25,10 +25,7 @@ func wontFail[T any](v T, err error) T { return v } -// THIS IS REDUNDANT USE LOGIN PACKET IN MINECRAFT DATA - var ( - // Edit these two Version = "1.21.11" SaveAs = "data/registry/" + Version + ".nbt" @@ -113,7 +110,6 @@ func (p *Proxy) OnTags(data *v1_21_8.ConfigurationToClientPacketTags) (err error func handleClient(c *proto.Conn) (err error) { acc := api.NewOfflineAccount("RegistryHunter") - // ignore resource leak warning we own c conn so we should not close it here _, err = server.ServeLogin(c, server.WithOtherAccount(acc), server.WithCompatibleVersions(CompatibleProtocolVersion), server.WithRawStatus(Status)) if err != nil { return @@ -150,10 +146,7 @@ func handleClient(c *proto.Conn) (err error) { return } -func main() { +func Run(args []string) error { slog.SetLogLoggerLevel(slog.LevelDebug) - err := server.StartServerWithOnConn(":25566", handleClient) - if err != nil { - panic(err) - } + return server.StartServerWithOnConn(":25566", handleClient) } diff --git a/cmd/strom/serve_tmcpr/main.go b/cmd/strom/serve_tmcpr/main.go new file mode 100644 index 0000000..92e5df3 --- /dev/null +++ b/cmd/strom/serve_tmcpr/main.go @@ -0,0 +1,97 @@ +package serve_tmcpr + +import ( + "errors" + "flag" + "io" + "log/slog" + "math/rand/v2" + "os" + "time" + + "github.com/admin-else/strom/event" + "github.com/admin-else/strom/proto" + "github.com/admin-else/strom/proto/replay" + "github.com/admin-else/strom/proto_generated/v1_21_8" + "github.com/admin-else/strom/server" +) + +type Server struct { + *proto.Conn + *event.Timer +} + +var ( + cmd = flag.NewFlagSet("serve-tmcpr", flag.ContinueOnError) + tmcprFile = cmd.String("file", "/tmp/world.tmcpr", "Path to .tmcpr replay file") + bindAddr = cmd.String("bind", ":25566", "Address to listen on") +) + +func Limbo(c *proto.Conn, tmcprPath string) (err error) { + s := &Server{Conn: c} + + _, err = server.ServeLogin(s.Conn, server.WithoutOnlineMode()) + if err != nil { + return + } + + err = server.ServeConfig(s.Conn) + if err != nil { + return + } + + f, err := os.Open(tmcprPath) + if err != nil { + return + } + defer f.Close() + + r := replay.NewReader(f) + r.Version = c.ProtocolVersion + + start := time.Now() + + for { + var packetData []byte + var timestamp uint32 + packetData, timestamp, _, err = r.ReadRaw() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + time.Sleep(time.Duration(timestamp)*time.Millisecond - time.Since(start)) + err = s.SendRaw(packetData) + if err != nil { + return err + } + } + + s.Timer = &event.Timer{} + s.Timer.Every(time.Second*15, s.SendKeepAlive) + s.Timer.Start(c.Loop) + + return s.StartConn() +} + +func (s *Server) SendKeepAlive() error { + return s.Send(&v1_21_8.PlayToClientPacketKeepAlive{KeepAliveId: rand.Int64()}) +} + +func Run(args []string) error { + slog.SetLogLoggerLevel(slog.LevelDebug) + + err := cmd.Parse(args) + if err != nil { + return err + } + if *tmcprFile == "" { + slog.Error("no file specified") + return nil + } + + return server.StartServerWithOnConn(*bindAddr, func(c *proto.Conn) error { + return Limbo(c, *tmcprFile) + }) +} diff --git a/tools/serve_world/main.go b/cmd/strom/serve_world/main.go similarity index 93% rename from tools/serve_world/main.go rename to cmd/strom/serve_world/main.go index ef9b648..0bce219 100644 --- a/tools/serve_world/main.go +++ b/cmd/strom/serve_world/main.go @@ -1,4 +1,4 @@ -package main +package serve_world import ( "bytes" @@ -28,7 +28,7 @@ type Server struct { var FullLight [][]uint8 func init() { - light := make([]uint8, 2048) // 4096 values 2 per uint8 + light := make([]uint8, 2048) for i := range light { light[i] = 0xFF } @@ -95,13 +95,11 @@ func Limbo(c *proto.Conn) (err error) { s := &Server{ Conn: c, } - //s.DebugPrintPackets = []string{""} _, err = server.ServeLogin(s.Conn, server.WithoutOnlineMode()) if err != nil { return } - //goland:noinspection GoResourceLeak captures the conn object which should NOT be closed err = server.ServeConfig(s.Conn) if err != nil { return @@ -176,10 +174,7 @@ func (s *Server) SendKeepAlive() (err error) { return } -func main() { +func Run(args []string) error { slog.SetLogLoggerLevel(slog.LevelDebug) - err := server.StartServerWithOnConn(":25566", Limbo) - if err != nil { - panic(err) - } + return server.StartServerWithOnConn(":25566", Limbo) }