move tools to strom cli

This commit is contained in:
adman
2026-06-27 13:06:05 +02:00
parent c0b1501f23
commit b2784574bd
6 changed files with 333 additions and 30 deletions
+18 -10
View File
@@ -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) {
+211
View File
@@ -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
}
+152
View File
@@ -0,0 +1,152 @@
package registry_data_hunter
import (
"encoding/json"
"errors"
"log/slog"
"os"
"github.com/admin-else/strom/api"
"github.com/admin-else/strom/client"
"github.com/admin-else/strom/data"
"github.com/admin-else/strom/event"
"github.com/admin-else/strom/nbt"
"github.com/admin-else/strom/proto"
"github.com/admin-else/strom/proto_base"
"github.com/admin-else/strom/proto_generated/v1_21_8"
"github.com/admin-else/strom/server"
"github.com/admin-else/strom/text"
)
func wontFail[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
var (
Version = "1.21.11"
SaveAs = "data/registry/" + Version + ".nbt"
CompatibleProtocolVersion = wontFail(data.LookUpVersionByName(Version)).Version
FinishedConfigErr = errors.New("finished configuration")
)
var Status = wontFail(json.Marshal(server.StatusResponse{
Version: server.StatusResponseVersion{
Name: nil,
Protocol: CompatibleProtocolVersion,
},
Description: text.Pretty("Registry data hunter"),
}))
type Proxy struct {
Client, Servee *proto.Conn
Data *nbt.Tag
}
func (p *Proxy) OnAnything(e event.Anything) (err error) {
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
if !ok {
return
}
packetInfo, ok := proto.LookupPacketInfoByType(packet)
if !ok {
return
}
switch packetInfo.Direction {
case proto_base.ToServer:
err = p.Client.Send(packet)
case proto_base.ToClient:
err = p.Servee.Send(packet)
}
return
}
func (p *Proxy) OnFinishConfiguration(_ *v1_21_8.ConfigurationToClientPacketFinishConfiguration) (err error) {
err = FinishedConfigErr
return
}
func (p *Proxy) OnSelectKnownPacks(data *v1_21_8.ConfigurationToClientPacketCommonSelectKnownPacks) (err error) {
var entries []any
for _, entry := range data.Packs {
e := map[string]any{"Namespace": entry.Namespace, "Id": entry.Id, "Version": entry.Version}
entries = append(entries, e)
}
p.Data.Value.(map[string]any)["KnownPacks"] = entries
return
}
func (p *Proxy) OnData(data *v1_21_8.ConfigurationToClientPacketRegistryData) (err error) {
d := p.Data.Value.(map[string]any)["Registries"].([]any)
var entries []any
for _, entry := range data.Entries {
if entry.Value != nil {
entries = append(entries, map[string]any{"TagType": entry.Key, "Value": entry.Value.Value})
} else {
entries = append(entries, map[string]any{"TagType": entry.Key})
}
}
d = append(d, map[string]any{"Id": data.Id, "Entries": entries})
p.Data.Value.(map[string]any)["Registries"] = d
return
}
func (p *Proxy) OnTags(data *v1_21_8.ConfigurationToClientPacketTags) (err error) {
var tags []any
for _, tag := range data.Tags {
var entries []any
for _, entry := range tag.Tags.Val {
entries = append(entries, map[string]any{"TagName": entry.TagName, "Entries": entry.Entries})
}
tags = append(tags, map[string]any{"TagType": tag.TagType, "Entries": entries})
}
p.Data.Value.(map[string]any)["Tags"] = tags
return
}
func handleClient(c *proto.Conn) (err error) {
acc := api.NewOfflineAccount("RegistryHunter")
_, err = server.ServeLogin(c, server.WithOtherAccount(acc), server.WithCompatibleVersions(CompatibleProtocolVersion), server.WithRawStatus(Status))
if err != nil {
return
}
sockPuppet, err := client.Login(":25565", acc)
if err != nil {
return
}
defer sockPuppet.Close()
errChan := make(chan error)
p := &Proxy{Client: sockPuppet, Servee: c}
p.Data.Value = map[string]any{"Registries": []any{}}
p.Client.RegisterCritical(p.OnAnything)
p.Servee.RegisterCritical(p.OnAnything)
p.Client.RegisterCriticalUntilLatest(p.OnData)
p.Client.RegisterCriticalUntilLatest(p.OnTags)
p.Client.RegisterCriticalUntilLatest(p.OnSelectKnownPacks)
p.Client.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
go func() {
errChan <- p.Client.StartConn()
}()
go func() {
errChan <- p.Servee.StartConn()
}()
err = <-errChan
f, err := os.Create(SaveAs)
if err != nil {
return
}
defer f.Close()
err = nbt.WriteUnstructuredFile(f, p.Data)
slog.Info("saved", "file", SaveAs)
return
}
func Run(args []string) error {
slog.SetLogLoggerLevel(slog.LevelDebug)
return server.StartServerWithOnConn(":25566", handleClient)
}
+97
View File
@@ -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)
})
}
+180
View File
@@ -0,0 +1,180 @@
package serve_world
import (
"bytes"
"fmt"
"log/slog"
"math/rand/v2"
"os"
"time"
"github.com/admin-else/strom/event"
"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"
"github.com/admin-else/strom/proto_generated/v1_21_8"
"github.com/admin-else/strom/server"
"github.com/admin-else/strom/util"
)
type Server struct {
*proto.Conn
*event.Timer
reconnectCalled bool
}
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 Limbo(c *proto.Conn) (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
}
login := &v1_21_8.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_8.PlayToClientSpawnInfo{Dimension: 0, Name: "minecraft:overworld", HashedSeed: -8365436745629239539, Gamemode: "survival", PreviousGamemode: 0xff, IsDebug: false, IsFlat: false, Death: nil, PortalCooldown: 0, SeaLevel: 63}, EnforcesSecureChat: false}
err = s.Send(login)
if err != nil {
return
}
err = s.Send(&v1_21_8.PlayToClientPacketPosition{
TeleportId: 0,
X: 0.5,
Y: 100,
Z: 0.5,
Dx: 0,
Dy: 0,
Dz: 0,
Yaw: 0,
Pitch: 0,
Flags: v1_21_8.PlayToClientPositionUpdateRelatives{},
})
if err != nil {
return
}
err = s.Send(&v1_21_8.PlayToClientPacketUpdateViewPosition{
ChunkX: 0,
ChunkZ: 0,
})
if err != nil {
return
}
err = s.Send(&v1_21_8.PlayToClientPacketGameStateChange{
Reason: "level_chunks_load_start",
})
radius := int32(10)
for x := -radius; x < radius; x++ {
for z := -radius; z < radius; z++ {
var chunkData []byte
chunkData, err = GetChunkFromMca(x, z, c.Version)
if err != nil {
return
}
if chunkData == nil {
continue
}
OLDpacket := &v1_21_8.PlayToClientPacketMapChunk{}
OLDpacket.X = x
OLDpacket.Z = z
OLDpacket.ChunkData = v1_21_8.ByteArray{Val: chunkData}
OLDpacket.BlockLight = FullLight
OLDpacket.BlockLightMask = []int64{(1 << 24) - 1}
err = s.Send(OLDpacket)
if err != nil {
return
}
}
}
s.Timer = &event.Timer{}
s.Timer.Every(time.Second*15, s.SendKeepAlive)
s.Timer.Start(c.Loop)
return s.StartConn()
}
func (s *Server) SendKeepAlive() (err error) {
err = s.Send(&v1_21_8.PlayToClientPacketKeepAlive{KeepAliveId: rand.Int64()})
if err != nil {
return
}
return
}
func Run(args []string) error {
slog.SetLogLoggerLevel(slog.LevelDebug)
return server.StartServerWithOnConn(":25566", Limbo)
}