32 Commits
Author SHA1 Message Date
adman e324a95727 cmd take account with env var 2026-07-16 22:16:04 +02:00
adman 7f878f40a1 add world module for in-memory block storage and integrate getblock command in messenger 2026-07-16 16:05:42 +02:00
adman 7baa9990cb yes222 2026-07-16 14:02:28 +02:00
adman 5c5c391aa6 yes 2026-07-16 01:13:31 +02:00
adman b966925212 move data stuff 2026-07-11 23:08:03 +02:00
adman efd7049859 biome map stuff 2026-07-11 22:08:29 +02:00
adman 3ef9446f50 biome depth stuff 2026-07-06 11:49:45 +02:00
adman 68612969c9 fix spline test 2026-07-05 22:32:39 +02:00
adman b4712ff5c6 spline 2026-07-05 20:53:59 +02:00
adman eab050e291 fixes 2026-07-05 10:49:45 +02:00
adman a8b090227b mc gen start 2026-07-05 10:25:43 +02:00
adman 201e0dfa44 fix moving pains 2026-06-27 14:10:31 +02:00
adman 666f250b3e mc root source folder cleanup 2026-06-27 13:54:54 +02:00
adman b2784574bd move tools to strom cli 2026-06-27 13:06:05 +02:00
adman c0b1501f23 add packet spy json logging 2026-06-17 11:45:54 +02:00
adman 4de76d8023 regenerate proto 2026-06-16 23:08:00 +02:00
adman f08684fb67 fix bug in entity metadata encoding 2026-06-16 23:07:33 +02:00
adman 9e4f9409c5 add TestEntityMetadata 2026-06-16 23:06:58 +02:00
adman 4d3fbed957 add SimplePacketToBytesLenPrefix 2026-06-16 23:06:42 +02:00
adman b338888b58 add SimpleBytesToPacket utility function for decoding packets 2026-06-16 22:29:26 +02:00
adman 4956c1d874 add OnFinishLogin handling to packet inspector 2026-06-16 22:28:01 +02:00
adman d4fe362d9f add MustLookupProtocolVersion utility function for testing purposes 2026-06-16 22:27:41 +02:00
adman 5d8ff870df add true-forward mode to packet inspector 2026-06-02 21:06:41 +02:00
adman 58c9a60236 add optional encryption flag to login server and update ServeLogin behavior 2026-05-30 15:53:35 +02:00
adman 3e5b141ec7 add ToolSum function for build info and integrate it into Version outputs 2026-05-29 01:27:35 +02:00
adman efeb5ecda6 add file comment generation and hashing for reproducible output in proto_generator 2026-05-29 01:14:56 +02:00
adman 624e8ad4de add SimplePacketToBytes utility function for encoding packets 2026-05-28 23:59:54 +02:00
adman 764cfd8c4d lpvec3d 2026-05-14 10:42:38 +02:00
adman d43ff7f83d add seek-back handling in entity metadata decoding 2026-05-14 00:06:45 +02:00
adman a39999b344 update Decode function signatures to use io.ReadSeeker instead of io.Reader 2026-05-13 23:54:06 +02:00
adman 8ee48e1a45 add EntityMetadataLoop handling 2026-05-13 23:21:31 +02:00
adman bf6affeb97 remove unused protodef_types file from proto_generator 2026-05-13 22:20:32 +02:00
505 changed files with 688541 additions and 1872 deletions
-1
View File
@@ -1,5 +1,4 @@
.devres/
.idea/
.failed_packets/
tools/.*/
.vscode/
-43
View File
@@ -1,43 +0,0 @@
package client
import (
"context"
"net"
"github.com/admin-else/strom/proto"
"github.com/admin-else/strom/proto_base"
)
// Connect connects to the given address note that it does not resolve the SRV records it's just a raw net.Dial
// You must close the connection when you're done with it
func Connect(addr string) (ret *proto.Conn, err error) {
s, err := Status(addr)
if err != nil {
return
}
ret, err = ConnectVersionLess(addr)
if err != nil {
return
}
err = ret.SetProtocolVersion(s.Version.Protocol)
return
}
func ConnectVersionLess(addr string) (ret *proto.Conn, err error) {
return ConnectVersionLessCtx(context.Background(), addr)
}
// ConnectVersionLess connects to the given address without setting the version you should set it
// when using this function.
// You must close the connection when you're done with it
func ConnectVersionLessCtx(ctx context.Context, addr string) (ret *proto.Conn, err error) {
ret = proto.NewConnCtx(ctx)
ret.Actor = proto_base.Client
ret.Conn, err = (&net.Dialer{}).DialContext(ctx, "tcp", addr)
if err != nil {
return
}
ret.R = ret.Conn
ret.W = ret.Conn
return
}
-126
View File
@@ -1,126 +0,0 @@
package client
import (
"context"
"errors"
"net"
"strconv"
"strings"
"time"
)
type DNSCheckFunc func(addr string) error
// DoDns resolves a DNS name to an IP address
// It should resolve a minecraft compatible address into something net.Dial
func DoDns(in string) (out string, host string, portN uint16, err error) {
return DoDNSChecked(in, nil)
}
// DoDNSChecked resolves a DNS name to an IP address
// It should resolve a minecraft compatible address into something net.Dial
// It also optionally accepts a function to "check" the address
func DoDNSChecked(in string, f DNSCheckFunc) (connectTo string, host string, portN uint16, err error) {
// https://mcsrc.dev/1/26.1-pre-1/net/minecraft/client/multiplayer/resolver/ServerAddress#L34
host, port, err := net.SplitHostPort(in)
if err != nil {
var addrErr *net.AddrError
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
err = nil
host = in
port = "25565"
} else {
return
}
}
porti, err := strconv.Atoi(port)
if err != nil {
return
}
if f == nil {
f = func(addr string) error { return nil }
}
err = f(host)
if err != nil {
return
}
portN = uint16(porti)
outIP, err := net.ResolveIPAddr("ip", host)
if err != nil {
return
}
err = f(outIP.String())
if err != nil {
return
}
connectTo = outIP.String() + ":" + port
if portN != 25565 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second) // otherwise this takes 10+ seconds
defer cancel()
r := &net.Resolver{}
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
if len(srvs) == 0 {
return
}
host = strings.TrimSuffix(srvs[0].Target, ".")
portN = srvs[0].Port
outIP, err = net.ResolveIPAddr("ip", host)
if err != nil {
return
}
err = f(outIP.String())
if err != nil {
return
}
connectTo = outIP.String() + ":" + port
return
}
// DoDnsSimple resolves a DNS name to an IP address
// It should resolve a minecraft compatible address into something net.Dial
// It does not check the address
func DoDnsSimple(in string) (out string, err error) {
host, port, err := net.SplitHostPort(in)
if err != nil {
var addrErr *net.AddrError
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
err = nil
host = in
port = "25565"
} else {
return
}
}
porti, err := strconv.Atoi(port)
if err != nil {
return
}
portN := uint16(porti)
_, err = net.ResolveIPAddr("ip", host)
if err != nil {
return
}
out = host + ":" + strconv.Itoa(int(portN))
if portN != 25565 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second) // otherwise this takes 10+ seconds
defer cancel()
r := &net.Resolver{}
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
if len(srvs) == 0 {
return
}
host = strings.TrimSuffix(srvs[0].Target, ".")
portN = srvs[0].Port
out = host + ":" + strconv.Itoa(int(portN))
return
}
+3 -2
View File
@@ -4,9 +4,10 @@ import (
"encoding/json"
"errors"
"flag"
"log/slog"
"os"
"github.com/admin-else/strom/api"
"github.com/admin-else/strom/mc/api"
)
var cmd = flag.NewFlagSet("api", flag.ContinueOnError)
@@ -52,7 +53,7 @@ func Run(args []string) (err error) {
return
}
} else {
err = PleaseProvideArgumentErr
slog.Info("account", "name", a.Name, "uuid", a.Uuid)
return
}
err = json.NewEncoder(os.Stdout).Encode(data)
+11
View File
@@ -0,0 +1,11 @@
package cmd_util
import "github.com/admin-else/strom/mc/api"
func Account(s string) (a *api.Account, err error) {
if len(s) > 16 {
return api.NewAccountFromYGG(s)
}
a = api.NewOfflineAccount(s)
return
}
+7 -7
View File
@@ -7,9 +7,9 @@ import (
"os"
"slices"
"github.com/admin-else/strom/data"
"github.com/admin-else/strom/proto_generated"
"github.com/admin-else/strom/util"
mcdata "github.com/admin-else/strom/mc/data"
"github.com/admin-else/strom/mc/proto_generated"
"github.com/admin-else/strom/mc/util"
)
// strom data 1.21.11 blocks id 0 name => air
@@ -39,11 +39,11 @@ func Run(args []string) (err error) {
case 0: // print available versions
dataValues = proto_generated.SupportedVersions
case 1: // print available data for version
dataValues = slices.Collect(maps.Keys(data.Paths.Data[args[0]]))
dataValues = slices.Collect(maps.Keys(mcdata.Paths.Data[args[0]]))
case 2:
err = data.LoadVersionedJson(args[0], args[1], &dataValues)
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
case 3:
err = data.LoadVersionedJson(args[0], args[1], &dataValues)
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
if err != nil {
return
}
@@ -68,7 +68,7 @@ func Run(args []string) (err error) {
}
dataValues = result
case 4, 5:
err = data.LoadVersionedJson(args[0], args[1], &dataValues)
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
if err != nil {
return
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"log/slog"
"os"
"github.com/admin-else/strom/level/anvil"
"github.com/admin-else/strom/nbt"
"github.com/admin-else/strom/mc/level/anvil"
"github.com/admin-else/strom/mc/nbt"
)
var (
+29 -10
View File
@@ -1,39 +1,52 @@
package main
import (
"debug/buildinfo"
"errors"
"fmt"
"log/slog"
"maps"
"os"
"runtime/debug"
"slices"
"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/messenger"
"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"
)
var ExpectedASubcommandErr = errors.New("expected a subcommand")
var UnknownSubcommandErr = errors.New("unknown subcommand try 'strom help' for a list of subcommands")
func Version(args []string) (err error) {
selfPath, err := os.Executable()
if err != nil {
return
func ToolVersion() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "devel"
}
info, err := buildinfo.ReadFile(selfPath)
if err != nil {
return
return info.Main.Version
}
fmt.Println("strom cli ", info.Main.Version, " (", info.Main.Sum, ")")
return
func ToolSum() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return ""
}
return info.Main.Sum
}
func Version(args []string) error {
fmt.Println("strom cli ", ToolVersion(), " (", ToolSum(), ")")
return nil
}
var subcommands = map[string]func(args []string) error{
@@ -47,6 +60,11 @@ var subcommands = map[string]func(args []string) error{
"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,
"messenger": messenger.Run,
}
func Help(args []string) (err error) {
@@ -59,6 +77,7 @@ func Help(args []string) (err error) {
func mainE() (err error) {
subcommands["help"] = Help
args := os.Args
//args = []string{"TEST", "messenger", "-addr", "localhost"}
if len(args) < 2 {
err = ExpectedASubcommandErr
+211
View File
@@ -0,0 +1,211 @@
package mca_to_tmcpr
import (
"bytes"
"flag"
"fmt"
"log/slog"
"os"
"github.com/admin-else/strom/mc/level"
"github.com/admin-else/strom/mc/level/anvil"
"github.com/admin-else/strom/mc/nbt"
"github.com/admin-else/strom/mc/proto/replay"
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
"github.com/admin-else/strom/mc/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
}
+134
View File
@@ -0,0 +1,134 @@
package messenger
import (
"flag"
"fmt"
"os"
"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"
)
func stror(a, b string) string {
if a != "" {
return a
}
return b
}
var (
cmd = flag.NewFlagSet("status", flag.ContinueOnError)
connectToFlag = cmd.String("addr", "", "server address to connect to")
accFlag = cmd.String("acc", stror(os.Getenv("STROM_ACC"), "strom_messenger"), "the name to use if longer will be treated as ygg token")
)
type Messenger struct {
*proto.Conn
chat *chat.Module
world *world.Module
}
func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) {
m.Log.Info("chat", "author", e.SenderUuid, "m", e.PlainMessage)
return
}
func (m *Messenger) OnChatUnsigned(e *v1_21_11.PlayToClientPacketProfilelessChat) (err error) {
m.Log.Info("chatP", "author", e.Name, "m", e.Message)
return
}
func (m *Messenger) OnChatSystem(e *v1_21_11.PlayToClientPacketSystemChat) (err error) {
m.Log.Info("chatS", "m", e.Content)
return
}
func (m *Messenger) OnStdin(e event.Stdin) (err error) {
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 <x> <y> <z>")
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) {
err = cmd.Parse(args)
if err != nil {
return
}
acc, err := cmd_util.Account(*accFlag)
if err != nil {
return
}
c, err := client.Login(*connectToFlag, acc)
if err != nil {
return
}
defer c.Close()
chatMod, err := chat.Start(c, acc)
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,
world: world.Start(c, w),
}
event.StartListingStdin(m.Loop)
m.Register(m.OnStdin)
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
keepalive.Start(m.Conn)
return m.StartConn()
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"flag"
"fmt"
"github.com/admin-else/strom/crypto"
"github.com/admin-else/strom/mc/crypto"
)
var cmd = flag.NewFlagSet("offline-uuid", flag.ContinueOnError)
+4 -4
View File
@@ -5,10 +5,10 @@ import (
"fmt"
"strconv"
"github.com/admin-else/strom/data"
"github.com/admin-else/strom/proto"
"github.com/admin-else/strom/proto_base"
"github.com/admin-else/strom/proto_generated"
"github.com/admin-else/strom/mc/data"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated"
)
var cmd = flag.NewFlagSet("packet-info", flag.ContinueOnError)
+93 -19
View File
@@ -1,18 +1,21 @@
package packet_inspector
import (
"encoding/json"
"flag"
"log/slog"
"os"
"time"
"github.com/admin-else/strom/api"
"github.com/admin-else/strom/client"
"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_11"
"github.com/admin-else/strom/server"
"github.com/admin-else/strom/text"
"github.com/admin-else/strom/mc/api"
"github.com/admin-else/strom/mc/client"
"github.com/admin-else/strom/mc/event"
"github.com/admin-else/strom/mc/nbt"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
"github.com/admin-else/strom/mc/server"
"github.com/admin-else/strom/mc/text"
)
var Cmd = flag.NewFlagSet("packet-inspector", flag.ContinueOnError)
@@ -22,6 +25,7 @@ var Token = Cmd.String("token", "", "token to use")
var TargetAddr = Cmd.String("addr", "127.0.0.1:25565", "address to connect to")
var ListenAddr = Cmd.String("listen", "127.0.0.1:25566", "address to listen on")
var TrueForward = Cmd.Bool("true-forward", false, "forward packets without modification -name and custom server status wont work")
type Proxy struct {
Client, Servee *proto.Conn
@@ -33,6 +37,10 @@ func (p *Proxy) Start() (err error) {
p.Servee.RegisterCritical(p.OnAnything)
p.Servee.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
p.Servee.RegisterCriticalUntilLatest(p.OnFinishLogin)
p.Servee.RegisterCriticalUntilLatest(p.OnHandshake)
//p.Servee.RegisterCriticalUntilLatest(p.OnCompress)
p.Client.RegisterCriticalUntilLatest(p.OnCompress)
errChan := make(chan error)
go func() {
@@ -45,6 +53,22 @@ func (p *Proxy) Start() (err error) {
return
}
type PacketLogEvent struct {
Time time.Time
Packet proto_base.EncodeDecodeAble
Info PacketInfoNoTypeJson
ClientAddr string
}
type PacketInfoNoTypeJson struct {
Type proto_base.EncodeDecodeAble `json:"-"`
Name string
Direction proto_base.Direction
State proto_base.State
PacketId int32
ProtocolVersion int32
}
func (p *Proxy) OnAnything(e event.Anything) (err error) {
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
if !ok {
@@ -54,6 +78,19 @@ func (p *Proxy) OnAnything(e event.Anything) (err error) {
if !ok {
return
}
jsonBytes, err := json.Marshal(PacketLogEvent{
Time: time.Now(),
Packet: packet,
ClientAddr: p.Client.RemoteAddr().String(),
Info: PacketInfoNoTypeJson(packetInfo),
})
if err != nil {
return
}
// :pray:
_, _ = os.Stdout.Write(jsonBytes)
_, _ = os.Stdout.Write([]byte("\n"))
switch packetInfo.Direction {
case proto_base.ToServer:
err = p.Client.Send(packet)
@@ -74,6 +111,40 @@ func (p *Proxy) OnFinishConfiguration(packet *v1_21_11.ConfigurationToServerPack
return
}
func (p *Proxy) OnFinishLogin(packet *v1_21_11.LoginToServerPacketLoginAcknowledged) (err error) {
err = p.Client.Send(packet)
if err != nil {
return
}
p.Servee.SetState(proto_base.Configuration)
p.Client.SetState(proto_base.Configuration)
err = event.DontForwardErr
return
}
func (p *Proxy) OnHandshake(packet *v1_21_11.HandshakingToServerPacketSetProtocol) (err error) {
err = p.Client.Send(packet)
state := proto_base.State(packet.NextState)
if err != nil {
return
}
p.Servee.SetState(state)
p.Client.SetState(state)
err = event.DontForwardErr
return
}
func (p *Proxy) OnCompress(packet *v1_21_11.LoginToClientPacketCompress) (err error) {
err = p.Servee.Send(packet)
if err != nil {
return
}
p.Servee.SetCompressionThreshold(packet.Threshold)
p.Client.SetCompressionThreshold(packet.Threshold)
err = event.DontForwardErr
return
}
var StatusResponse = server.StatusResponse{
Version: server.StatusResponseVersion{
Name: text.Pretty("STROM"),
@@ -90,38 +161,41 @@ func Run(args []string) (err error) {
if err != nil {
return
}
slog.SetLogLoggerLevel(slog.LevelDebug)
slog.Info("Starting packet inspector", "listen", *ListenAddr)
err = server.StartServerWithOnConn(*ListenAddr, func(serveeConn *proto.Conn) (err error) {
serveeConn.DebugPrintPackets = []string{""}
p := &Proxy{Servee: serveeConn, Client: nil}
var acc = api.NewOfflineAccount(*OfflineNameFlag)
if !*TrueForward {
if *Token != "" {
acc, err = api.NewAccountFromYGG(*Token)
if err != nil {
return
}
}
_, err = server.ServeLogin(serveeConn, server.WithOtherAccount(acc), server.WithStatus(StatusResponse), server.WithoutOnlineMode())
_, err = server.ServeLogin(serveeConn, server.WithOtherAccount(acc), server.WithStatus(StatusResponse), server.WithoutOnlineMode(), server.WithoutEncryption())
if err != nil {
return
}
}
*TargetAddr, err = client.DoDnsSimple(*TargetAddr)
*TargetAddr, _, _, err = client.DoDNSChecked(nil, *TargetAddr, nil)
if err != nil {
return
}
c, err := client.Connect(*TargetAddr)
if err != nil {
return
}
c.DebugPrintPackets = []string{""}
err = client.LoginRaw(c, acc)
c, err := client.Connect(nil, *TargetAddr, "")
if err != nil {
return
}
defer c.Close()
if !*TrueForward {
err = client.LoginRaw(c, acc)
if err != nil {
return
}
}
p.Client = c
p.Client.AlwaysDecode = true
p.Servee.AlwaysDecode = true
err = p.Start()
return
})
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"fmt"
"os"
"github.com/admin-else/strom/nbt"
"github.com/admin-else/strom/mc/nbt"
)
var (
+3 -3
View File
@@ -14,9 +14,9 @@ import (
"reflect"
"strings"
"github.com/admin-else/strom/data"
"github.com/admin-else/strom/proto"
"github.com/admin-else/strom/proto_base"
"github.com/admin-else/strom/mc/data"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
)
var (
@@ -1,4 +1,4 @@
package main
package registry_data_hunter
import (
"encoding/json"
@@ -6,16 +6,16 @@ import (
"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"
"github.com/admin-else/strom/mc/api"
"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/nbt"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/server"
"github.com/admin-else/strom/mc/text"
)
func wontFail[T any](v T, err error) T {
@@ -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)
}
+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/mc/event"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto/replay"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/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)
})
}
@@ -1,4 +1,4 @@
package main
package serve_world
import (
"bytes"
@@ -8,14 +8,14 @@ import (
"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"
"github.com/admin-else/strom/mc/event"
"github.com/admin-else/strom/mc/level"
"github.com/admin-else/strom/mc/level/anvil"
"github.com/admin-else/strom/mc/nbt"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/server"
"github.com/admin-else/strom/mc/util"
)
type Server struct {
@@ -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)
}
+7 -7
View File
@@ -6,11 +6,11 @@ import (
"log/slog"
"time"
"github.com/admin-else/strom/client"
"github.com/admin-else/strom/event"
"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/mc/client"
"github.com/admin-else/strom/mc/event"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
)
var cmd = flag.NewFlagSet("status", flag.ContinueOnError)
@@ -46,7 +46,7 @@ func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
// So ignore the resource leak warning. And maybe attach a warn ignore comment.
// It does not resolve SRV records.
func StatusRaw(addr string) (s *StatusClient, err error) {
c, err := client.ConnectVersionLess(addr)
c, err := client.ConnectVersionLess(nil, addr)
if err != nil {
return
}
@@ -82,7 +82,7 @@ func Run(args []string) (err error) {
return
}
if *srvFlag {
*addrFlag, err = client.DoDnsSimple(*addrFlag)
*addrFlag, _, _, err = client.DoDNSChecked(nil, *addrFlag, nil)
if err != nil {
return
}
-2
View File
@@ -2,7 +2,5 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
View File
+3 -1
View File
@@ -1,6 +1,8 @@
package api
import "github.com/admin-else/strom/crypto"
import (
"github.com/admin-else/strom/mc/crypto"
)
func NewOfflineAccount(name string) *Account {
return &Account{
+207
View File
@@ -0,0 +1,207 @@
package chat
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/pem"
"fmt"
"time"
"github.com/admin-else/strom/mc/api"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
"github.com/google/uuid"
)
type Module struct {
*proto.Conn
Account *api.Account
// Chat signing state
canSign bool
privKey *rsa.PrivateKey
sessionUUID uuid.UUID
msgIndex int32
}
// Start creates a chat module for the connection. If acc is an online account,
// it fetches the profile key pair from Mojang and enables signed chat.
func Start(c *proto.Conn, acc *api.Account) (*Module, error) {
m := &Module{Conn: c, Account: acc}
if acc != nil && acc.Ygg != "" {
if err := m.initSigning(acc); err != nil {
return nil, err
}
}
return m, nil
}
func (m *Module) initSigning(acc *api.Account) (err error) {
keys, err := acc.FetchKeys()
if err != nil {
return fmt.Errorf("fetch profile keys: %w", err)
}
priv, err := parsePrivateKey(keys.KeyPair.PrivateKey)
if err != nil {
return fmt.Errorf("parse private key: %w", err)
}
m.privKey = priv
m.sessionUUID = uuid.New()
pubDer, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
return fmt.Errorf("marshal public key: %w", err)
}
sigBytes, err := base64.StdEncoding.DecodeString(keys.PublicKeySignatureV2)
if err != nil {
return fmt.Errorf("decode key signature: %w", err)
}
if err = m.Conn.Send(&v1_21_11.PlayToServerPacketChatSessionUpdate{
SessionUUID: m.sessionUUID,
ExpireTime: keys.ExpiresAt.UnixMilli(),
PublicKey: v1_21_11.ByteArray{Val: pubDer},
Signature: v1_21_11.ByteArray{Val: sigBytes},
}); err != nil {
return fmt.Errorf("send chat session update: %w", err)
}
m.canSign = true
return nil
}
// SendMessage sends a chat message. If signing is enabled, the message is signed;
// otherwise it is sent unsigned.
func (m *Module) SendMessage(message string) (err error) {
if m.canSign {
return m.sendSigned(message)
}
return m.Conn.Send(&v1_21_11.PlayToServerPacketChatMessage{Message: message})
}
func (m *Module) sendSigned(message string) (err error) {
now := time.Now()
saltBytes := make([]byte, 8)
if _, err = rand.Read(saltBytes); err != nil {
return
}
salt := int64(binary.BigEndian.Uint64(saltBytes))
timestamp := now.UnixMilli()
payload, err := m.buildSignaturePayload(message, salt, timestamp)
if err != nil {
return
}
hashed := sha256.Sum256(payload)
sigBytes, err := rsa.SignPKCS1v15(rand.Reader, m.privKey, crypto.SHA256, hashed[:])
if err != nil {
return
}
if len(sigBytes) != 256 {
return fmt.Errorf("unexpected signature length: %d", len(sigBytes))
}
var signature [256]byte
copy(signature[:], sigBytes)
m.msgIndex++
return m.Conn.Send(&v1_21_11.PlayToServerPacketChatMessage{
Message: message,
Timestamp: timestamp,
Salt: salt,
Signature: &signature,
Offset: 0,
Acknowledged: [3]byte{},
Checksum: 0,
})
}
func (m *Module) buildSignaturePayload(content string, salt, timestampMillis int64) ([]byte, error) {
buf := make([]byte, 0, 64+len(content))
w := func(b []byte) { buf = append(buf, b...) }
// Header
w(int32Bytes(1))
// SignedMessageLink: sender UUID, session UUID, index
w(uuidBytes(m.Account.Uuid))
w(uuidBytes(m.sessionUUID))
w(int32Bytes(m.msgIndex))
// SignedMessageBody: salt, timestamp seconds, content
w(int64Bytes(uint64(salt)))
w(int64Bytes(uint64(timestampMillis / 1000)))
contentBytes := []byte(content)
w(int32Bytes(int32(len(contentBytes))))
w(contentBytes)
// LastSeenMessages: empty
w(int32Bytes(0))
return buf, nil
}
func parsePrivateKey(s string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(s))
if block != nil {
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
k, err = x509.ParsePKCS1PrivateKey(block.Bytes)
}
if err != nil {
return nil, err
}
if key, ok := k.(*rsa.PrivateKey); ok {
return key, nil
}
return nil, fmt.Errorf("not an RSA private key")
}
// Some implementations return raw base64 DER instead of PEM
raw, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, err
}
k, err := x509.ParsePKCS8PrivateKey(raw)
if err != nil {
k, err = x509.ParsePKCS1PrivateKey(raw)
}
if err != nil {
return nil, err
}
key, ok := k.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("not an RSA private key")
}
return key, nil
}
func int32Bytes(v int32) []byte {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(v))
return b
}
func int64Bytes(v uint64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, v)
return b
}
func uuidBytes(u uuid.UUID) []byte {
b := make([]byte, 16)
copy(b, u[:])
return b
}
+19
View File
@@ -0,0 +1,19 @@
package keepalive
import (
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
)
type mod struct {
*proto.Conn
}
func (m *mod) onKeepAlive(p *v1_21_8.PlayToClientPacketKeepAlive) (err error) {
return m.Send(&v1_21_8.PlayToServerPacketKeepAlive{KeepAliveId: p.KeepAliveId})
}
func Start(c *proto.Conn) {
m := &mod{Conn: c}
m.RegisterUntilLatest(m.onKeepAlive)
}
+178
View File
@@ -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
}
+57
View File
@@ -0,0 +1,57 @@
package client
import (
"context"
"net"
"github.com/admin-else/strom/mc/data"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/server"
)
// Connect connects to the given address note that it does not resolve the SRV records it's just a raw net.Dial
// You must close the connection when you're done with it
// version must be a vaild minecraft version eg 1.21.11 if left empty it will be detected with a status-request.
func Connect(ctx context.Context, addr string, version string) (ret *proto.Conn, err error) {
var protocolVersion int32
if version == "" {
var s server.StatusResponse
s, err = Status(ctx, addr)
if err != nil {
return
}
protocolVersion = s.Version.Protocol
} else {
var v data.VersionInfo
v, err = data.LookUpVersionByName(version)
if err != nil {
return
}
protocolVersion = v.Version
}
ret, err = ConnectVersionLess(ctx, addr)
if err != nil {
return
}
err = ret.SetProtocolVersion(protocolVersion)
return
}
// ConnectVersionLess connects to the given address without setting the version you should set it
// when using this function.
// You must close the connection when you're done with it
func ConnectVersionLess(ctx context.Context, addr string) (ret *proto.Conn, err error) {
if ctx == nil {
ctx = context.Background()
}
ret = proto.NewConnCtx(ctx)
ret.Actor = proto_base.Client
ret.Conn, err = (&net.Dialer{}).DialContext(ctx, "tcp", addr)
if err != nil {
return
}
ret.R = ret.Conn
ret.W = ret.Conn
return
}
@@ -4,10 +4,10 @@ import (
"errors"
"fmt"
"github.com/admin-else/strom/event"
"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/mc/event"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
)
type ConfigIgnorer struct {
+80
View File
@@ -0,0 +1,80 @@
package client
import (
"context"
"errors"
"net"
"strconv"
"strings"
"time"
)
type DNSCheckFunc func(addr string) error
// DoDNSChecked resolves a DNS name to an IP address
// It should resolve a minecraft compatible address into something net.Dial
// It also optionally accepts a function to "check" the address
func DoDNSChecked(ctx context.Context, in string, f DNSCheckFunc) (connectTo string, host string, portN uint16, err error) {
if f == nil {
f = func(addr string) error { return nil }
}
if ctx == nil {
ctx = context.Background()
}
// https://mcsrc.dev/1/26.1-pre-1/net/minecraft/client/multiplayer/resolver/ServerAddress#L34
host, port, err := net.SplitHostPort(in)
if err != nil {
if addrErr, ok := errors.AsType[*net.AddrError](err); ok && addrErr.Err == "missing port in address" {
err = nil
host = in
port = "25565"
} else {
return
}
}
porti, err := strconv.Atoi(port)
if err != nil {
return
}
err = f(host)
if err != nil {
return
}
portN = uint16(porti)
outIP, err := net.ResolveIPAddr("ip", host)
if err != nil {
return
}
err = f(outIP.String())
if err != nil {
return
}
connectTo = outIP.String() + ":" + port
if portN != 25565 {
return
}
ctx, cancel := context.WithTimeout(ctx, time.Second) // otherwise this takes 10+ seconds
defer cancel()
r := &net.Resolver{}
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
if len(srvs) == 0 {
return
}
host = strings.TrimSuffix(srvs[0].Target, ".")
portN = srvs[0].Port
outIP, err = net.ResolveIPAddr("ip", host)
if err != nil {
return
}
err = f(outIP.String())
if err != nil {
return
}
connectTo = outIP.String() + ":" + port
return
}
+1 -1
View File
@@ -15,7 +15,7 @@ func TestDoDNS(t *testing.T) {
}
for _, c := range cases {
t.Logf("Testing %s", c.Input)
_, host, portN, err := DoDns(c.Input)
_, host, portN, err := DoDNSChecked(nil, c.Input, nil)
if err != nil {
t.Error(err)
continue
@@ -4,9 +4,9 @@ import (
"net"
"strconv"
"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/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
)
func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) {
+48 -17
View File
@@ -1,20 +1,20 @@
package client
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"errors"
"github.com/admin-else/strom/api"
"github.com/admin-else/strom/crypto"
"github.com/admin-else/strom/event"
"github.com/admin-else/strom/proto"
"github.com/admin-else/strom/proto_base"
"github.com/admin-else/strom/text"
"github.com/admin-else/strom/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/api"
"github.com/admin-else/strom/mc/crypto"
"github.com/admin-else/strom/mc/event"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/text"
)
type LoginClient struct {
@@ -156,24 +156,55 @@ func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
return
}
func LoginNoSrv(connectTo string, account *api.Account) (c *proto.Conn, err error) {
c, err = Connect(connectTo)
if err != nil {
return
func WithoutDns() func(ls *loginSettings) {
return func(ls *loginSettings) {
ls.NoDns = true
}
err = LoginRaw(c, account)
return
}
func Login(connectTo string, account *api.Account) (c *proto.Conn, err error) {
connectTo, err = DoDnsSimple(connectTo)
func WithContext(ctx context.Context) func(ls *loginSettings) {
return func(ls *loginSettings) {
ls.Ctx = ctx
}
}
func WithVersion(version string) func(ls *loginSettings) {
return func(ls *loginSettings) {
ls.Version = version
}
}
type loginSettings struct {
NoDns bool
Ctx context.Context
Version string
IgnoreConfig bool
}
func Login(connectTo string, account *api.Account, settings ...func(ls *loginSettings)) (c *proto.Conn, err error) {
ls := &loginSettings{}
for _, setting := range settings {
setting(ls)
}
if !ls.NoDns {
connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil)
if err != nil {
return
}
c, err = Connect(connectTo)
}
c, err = Connect(ls.Ctx, connectTo, ls.Version)
if err != nil {
return
}
err = LoginRaw(c, account)
if err != nil {
return
}
if !ls.IgnoreConfig {
err = IgnoreConfig(c)
}
return
}
+8 -9
View File
@@ -5,12 +5,11 @@ import (
"encoding/json"
"time"
"github.com/admin-else/strom/event"
"github.com/admin-else/strom/proto"
"github.com/admin-else/strom/proto_base"
"github.com/admin-else/strom/server"
"github.com/admin-else/strom/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/event"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/server"
)
type StatusClient struct {
@@ -41,7 +40,7 @@ func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
// So ignore the resource leak warning. And maybe attach a warn ignore comment.
// It does not resolve SRV records.
func StatusRaw(ctx context.Context, addr string) (s *StatusClient, err error) {
c, err := ConnectVersionLess(addr)
c, err := ConnectVersionLess(ctx, addr)
if err != nil {
return
}
@@ -82,8 +81,8 @@ func StatusNoDns(addr string) (status server.StatusResponse, err error) {
}
// Status resolves the status of the minecraft server at the given address.
func Status(addr string) (status server.StatusResponse, err error) {
addr, err = DoDnsSimple(addr)
func Status(ctx context.Context, addr string) (status server.StatusResponse, err error) {
addr, _, _, err = DoDNSChecked(ctx, addr, nil)
if err != nil {
return
}
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"github.com/google/uuid"
)
// UUIDv3 Variation of google.uuid.NewHash
// uuidV3 Variation of google.uuid.NewHash
// We need this because we want a 14-byte namespace and Google forces 16
func UUIDv3(space []byte, data []byte) uuid.UUID {
func uuidV3(space []byte, data []byte) uuid.UUID {
h := md5.New()
h.Reset()
h.Write(space) //nolint:errcheck
@@ -22,5 +22,5 @@ func UUIDv3(space []byte, data []byte) uuid.UUID {
}
func FromOfflinePlayer(displayName string) uuid.UUID {
return UUIDv3([]byte("OfflinePlayer:"), []byte(displayName))
return uuidV3([]byte("OfflinePlayer:"), []byte(displayName))
}
@@ -3,7 +3,7 @@ package data_test
import (
"testing"
"github.com/admin-else/strom/data"
"github.com/admin-else/strom/mc/data"
)
func TestLookupBiomeById(t *testing.T) {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"slices"
"strconv"
"github.com/admin-else/strom/util"
"github.com/admin-else/strom/mc/util"
)
// {
@@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"github.com/admin-else/strom/data"
"github.com/admin-else/strom/mc/data"
)
func TestLookupBlockByStateId(t *testing.T) {
View File
View File
@@ -1630,6 +1630,7 @@
"loginPacket": "pc/1.21.3",
"mapIcons": "pc/1.20.2",
"materials": "pc/1.21.8",
"multi_noise_biome_source_parameter_list": "pc/1.21.8",
"particles": "pc/1.21.8",
"protocol": "pc/1.21.8",
"recipes": "pc/1.21.8",
@@ -1657,6 +1658,7 @@
"loginPacket": "pc/1.21.9",
"mapIcons": "pc/1.20.2",
"materials": "pc/1.21.9",
"multi_noise_biome_source_parameter_list": "pc/1.21.9",
"particles": "pc/1.21.9",
"protocol": "pc/1.21.9",
"recipes": "pc/1.21.9",
@@ -1711,6 +1713,7 @@
"loginPacket": "pc/1.21.11",
"mapIcons": "pc/1.20.2",
"materials": "pc/1.21.11",
"multi_noise_biome_source_parameter_list": "pc/1.21.11",
"particles": "pc/1.21.11",
"protocol": "pc/1.21.11",
"recipes": "pc/1.21.11",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More