Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb5fe8d90 | ||
|
|
d9bca7e517 | ||
|
|
b162d0d401 | ||
|
|
3273244c42 | ||
|
|
f579645b88 | ||
|
|
33754d7c71 | ||
|
|
8bd54a04f2 | ||
|
|
492dae8e83 | ||
|
|
d9543de951 | ||
|
|
f1f5374a5c | ||
|
|
0d6b6e1ab9 | ||
|
|
56c97a2b3f | ||
|
|
9063a0328d | ||
|
|
84b28d4c12 | ||
|
|
30cfc507ba | ||
|
|
d84618c116 | ||
|
|
5a55520126 | ||
|
|
8ac5e00c8e | ||
|
|
d46c678b89 | ||
|
|
0b714edcf5 | ||
|
|
45151add64 | ||
|
|
b90830881f | ||
|
|
94301a4a50 | ||
|
|
aa5d15a4dc | ||
|
|
d388e2c350 | ||
|
|
126ff5bca0 | ||
|
|
a347aedfc2 | ||
|
|
e324a95727 | ||
|
|
7f878f40a1 | ||
|
|
7baa9990cb | ||
|
|
5c5c391aa6 | ||
|
|
b966925212 | ||
|
|
efd7049859 | ||
|
|
3ef9446f50 | ||
|
|
68612969c9 | ||
|
|
b4712ff5c6 | ||
|
|
eab050e291 | ||
|
|
a8b090227b | ||
|
|
201e0dfa44 | ||
|
|
666f250b3e | ||
|
|
b2784574bd | ||
|
|
c0b1501f23 | ||
|
|
4de76d8023 | ||
|
|
f08684fb67 | ||
|
|
9e4f9409c5 | ||
|
|
4d3fbed957 | ||
|
|
b338888b58 | ||
|
|
4956c1d874 | ||
|
|
d4fe362d9f | ||
|
|
5d8ff870df | ||
|
|
58c9a60236 | ||
|
|
3e5b141ec7 | ||
|
|
efeb5ecda6 | ||
|
|
624e8ad4de | ||
|
|
764cfd8c4d | ||
|
|
d43ff7f83d | ||
|
|
a39999b344 | ||
|
|
8ee48e1a45 | ||
|
|
bf6affeb97 | ||
|
|
ede9a47ca2 | ||
|
|
2fb290572a | ||
|
|
021b8ddc1a | ||
|
|
05e8da2ff4 | ||
|
|
a8936d92a5 | ||
|
|
c56898e787 | ||
|
|
c483bf9553 | ||
|
|
b5ee17d1ae | ||
|
|
57cef25e27 | ||
|
|
148302c94d | ||
|
|
02c402251d | ||
|
|
c800c5d471 | ||
|
|
477f51cce7 | ||
|
|
60103ebf47 | ||
|
|
21eeebf0f7 | ||
|
|
bb6f6548d5 | ||
|
|
d6c07cf428 | ||
|
|
e98921755d | ||
|
|
60383fede1 | ||
|
|
3bfa70d384 | ||
|
|
60147fd4b6 | ||
|
|
fe51287cc6 | ||
|
|
a44c5f8606 | ||
|
|
d5979b2c3d | ||
|
|
4b95ea9ae2 | ||
|
|
a80fd29297 | ||
|
|
589f35d147 | ||
|
|
c7b5f6ba0f | ||
|
|
0600315e69 | ||
|
|
12005df68b | ||
|
|
600842a679 | ||
|
|
c93d907d46 | ||
|
|
be6d2fb461 | ||
|
|
b2df88203d | ||
|
|
62d12386e1 | ||
|
|
00ea02a529 | ||
|
|
e5231dfdb6 | ||
|
|
3b1b436ecf | ||
|
|
47180bf650 | ||
|
|
325db98b99 | ||
|
|
ceae543352 | ||
|
|
aef9615476 | ||
|
|
5d3860b91e |
@@ -1,2 +1,6 @@
|
||||
**/*_test.go export-ignore
|
||||
**/testdata export-ignore
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.tmcpr filter=lfs diff=lfs merge=lfs -text
|
||||
*.mcpr filter=lfs diff=lfs merge=lfs -text
|
||||
*.mca filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -14,15 +14,22 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout with LFS
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.25'
|
||||
go-version: '1.26.2'
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Vet
|
||||
run: go vet -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.devres/
|
||||
.idea/
|
||||
.failed_packets/
|
||||
tools/.*/
|
||||
.vscode/
|
||||
@@ -1,10 +0,0 @@
|
||||
package api
|
||||
|
||||
import "github.com/admin-else/strom/crypto"
|
||||
|
||||
func NewOfflineAccount(name string) *Account {
|
||||
return &Account{
|
||||
Name: name,
|
||||
Uuid: crypto.FromOfflinePlayer(name),
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
type LoginClient struct {
|
||||
*proto.Conn
|
||||
Account *api.Account
|
||||
ServerHost string
|
||||
ServerPort uint16
|
||||
|
||||
GivenAccount *v1_21_8.LoginToClientPacketSuccess
|
||||
}
|
||||
|
||||
var (
|
||||
NoTokenErr = errors.New("the account has no token so we cant join online servers")
|
||||
BadPublicKeyTypeErr = errors.New("the public key is not rsa")
|
||||
FailedToLoginErr = errors.New("failed to login")
|
||||
)
|
||||
|
||||
type KickedDuringLoginErr struct {
|
||||
text.Component
|
||||
}
|
||||
|
||||
func (k KickedDuringLoginErr) Error() string {
|
||||
return k.String()
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnDefault(event event.Unhandled) (err error) {
|
||||
if _, ok := event.Val.(proto_base.EncodeDecodeAble); !ok {
|
||||
return
|
||||
}
|
||||
s.Log.Warn("unexpected packet during login client", "packet", event.Val)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnClose(_ event.Close) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnCompress(compress *v1_21_8.LoginToClientPacketCompress) (err error) {
|
||||
s.SetCompressionThreshold(compress.Threshold)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnEncrypt(packet *v1_21_8.LoginToClientPacketEncryptionBegin) (err error) {
|
||||
sharedSecret := make([]byte, 16)
|
||||
_, _ = rand.Read(sharedSecret) //never fails
|
||||
|
||||
if packet.ShouldAuthenticate {
|
||||
if s.Account.Ygg == "" {
|
||||
err = NoTokenErr
|
||||
return
|
||||
}
|
||||
serverId := crypto.AuthDigest([]byte(packet.ServerId), sharedSecret, packet.PublicKey)
|
||||
err = s.Account.JoinServer(serverId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pubAny, err := x509.ParsePKIXPublicKey(packet.PublicKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pub, ok := pubAny.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
err = BadPublicKeyTypeErr
|
||||
return
|
||||
}
|
||||
verifyTokenEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, packet.VerifyToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sharedSecretEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, sharedSecret)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(&v1_21_8.LoginToServerPacketEncryptionBegin{SharedSecret: sharedSecretEnc, VerifyToken: verifyTokenEnc})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.SetSecret(sharedSecret)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnDisconnect(packet *v1_21_8.LoginToClientPacketDisconnect) (err error) {
|
||||
var reason text.Component
|
||||
err = json.Unmarshal([]byte(packet.Reason), &reason)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = KickedDuringLoginErr{reason}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnSuccess(success *v1_21_8.LoginToClientPacketSuccess) (err error) {
|
||||
s.GivenAccount = success
|
||||
err = s.Send(&v1_21_8.LoginToServerPacketLoginAcknowledged{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.SetState(proto_base.Configuration)
|
||||
err = event.HandlerDoneErr{}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnStart() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
|
||||
lc := &LoginClient{
|
||||
Conn: c,
|
||||
Account: account,
|
||||
}
|
||||
lc.RegisterCritical(lc.OnDefault)
|
||||
lc.RegisterCritical(lc.OnClose)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnCompress)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnEncrypt)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnDisconnect)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnSuccess)
|
||||
|
||||
p, err := MakeHandshakePacket(lc.Conn, proto_base.Login)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = lc.Send(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lc.SetState(proto_base.Login)
|
||||
err = lc.Send(&v1_21_8.LoginToServerPacketLoginStart{Username: lc.Account.Name, PlayerUUID: lc.Account.Uuid})
|
||||
|
||||
err = lc.StartConn()
|
||||
if err != nil {
|
||||
err = errors.Join(FailedToLoginErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LoginNoSrv(connectTo string, account *api.Account) (c *proto.Conn, err error) {
|
||||
c, err = Connect(connectTo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = LoginRaw(c, account)
|
||||
return
|
||||
}
|
||||
|
||||
func Login(connectTo string, account *api.Account) (c *proto.Conn, err error) {
|
||||
connectTo, err = DoDnsSimple(connectTo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, err = Connect(connectTo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = LoginRaw(c, account)
|
||||
return
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+46
-25
@@ -1,48 +1,74 @@
|
||||
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/raw_capture" // removed: use tcpflow instead for raw TCP capture
|
||||
"github.com/admin-else/strom/cmd/strom/raw_to_tmcpr"
|
||||
"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
|
||||
}
|
||||
|
||||
func ToolSum() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
fmt.Println("strom cli ", info.Main.Version, " (", info.Main.Sum, ")")
|
||||
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{
|
||||
"offline-uuid": offline_uuid.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,
|
||||
"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,
|
||||
// "raw-capture": raw_capture.Run, // removed: use tcpflow instead for raw TCP capture, then raw-to-tmcpr
|
||||
"raw-to-tmcpr": raw_to_tmcpr.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) {
|
||||
@@ -53,14 +79,9 @@ func Help(args []string) (err error) {
|
||||
}
|
||||
|
||||
func mainE() (err error) {
|
||||
subcommands["help"] = Help // this is here because if it isn't tools/cli/main.go:17:5: initialization cycle for subcommands
|
||||
// tools/cli/main.go:17:5: subcommands refers to Help
|
||||
// tools/cli/main.go:22:6: Help refers to subcommands
|
||||
subcommands["help"] = Help
|
||||
args := os.Args
|
||||
|
||||
// Debug stuff
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
//args = []string{"TEST", "version"}
|
||||
//args = []string{"TEST", "messenger", "-addr", "localhost"}
|
||||
|
||||
if len(args) < 2 {
|
||||
err = ExpectedASubcommandErr
|
||||
|
||||
@@ -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("./.devres/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
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_8"
|
||||
)
|
||||
|
||||
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")
|
||||
versionFlag = cmd.String("version", "26.2", "minecraft version to use (e.g. 1.8, 1.21.11)")
|
||||
)
|
||||
|
||||
type Messenger struct {
|
||||
*proto.Conn
|
||||
chat *chat.Module
|
||||
world *world.Module
|
||||
legacy bool
|
||||
}
|
||||
|
||||
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) OnChatLegacy(e *v1_8.PlayToClientPacketChat) (err error) {
|
||||
m.Log.Info("chat", "m", e.Message)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Messenger) OnStdin(e event.Stdin) (err error) {
|
||||
input := strings.TrimSpace(e.Val)
|
||||
if strings.HasPrefix(input, ">say ") {
|
||||
return m.sendChat(strings.TrimSpace(strings.TrimPrefix(input, ">say")))
|
||||
}
|
||||
if !m.legacy && strings.HasPrefix(input, ">getblock ") {
|
||||
return m.handleGetBlock(strings.TrimSpace(strings.TrimPrefix(input, ">getblock")))
|
||||
}
|
||||
return m.sendChat(input)
|
||||
}
|
||||
|
||||
func (m *Messenger) sendChat(message string) (err error) {
|
||||
if m.legacy {
|
||||
return m.Send(&v1_8.PlayToServerPacketChat{Message: message})
|
||||
}
|
||||
return m.chat.SendMessage(message)
|
||||
}
|
||||
|
||||
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, client.WithVersion(*versionFlag))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
isLegacy := c.ProtocolVersion < 764
|
||||
|
||||
var chatMod *chat.Module
|
||||
if !isLegacy {
|
||||
chatMod, err = chat.Start(c, acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
m := &Messenger{
|
||||
Conn: c,
|
||||
chat: chatMod,
|
||||
legacy: isLegacy,
|
||||
}
|
||||
|
||||
if !isLegacy {
|
||||
w := world.NewWorld(c.Version, -64, 384)
|
||||
m.world = world.Start(c, w)
|
||||
}
|
||||
|
||||
event.StartListingStdin(m.Loop)
|
||||
m.Register(m.OnStdin)
|
||||
|
||||
if isLegacy {
|
||||
m.Register(m.OnChatLegacy)
|
||||
} else {
|
||||
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
|
||||
}
|
||||
|
||||
if !isLegacy {
|
||||
keepalive.Start(m.Conn)
|
||||
}
|
||||
|
||||
return m.StartConn()
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package packet_info
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"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)
|
||||
var versionFlag = cmd.String("version", "", "protocol version or minecraft version name")
|
||||
var stateFlag = cmd.String("state", "", "handshaking, status, login, configuration, play")
|
||||
var directionFlag = cmd.String("direction", "", "toserver, toclient")
|
||||
var idFlag = cmd.Int("id", -1, "packet id")
|
||||
var nameFlag = cmd.String("name", "", "packet name")
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var version int32
|
||||
if *versionFlag != "" {
|
||||
v, err := data.LookUpVersionByName(*versionFlag)
|
||||
if err != nil {
|
||||
v2, err := parseVersion(*versionFlag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid version: %s", *versionFlag)
|
||||
}
|
||||
v, err = data.LookUpVersionByProtocolVersion(v2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
version = v.Version
|
||||
}
|
||||
|
||||
var state proto_base.State
|
||||
if *stateFlag != "" {
|
||||
state, err = parseState(*stateFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var direction proto_base.Direction
|
||||
if *directionFlag != "" {
|
||||
direction, err = parseDirection(*directionFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if *idFlag != -1 && *nameFlag != "" {
|
||||
err = fmt.Errorf("cannot specify both id and name")
|
||||
return
|
||||
}
|
||||
|
||||
if *idFlag != -1 {
|
||||
if *versionFlag == "" || *stateFlag == "" || *directionFlag == "" {
|
||||
err = fmt.Errorf("version, state, and direction are required when looking up by id")
|
||||
return
|
||||
}
|
||||
p, ok := proto.LookUpTypeByPacketInfo(direction, state, int32(*idFlag), version)
|
||||
if !ok {
|
||||
err = fmt.Errorf("packet not found")
|
||||
return
|
||||
}
|
||||
printPacketInfo(p)
|
||||
return
|
||||
}
|
||||
|
||||
if *nameFlag != "" {
|
||||
if *versionFlag == "" {
|
||||
err = fmt.Errorf("version is required when looking up by name")
|
||||
return
|
||||
}
|
||||
if state != 0 || direction != 0 {
|
||||
p, ok := proto.LookupPacketInfoByNameProtocolVersionStateAndDirection(*nameFlag, version, state, direction)
|
||||
if !ok {
|
||||
err = fmt.Errorf("packet not found")
|
||||
return
|
||||
}
|
||||
printPacketInfo(p)
|
||||
} else {
|
||||
var found bool
|
||||
for _, p := range proto_generated.Packets {
|
||||
if p.Name == *nameFlag && p.ProtocolVersion == version {
|
||||
printPacketInfo(p)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = fmt.Errorf("packet not found")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = fmt.Errorf("must specify either -id or -name")
|
||||
return
|
||||
}
|
||||
|
||||
func parseState(s string) (proto_base.State, error) {
|
||||
switch s {
|
||||
case "handshaking":
|
||||
return proto_base.Handshaking, nil
|
||||
case "status":
|
||||
return proto_base.Status, nil
|
||||
case "login":
|
||||
return proto_base.Login, nil
|
||||
case "configuration":
|
||||
return proto_base.Configuration, nil
|
||||
case "play":
|
||||
return proto_base.Play, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown state: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func parseDirection(s string) (proto_base.Direction, error) {
|
||||
switch s {
|
||||
case "toserver":
|
||||
return proto_base.ToServer, nil
|
||||
case "toclient":
|
||||
return proto_base.ToClient, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown direction: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func parseVersion(val string) (int32, error) {
|
||||
v, err := strconv.ParseInt(val, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int32(v), nil
|
||||
}
|
||||
|
||||
func printPacketInfo(p proto_base.PacketInfo) {
|
||||
v, _ := data.LookUpVersionByProtocolVersion(p.ProtocolVersion)
|
||||
fmt.Printf("Name: %s\n", p.Name)
|
||||
fmt.Printf("ID: %d\n", p.PacketId)
|
||||
fmt.Printf("State: %s\n", stateToString(p.State))
|
||||
fmt.Printf("Direction: %s\n", p.Direction)
|
||||
fmt.Printf("Version: %d (%s)\n", p.ProtocolVersion, v.MinecraftVersion)
|
||||
}
|
||||
|
||||
func stateToString(s proto_base.State) string {
|
||||
switch s {
|
||||
case proto_base.Handshaking:
|
||||
return "handshaking"
|
||||
case proto_base.Status:
|
||||
return "status"
|
||||
case proto_base.Login:
|
||||
return "login"
|
||||
case proto_base.Configuration:
|
||||
return "configuration"
|
||||
case proto_base.Play:
|
||||
return "play"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
@@ -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 *Token != "" {
|
||||
acc, err = api.NewAccountFromYGG(*Token)
|
||||
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(), server.WithoutEncryption())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = server.ServeLogin(serveeConn, server.WithOtherAccount(acc), server.WithStatus(StatusResponse), server.WithoutOnlineMode())
|
||||
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
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/nbt"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+3
-3
@@ -4,13 +4,13 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
generated "github.com/admin-else/strom/mc/proto_generated/%v"
|
||||
)
|
||||
|
||||
// Error: %v
|
||||
func TestFailedPacket%10X(t *testing.T) {
|
||||
p := v1_21_8.Play%vPacket{}
|
||||
b := bytes.NewBuffer(%#v)
|
||||
p := &generated.%v{}
|
||||
b := bytes.NewBuffer(%#v)
|
||||
err := p.Decode(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -0,0 +1,217 @@
|
||||
package print_replay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("print-replay", flag.ContinueOnError)
|
||||
FileFlag = cmd.String("file", "", "The tmcpr file to print")
|
||||
ProtocolVersion = cmd.Int("protocol", -1, "The protocol version")
|
||||
CreateTestsPath = cmd.String("create-tests", "", "")
|
||||
StateFlag = cmd.String("state", "login", "Initial state: handshake, login, config, status, play")
|
||||
DirectionFlag = cmd.String("direction", "clientbound", "Packet direction: clientbound (ToClient) or serverbound (ToServer)")
|
||||
)
|
||||
|
||||
// TODO: maybe use the proto replay api
|
||||
|
||||
//go:embed failed_packet.go.tmpl
|
||||
var TestSrcF string
|
||||
|
||||
func SaveUnCodeAbleAsTest(packet *proto.UnCodablePacket) (err error) {
|
||||
if packet.Info.Type == nil {
|
||||
slog.Warn("skipping test for unknown packet id", "packet_id", packet.Info.PacketId, "state", packet.Info.State, "error", packet.Err)
|
||||
return
|
||||
}
|
||||
|
||||
hUntrimmed := sha256.Sum256(packet.Data)
|
||||
h := hUntrimmed[:8]
|
||||
if err = os.MkdirAll(*CreateTestsPath, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret, err := data.LookUpVersionByProtocolVersion(packet.Info.ProtocolVersion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
goPackage := "v" + strings.ReplaceAll(ret.MinecraftVersion, ".", "_")
|
||||
typeName := reflect.TypeOf(packet.Info.Type).Elem().Name()
|
||||
|
||||
src := fmt.Sprintf(TestSrcF, goPackage, packet.Err, h, typeName, packet.Data)
|
||||
formatted, formatErr := format.Source([]byte(src))
|
||||
if formatErr != nil {
|
||||
formatted = []byte(src)
|
||||
}
|
||||
|
||||
filePath := fmt.Sprintf(*CreateTestsPath+"/%v_%10x_test.go", len(packet.Data), h)
|
||||
if err = os.WriteFile(filePath, formatted, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
slog.Info("Packet failed to parse", "error", packet.Err, "saved", filePath, "data", fmt.Sprintf("%q", packet.Data))
|
||||
return
|
||||
}
|
||||
|
||||
var state = proto_base.Login
|
||||
var direction = proto_base.ToClient
|
||||
|
||||
func UnpackPacket(r io.Reader) (packet proto_base.EncodeDecodeAble, i proto_base.PacketInfo, err error) {
|
||||
var timestamp, length uint32
|
||||
err = binary.Read(r, binary.BigEndian, ×tamp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Read the packet data into a byte slice
|
||||
packetData := make([]byte, length)
|
||||
_, err = io.ReadFull(r, packetData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Use bytes.Reader instead of bytes.Buffer
|
||||
b := bytes.NewReader(packetData)
|
||||
|
||||
packetId, err := proto_base.DecodeVarInt(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate position after reading the packet ID
|
||||
// indexafterid represents how many bytes we've read so far
|
||||
indexafterid := int(b.Size()) - b.Len()
|
||||
|
||||
var ok bool
|
||||
i, ok = proto.LookUpTypeByPacketInfo(direction, state, packetId, int32(*ProtocolVersion))
|
||||
if !ok {
|
||||
packet = &proto.UnCodablePacket{Err: proto.BadPacketIdErr, Data: packetData, Info: i}
|
||||
return
|
||||
}
|
||||
packet = reflect.New(reflect.TypeOf(i.Type).Elem()).Interface().(proto_base.EncodeDecodeAble)
|
||||
err = packet.Decode(b)
|
||||
if err != nil {
|
||||
packet = &proto.UnCodablePacket{Err: err, Data: packetData[indexafterid:], Info: i}
|
||||
return
|
||||
}
|
||||
if b.Len() != 0 {
|
||||
packet = &proto.UnCodablePacket{Err: proto.PacketNotFullyDecodedErr, Data: packetData[indexafterid:], Info: i}
|
||||
return
|
||||
}
|
||||
trackState(packet, i)
|
||||
return
|
||||
}
|
||||
|
||||
func trackState(packet proto_base.EncodeDecodeAble, i proto_base.PacketInfo) {
|
||||
switch i.Name {
|
||||
case "set_protocol":
|
||||
v := reflect.ValueOf(packet).Elem()
|
||||
nsField := v.FieldByName("NextState")
|
||||
if nsField.IsValid() && nsField.Kind() == reflect.Int32 {
|
||||
switch nsField.Int() {
|
||||
case 1:
|
||||
state = proto_base.Status
|
||||
case 2:
|
||||
state = proto_base.Login
|
||||
}
|
||||
}
|
||||
case "login_start":
|
||||
if int32(*ProtocolVersion) <= 47 {
|
||||
state = proto_base.Play
|
||||
}
|
||||
case "success":
|
||||
if int32(*ProtocolVersion) >= 764 {
|
||||
state = proto_base.Configuration
|
||||
} else {
|
||||
state = proto_base.Play
|
||||
}
|
||||
case "finish_configuration":
|
||||
state = proto_base.Play
|
||||
case "login_acknowledged":
|
||||
state = proto_base.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *ProtocolVersion == -1 {
|
||||
return fmt.Errorf("no protocol version specified")
|
||||
}
|
||||
if *FileFlag == "" {
|
||||
return fmt.Errorf("no file specified")
|
||||
}
|
||||
switch *StateFlag {
|
||||
case "handshake":
|
||||
state = proto_base.Handshaking
|
||||
case "login":
|
||||
state = proto_base.Login
|
||||
case "config":
|
||||
state = proto_base.Configuration
|
||||
case "play":
|
||||
state = proto_base.Play
|
||||
case "status":
|
||||
state = proto_base.Status
|
||||
default:
|
||||
return fmt.Errorf("unknown state: %s", *StateFlag)
|
||||
}
|
||||
switch *DirectionFlag {
|
||||
case "clientbound", "to-client":
|
||||
direction = proto_base.ToClient
|
||||
case "serverbound", "to-server":
|
||||
direction = proto_base.ToServer
|
||||
default:
|
||||
return fmt.Errorf("unknown direction: %s", *DirectionFlag)
|
||||
}
|
||||
f, err := os.Open(*FileFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var totalPackets, failedPackets int
|
||||
for {
|
||||
var packet proto_base.EncodeDecodeAble
|
||||
var info proto_base.PacketInfo
|
||||
packet, info, err = UnpackPacket(f)
|
||||
unCodablePacket, ok := packet.(*proto.UnCodablePacket)
|
||||
if ok {
|
||||
slog.Error("failed to decode packet", "error", err, "packet_name", info.Name)
|
||||
if *CreateTestsPath != "" {
|
||||
err = SaveUnCodeAbleAsTest(unCodablePacket)
|
||||
if err != nil {
|
||||
slog.Error("failed to save uncodable packet as test", "error", err)
|
||||
}
|
||||
}
|
||||
failedPackets++
|
||||
}
|
||||
|
||||
fmt.Printf("Packet: %#v\n", packet)
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
break
|
||||
}
|
||||
totalPackets++
|
||||
}
|
||||
fmt.Printf("Total packets: %d, failed packets: %d, %%failed: %.2f%%\n", totalPackets, failedPackets, float64(failedPackets)/float64(totalPackets)*100)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package raw_to_tmcpr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/proto/replay"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
)
|
||||
|
||||
var Cmd = flag.NewFlagSet("raw-to-tmcpr", flag.ContinueOnError)
|
||||
|
||||
var ClientboundIn = Cmd.String("clientbound", "", "raw tcpflow file for clientbound (server->client) direction")
|
||||
var ServerboundIn = Cmd.String("serverbound", "", "raw tcpflow file for serverbound (client->server) direction")
|
||||
var ClientboundOut = Cmd.String("clientbound-out", "clientbound.tmcpr", "output TMCPR file for clientbound")
|
||||
var ServerboundOut = Cmd.String("serverbound-out", "serverbound.tmcpr", "output TMCPR file for serverbound")
|
||||
var ClientboundState = Cmd.String("clientbound-state", "login", "initial state for clientbound: handshake, login, status, play")
|
||||
var ServerboundState = Cmd.String("serverbound-state", "handshake", "initial state for serverbound: handshake, login, status, play")
|
||||
|
||||
type captureReader struct {
|
||||
r io.Reader
|
||||
out *replay.Writer
|
||||
state proto_base.State
|
||||
threshold int32
|
||||
startTime time.Time
|
||||
haveStartTime bool
|
||||
direction proto_base.Direction
|
||||
pendingThreshold int32
|
||||
}
|
||||
|
||||
func (cr *captureReader) setStartTime() {
|
||||
if !cr.haveStartTime {
|
||||
cr.startTime = time.Now()
|
||||
cr.haveStartTime = true
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *captureReader) timestamp() uint32 {
|
||||
return uint32(time.Since(cr.startTime).Milliseconds())
|
||||
}
|
||||
|
||||
func readPacketBytes(r io.Reader, threshold int32) ([]byte, error) {
|
||||
packetLen, err := proto_base.DecodeVarInt(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet length: %w", err)
|
||||
}
|
||||
|
||||
rawBytes := make([]byte, packetLen)
|
||||
_, err = io.ReadFull(r, rawBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet data: %w", err)
|
||||
}
|
||||
|
||||
if threshold >= 0 {
|
||||
buf := bytes.NewReader(rawBytes)
|
||||
uncompressedLen, err := proto_base.DecodeVarInt(buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read uncompressed length: %w", err)
|
||||
}
|
||||
if uncompressedLen == 0 {
|
||||
return io.ReadAll(buf)
|
||||
} else {
|
||||
zReader, err := zlib.NewReader(buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zlib reader: %w", err)
|
||||
}
|
||||
defer zReader.Close()
|
||||
decompressed, err := io.ReadAll(zReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decompress: %w", err)
|
||||
}
|
||||
return decompressed, nil
|
||||
}
|
||||
}
|
||||
|
||||
return rawBytes, nil
|
||||
}
|
||||
|
||||
func (cr *captureReader) trackState(packetBytes []byte) {
|
||||
buf := bytes.NewReader(packetBytes)
|
||||
packetId, err := proto_base.DecodeVarInt(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch cr.state {
|
||||
case proto_base.Handshaking:
|
||||
if cr.direction == proto_base.ToServer && packetId == 0 {
|
||||
_, err = proto_base.DecodeVarInt(buf) // protocol version
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = proto_base.DecodeString(buf) // server address
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
portBytes := make([]byte, 2)
|
||||
_, err = io.ReadFull(buf, portBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nextState, err := proto_base.DecodeVarInt(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if nextState == 2 {
|
||||
cr.state = proto_base.Login
|
||||
} else if nextState == 1 {
|
||||
cr.state = proto_base.Status
|
||||
}
|
||||
}
|
||||
case proto_base.Login:
|
||||
if cr.direction == proto_base.ToClient {
|
||||
if packetId == 0x03 {
|
||||
threshold, err := proto_base.DecodeVarInt(buf)
|
||||
if err == nil {
|
||||
cr.threshold = threshold
|
||||
}
|
||||
} else if packetId == 0x02 {
|
||||
cr.state = proto_base.Play
|
||||
}
|
||||
} else if cr.direction == proto_base.ToServer && packetId == 0 {
|
||||
if cr.pendingThreshold >= 0 {
|
||||
cr.threshold = cr.pendingThreshold
|
||||
}
|
||||
}
|
||||
case proto_base.Status:
|
||||
case proto_base.Play:
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *captureReader) process() (count int, err error) {
|
||||
for {
|
||||
packetBytes, err := readPacketBytes(cr.r, cr.threshold)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return count, nil
|
||||
}
|
||||
return count, err
|
||||
}
|
||||
|
||||
cr.setStartTime()
|
||||
cr.trackState(packetBytes)
|
||||
|
||||
err = cr.out.WriteRaw(packetBytes, cr.timestamp())
|
||||
if err != nil {
|
||||
return count, fmt.Errorf("write TMCPR: %w", err)
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = Cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if *ClientboundIn == "" && *ServerboundIn == "" {
|
||||
return fmt.Errorf("at least one of -clientbound or -serverbound must be specified")
|
||||
}
|
||||
|
||||
parseState := func(name string) (proto_base.State, error) {
|
||||
switch name {
|
||||
case "handshake":
|
||||
return proto_base.Handshaking, nil
|
||||
case "login":
|
||||
return proto_base.Login, nil
|
||||
case "play":
|
||||
return proto_base.Play, nil
|
||||
case "status":
|
||||
return proto_base.Status, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown state: %s (valid: handshake, login, status, play)", name)
|
||||
}
|
||||
}
|
||||
|
||||
var compressionThreshold int32 = -1
|
||||
|
||||
if *ClientboundIn != "" {
|
||||
state, err := parseState(*ClientboundState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compressionThreshold, err = processFile(*ClientboundIn, *ClientboundOut, state, proto_base.ToClient, -1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clientbound: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if *ServerboundIn != "" {
|
||||
state, err := parseState(*ServerboundState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = processFile(*ServerboundIn, *ServerboundOut, state, proto_base.ToServer, compressionThreshold)
|
||||
if err != nil {
|
||||
return fmt.Errorf("serverbound: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func processFile(inPath, outPath string, initial proto_base.State, dir proto_base.Direction, initialThreshold int32) (finalThreshold int32, err error) {
|
||||
in, err := os.Open(inPath)
|
||||
if err != nil {
|
||||
return initialThreshold, fmt.Errorf("open input: %w", err)
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return initialThreshold, fmt.Errorf("create output: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
cr := &captureReader{
|
||||
r: in,
|
||||
out: replay.NewWriter(out),
|
||||
state: initial,
|
||||
threshold: -1,
|
||||
direction: dir,
|
||||
pendingThreshold: initialThreshold,
|
||||
}
|
||||
if dir == proto_base.ToClient {
|
||||
cr.threshold = initialThreshold
|
||||
}
|
||||
|
||||
count, err := cr.process()
|
||||
if err != nil {
|
||||
return cr.threshold, err
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %d packets written to %s (threshold=%d)\n", inPath, count, outPath, cr.threshold)
|
||||
return cr.threshold, nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package serve_world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
*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("./.devres/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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module github.com/admin-else/strom
|
||||
|
||||
go 1.25
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // FIXME: this should be replaced with my version
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package anvil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/admin-else/strom/data"
|
||||
"github.com/admin-else/strom/level"
|
||||
"github.com/admin-else/strom/util"
|
||||
)
|
||||
|
||||
type Chunk struct {
|
||||
DataVersion int32
|
||||
Heightmaps map[string][]int64
|
||||
InhabitedTime int64
|
||||
XPos, YPos, ZPos int32
|
||||
Sections []Section
|
||||
}
|
||||
|
||||
const McPrefix = "minecraft:"
|
||||
|
||||
func (c Chunk) ToStorage(version string) (toChunk *level.Chunk, err error) {
|
||||
f := level.MakeBlockFormat(version)
|
||||
toChunk = new(level.Chunk)
|
||||
|
||||
toChunk.Sections = make([]level.Section, len(c.Sections))
|
||||
for i, s := range c.Sections {
|
||||
pallete := make([]int32, len(s.BlockStates.Palette))
|
||||
for j, p := range s.BlockStates.Palette {
|
||||
pallete[j], err = data.StateIdFromBlocKAndStateMap(version, strings.TrimPrefix(p.Name, McPrefix), p.Properties)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// this is probably fine
|
||||
uintSlice := unsafe.Slice((*uint64)(unsafe.Pointer(unsafe.SliceData(s.BlockStates.Data))), len(s.BlockStates.Data))
|
||||
|
||||
toChunk.Sections[i].Blocks, err = f.Import(uintSlice, util.BpeByNum(float64(len(s.BlockStates.Palette))), pallete)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
toChunk.Sections[i].BlockCount = int16(level.CountStorage(toChunk.Sections[i].Blocks, 0))
|
||||
}
|
||||
|
||||
for i, s := range c.Sections {
|
||||
pallete := make([]int32, len(s.Biomes.Palette))
|
||||
for j, p := range s.Biomes.Palette {
|
||||
var b *data.Biome
|
||||
b, ok := data.LookupBiomeByName(version, strings.TrimPrefix(p, McPrefix))
|
||||
if !ok {
|
||||
err = data.UnknownBiomeNameErr
|
||||
return
|
||||
}
|
||||
pallete[j] = b.Id
|
||||
}
|
||||
// this is probably fine
|
||||
uintSlice := unsafe.Slice((*uint64)(unsafe.Pointer(unsafe.SliceData(s.Biomes.Data))), len(s.Biomes.Data))
|
||||
|
||||
toChunk.Sections[i].Biomes, err = f.Import(uintSlice, util.BpeByNum(float64(len(pallete))), pallete)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Section struct {
|
||||
Y int8
|
||||
BlockStates struct {
|
||||
Data []int64 `nbt:"data,omitempty"`
|
||||
Palette []struct {
|
||||
Name string
|
||||
Properties map[string]string `nbt:"properties,omitempty"`
|
||||
}
|
||||
}
|
||||
Biomes struct {
|
||||
Data []int64 `nbt:"data,omitempty"`
|
||||
Palette []string
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package anvil_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/admin-else/strom/level/anvil"
|
||||
"github.com/admin-else/strom/util"
|
||||
)
|
||||
|
||||
func TestLevel(t *testing.T) {
|
||||
f, err := os.Open("./testdata/testworld/level.dat")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
l, err := anvil.ReadLevelData(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(l)
|
||||
}
|
||||
|
||||
func TestRegionFile(t *testing.T) {
|
||||
f, err := os.Open("./testdata/testworld/region/r.0.0.mca")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
ch, err := anvil.ReadChunkHeader(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n, err := ch.ChunkAt(f, 0, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(string(util.MustT(json.MarshalIndent(n, "", " "))))
|
||||
fmt.Println("Chunk loaded successfully")
|
||||
}
|
||||
|
||||
func TestConverToPacket(t *testing.T) {
|
||||
f, err := os.Open("./level/anvil/testdata/r.0.0.mca")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-215
@@ -1,215 +0,0 @@
|
||||
{
|
||||
"minecraft:recipes/building_blocks/polished_deepslate": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tile_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_brick_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/combat/stone_sword": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_axe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:story/mine_stone": {
|
||||
"criteria": {
|
||||
"get_stone": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/furnace": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/chiseled_deepslate_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/crafting_table": {
|
||||
"criteria": {
|
||||
"unlock_right_away": "2026-01-18 19:40:40 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_slab": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/cobbled_deepslate_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_campfire_cooking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-01-18 19:40:40 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/cobbled_deepslate_wall": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/polished_deepslate_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/deepslate_brick_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-01-18 19:40:40 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_pickaxe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_brick_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tile_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/misc/charcoal": {
|
||||
"criteria": {
|
||||
"has_log": "2026-01-18 19:41:43 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_stairs": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/deepslate_tile_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/oak_planks": {
|
||||
"criteria": {
|
||||
"has_logs": "2026-01-18 19:41:43 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_smoking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-01-18 19:40:40 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_bricks_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:adventure/adventuring_time": {
|
||||
"criteria": {
|
||||
"minecraft:forest": "2026-01-18 19:40:40 +0100",
|
||||
"minecraft:deep_dark": "2026-01-18 19:41:56 +0100",
|
||||
"minecraft:stony_shore": "2026-01-18 19:40:46 +0100"
|
||||
},
|
||||
"done": false
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tiles_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_hoe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_shovel": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-01-18 19:41:36 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"DataVersion": 4556
|
||||
}
|
||||
-220
@@ -1,220 +0,0 @@
|
||||
{
|
||||
"minecraft:recipes/building_blocks/polished_deepslate": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tile_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_brick_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/combat/stone_sword": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_axe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:story/mine_stone": {
|
||||
"criteria": {
|
||||
"get_stone": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/furnace": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/chiseled_deepslate_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/crafting_table": {
|
||||
"criteria": {
|
||||
"unlock_right_away": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_slab": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/cobbled_deepslate_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_campfire_cooking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/cobbled_deepslate_wall": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/polished_deepslate_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/deepslate_brick_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_pickaxe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_brick_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tile_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/misc/charcoal": {
|
||||
"criteria": {
|
||||
"has_log": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_stairs": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/combat/stone_spear": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/deepslate_tile_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/oak_planks": {
|
||||
"criteria": {
|
||||
"has_logs": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_smoking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_bricks_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:adventure/adventuring_time": {
|
||||
"criteria": {
|
||||
"minecraft:stony_shore": "2026-03-11 19:22:59 +0100",
|
||||
"minecraft:plains": "2026-03-11 19:22:38 +0100"
|
||||
},
|
||||
"done": false
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tiles_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_hoe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_shovel": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-11 19:22:37 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"DataVersion": 4671
|
||||
}
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
{
|
||||
"minecraft:recipes/building_blocks/polished_deepslate": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tile_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_brick_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/combat/stone_sword": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_axe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:story/mine_stone": {
|
||||
"criteria": {
|
||||
"get_stone": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/furnace": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/chiseled_deepslate_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/crafting_table": {
|
||||
"criteria": {
|
||||
"unlock_right_away": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_slab": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/cobbled_deepslate_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_campfire_cooking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/cobbled_deepslate_wall": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/polished_deepslate_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/deepslate_brick_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_pickaxe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_brick_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tile_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/misc/charcoal": {
|
||||
"criteria": {
|
||||
"has_log": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_stairs": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/combat/stone_spear": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/deepslate_tile_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/oak_planks": {
|
||||
"criteria": {
|
||||
"has_logs": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_smoking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_bricks_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:adventure/adventuring_time": {
|
||||
"criteria": {
|
||||
"minecraft:forest": "2026-02-07 19:19:04 +0100",
|
||||
"minecraft:river": "2026-02-07 19:20:26 +0100",
|
||||
"minecraft:stony_shore": "2026-02-07 19:20:11 +0100",
|
||||
"minecraft:plains": "2026-02-07 19:20:12 +0100"
|
||||
},
|
||||
"done": false
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tiles_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_hoe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_shovel": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-02-07 19:19:03 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"DataVersion": 4671
|
||||
}
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"minecraft:recipes/decorations/crafting_table": {
|
||||
"criteria": {
|
||||
"unlock_right_away": "2025-11-30 09:32:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/cherry_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:adventure/kill_all_mobs": {
|
||||
"criteria": {
|
||||
"minecraft:zombie": "2025-11-30 10:06:33 +0100"
|
||||
},
|
||||
"done": false
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_campfire_cooking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2025-11-30 10:06:57 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/bamboo_raft": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/spruce_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/birch_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/oak_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/mangrove_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2025-11-30 10:06:57 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/acacia_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:adventure/root": {
|
||||
"criteria": {
|
||||
"killed_something": "2025-11-30 10:06:33 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_smoking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2025-11-30 10:06:57 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:adventure/kill_a_mob": {
|
||||
"criteria": {
|
||||
"minecraft:zombie": "2025-11-30 10:06:33 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/pale_oak_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:adventure/adventuring_time": {
|
||||
"criteria": {
|
||||
"minecraft:old_growth_birch_forest": "2025-11-30 10:03:58 +0100",
|
||||
"minecraft:forest": "2025-11-30 10:04:40 +0100",
|
||||
"minecraft:taiga": "2025-11-30 09:32:29 +0100",
|
||||
"minecraft:deep_dark": "2025-11-30 10:05:12 +0100",
|
||||
"minecraft:river": "2025-11-30 10:09:03 +0100",
|
||||
"minecraft:grove": "2025-11-30 09:32:32 +0100",
|
||||
"minecraft:meadow": "2025-11-30 09:32:44 +0100",
|
||||
"minecraft:birch_forest": "2025-11-30 10:05:33 +0100",
|
||||
"minecraft:frozen_peaks": "2025-11-30 09:32:55 +0100"
|
||||
},
|
||||
"done": false
|
||||
},
|
||||
"minecraft:recipes/transportation/jungle_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/transportation/dark_oak_boat": {
|
||||
"criteria": {
|
||||
"in_water": "2025-11-30 10:05:13 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"DataVersion": 4556
|
||||
}
|
||||
-220
@@ -1,220 +0,0 @@
|
||||
{
|
||||
"minecraft:recipes/building_blocks/polished_deepslate": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tile_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_brick_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/combat/stone_sword": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_axe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:story/mine_stone": {
|
||||
"criteria": {
|
||||
"get_stone": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/furnace": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/chiseled_deepslate_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/crafting_table": {
|
||||
"criteria": {
|
||||
"unlock_right_away": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_slab": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/cobbled_deepslate_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_campfire_cooking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/cobbled_deepslate_wall": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/polished_deepslate_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_stairs_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/deepslate_brick_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/polished_deepslate_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_pickaxe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_brick_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tile_slab_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/misc/charcoal": {
|
||||
"criteria": {
|
||||
"has_log": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/cobbled_deepslate_stairs": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/combat/stone_spear": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/decorations/deepslate_tile_wall_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/oak_planks": {
|
||||
"criteria": {
|
||||
"has_logs": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/food/cooked_porkchop_from_smoking": {
|
||||
"criteria": {
|
||||
"has_porkchop": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_bricks_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:adventure/adventuring_time": {
|
||||
"criteria": {
|
||||
"minecraft:stony_shore": "2026-03-12 21:17:48 +0100",
|
||||
"minecraft:plains": "2026-03-12 21:17:31 +0100"
|
||||
},
|
||||
"done": false
|
||||
},
|
||||
"minecraft:recipes/building_blocks/deepslate_tiles_from_cobbled_deepslate_stonecutting": {
|
||||
"criteria": {
|
||||
"has_cobbled_deepslate": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_hoe": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"minecraft:recipes/tools/stone_shovel": {
|
||||
"criteria": {
|
||||
"has_cobblestone": "2026-03-12 21:17:28 +0100"
|
||||
},
|
||||
"done": true
|
||||
},
|
||||
"DataVersion": 4671
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1 +0,0 @@
|
||||
https://github.com/cabaletta/baritone
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user