14 Commits
Author SHA1 Message Date
Admin Else acb5fe8d90 port messenger to more versions 2026-08-04 19:28:43 +02:00
Admin Else d9bca7e517 strom: fix generator for pointer-receiver Encode, use nbt.Tag for named NBT
- Delete raw_capture subcommand (use tcpflow + raw-to-tmcpr instead)
- Fix DefaultEncoder to use temp variable for non-addressable value encoding,
  eliminating manual post-generation patches
- Map nbt and optionalNbt types to nbt.Tag (named NBT) instead of nbt.Anon
  (anonymous NBT) — 1.8 uses named NBT with root tag names
2026-08-04 16:40:47 +02:00
Admin Else b162d0d401 strom: add 1.8 protocol support, raw-to-tmcpr, disable raw-capture
- Add minecraft-data for version 1.8 (protocol 47)
- Generate proto_generated/v1_8 from 1.8 protocol.json
- Fix generator compatibility: missing CompareTo natives (i16, i32, u16, u32,
  i64, string, nbt, optionalNbt), parameterized type placeholder handling ()
- Add raw-to-tmcpr subcommand: converts raw tcpflow captures to TMCPR
  with zlib decompression and state-aware compression threshold tracking
- Extend print-replay: 1.8 state transitions (success->Play, login_start->Play,
  handshake/status initial states, set_protocol NextState detection)
- Comment out broken raw-capture subcommand (use tcpflow instead)
- Fix nbt.Anon Encode/Decode on value receivers
2026-08-04 16:10:15 +02:00
Admin Else 3273244c42 format 2026-08-02 20:21:15 +02:00
adman f579645b88 client: add StatusRawWithVersion for version-aware status pings 2026-07-31 19:34:16 +02:00
adman 33754d7c71 server: guard against nil KnownPacks in ServeConfig 2026-07-31 15:49:05 +02:00
adman 8bd54a04f2 client: add ConnectWithConn, WithConn, WithDialer, LoginRawAddr
- ConnectWithConn wraps an existing net.Conn into a versioned proto.Conn
- DialFunc is a context-aware dial function type
- WithConn uses a pre-established net.Conn (version required, no DNS)
- WithDialer replaces net.Dialer with a custom dial function
- LoginRawAddr accepts an explicit handshake host address
- Login() wired to support all three connection modes
2026-07-31 14:30:25 +02:00
adman 492dae8e83 Add WithIgnoreConfig option to skip CONFIGURATION auto-completion 2026-07-31 12:10:32 +02:00
adman d9543de951 add check for bad servers 2026-07-30 02:40:43 +02:00
adman f1f5374a5c more context 2026-07-30 02:36:37 +02:00
adman 0d6b6e1ab9 docs: add godoc comments to all exported functions and methods 2026-07-30 00:20:55 +02:00
adman 56c97a2b3f docs: add API reference for all user-facing packages 2026-07-30 00:10:57 +02:00
adman 9063a0328d fix: make RegisterCritical variadic for API consistency with RegisterCriticalUntilLatest 2026-07-30 00:05:39 +02:00
adman 84b28d4c12 enforce MaxPacketSize constraint for raw and compressed packets 2026-07-28 16:37:45 +02:00
86 changed files with 61392 additions and 3153 deletions
+4 -2
View File
@@ -19,7 +19,8 @@ import (
"github.com/admin-else/strom/cmd/strom/packet_inspector" "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_nbt"
"github.com/admin-else/strom/cmd/strom/print_replay" "github.com/admin-else/strom/cmd/strom/print_replay"
"github.com/admin-else/strom/cmd/strom/raw_capture" // "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/registry_data_hunter"
"github.com/admin-else/strom/cmd/strom/serve_tmcpr" "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/serve_world"
@@ -61,7 +62,8 @@ var subcommands = map[string]func(args []string) error{
"extract-mca": extract_mca.Run, "extract-mca": extract_mca.Run,
"data": data.Run, "data": data.Run,
"print-replay": print_replay.Run, "print-replay": print_replay.Run,
"raw-capture": raw_capture.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, "registry-data-hunter": registry_data_hunter.Run,
"serve-world": serve_world.Run, "serve-world": serve_world.Run,
"mca-to-tmcpr": mca_to_tmcpr.Run, "mca-to-tmcpr": mca_to_tmcpr.Run,
+47 -17
View File
@@ -16,6 +16,7 @@ import (
"github.com/admin-else/strom/mc/event" "github.com/admin-else/strom/mc/event"
"github.com/admin-else/strom/mc/proto" "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_21_11"
"github.com/admin-else/strom/mc/proto_generated/v1_8"
) )
func stror(a, b string) string { func stror(a, b string) string {
@@ -29,12 +30,14 @@ var (
cmd = flag.NewFlagSet("status", flag.ContinueOnError) cmd = flag.NewFlagSet("status", flag.ContinueOnError)
connectToFlag = cmd.String("addr", "", "server address to connect to") 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") 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 { type Messenger struct {
*proto.Conn *proto.Conn
chat *chat.Module chat *chat.Module
world *world.Module world *world.Module
legacy bool
} }
func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) { func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) {
@@ -52,15 +55,27 @@ func (m *Messenger) OnChatSystem(e *v1_21_11.PlayToClientPacketSystemChat) (err
return 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) { func (m *Messenger) OnStdin(e event.Stdin) (err error) {
input := strings.TrimSpace(e.Val) input := strings.TrimSpace(e.Val)
if strings.HasPrefix(input, ">say ") { if strings.HasPrefix(input, ">say ") {
return m.chat.SendMessage(strings.TrimSpace(strings.TrimPrefix(input, ">say"))) return m.sendChat(strings.TrimSpace(strings.TrimPrefix(input, ">say")))
} }
if strings.HasPrefix(input, ">getblock ") { if !m.legacy && strings.HasPrefix(input, ">getblock ") {
return m.handleGetBlock(strings.TrimSpace(strings.TrimPrefix(input, ">getblock"))) return m.handleGetBlock(strings.TrimSpace(strings.TrimPrefix(input, ">getblock")))
} }
return m.chat.SendMessage(input) 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) { func (m *Messenger) handleGetBlock(args string) (err error) {
@@ -105,30 +120,45 @@ func Run(args []string) (err error) {
if err != nil { if err != nil {
return return
} }
c, err := client.Login(*connectToFlag, acc) c, err := client.Login(*connectToFlag, acc, client.WithVersion(*versionFlag))
if err != nil { if err != nil {
return return
} }
defer c.Close() defer c.Close()
chatMod, err := chat.Start(c, acc) isLegacy := c.ProtocolVersion < 764
if err != nil {
return var chatMod *chat.Module
if !isLegacy {
chatMod, err = chat.Start(c, acc)
if err != nil {
return
}
} }
// Shareable world storage. Other bots can subscribe to the same World.
w := world.NewWorld(c.Version, -64, 384)
m := &Messenger{ m := &Messenger{
Conn: c, Conn: c,
chat: chatMod, chat: chatMod,
world: world.Start(c, w), legacy: isLegacy,
}
if !isLegacy {
w := world.NewWorld(c.Version, -64, 384)
m.world = world.Start(c, w)
} }
event.StartListingStdin(m.Loop) event.StartListingStdin(m.Loop)
m.Register(m.OnStdin) m.Register(m.OnStdin)
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
keepalive.Start(m.Conn) if isLegacy {
m.Register(m.OnChatLegacy)
} else {
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
}
if !isLegacy {
keepalive.Start(m.Conn)
}
return m.StartConn() return m.StartConn()
} }
+36 -17
View File
@@ -25,7 +25,7 @@ var (
FileFlag = cmd.String("file", "", "The tmcpr file to print") FileFlag = cmd.String("file", "", "The tmcpr file to print")
ProtocolVersion = cmd.Int("protocol", -1, "The protocol version") ProtocolVersion = cmd.Int("protocol", -1, "The protocol version")
CreateTestsPath = cmd.String("create-tests", "", "") CreateTestsPath = cmd.String("create-tests", "", "")
StateFlag = cmd.String("state", "login", "Initial state: login, config, play") StateFlag = cmd.String("state", "login", "Initial state: handshake, login, config, status, play")
DirectionFlag = cmd.String("direction", "clientbound", "Packet direction: clientbound (ToClient) or serverbound (ToServer)") DirectionFlag = cmd.String("direction", "clientbound", "Packet direction: clientbound (ToClient) or serverbound (ToServer)")
) )
@@ -106,22 +106,6 @@ func UnpackPacket(r io.Reader) (packet proto_base.EncodeDecodeAble, i proto_base
packet = &proto.UnCodablePacket{Err: proto.BadPacketIdErr, Data: packetData, Info: i} packet = &proto.UnCodablePacket{Err: proto.BadPacketIdErr, Data: packetData, Info: i}
return return
} }
switch direction {
case proto_base.ToClient:
switch i.Name {
case "success":
state = proto_base.Configuration
case "finish_configuration":
state = proto_base.Play
}
case proto_base.ToServer:
switch i.Name {
case "login_acknowledged":
state = proto_base.Configuration
case "finish_configuration":
state = proto_base.Play
}
}
packet = reflect.New(reflect.TypeOf(i.Type).Elem()).Interface().(proto_base.EncodeDecodeAble) packet = reflect.New(reflect.TypeOf(i.Type).Elem()).Interface().(proto_base.EncodeDecodeAble)
err = packet.Decode(b) err = packet.Decode(b)
if err != nil { if err != nil {
@@ -132,9 +116,40 @@ func UnpackPacket(r io.Reader) (packet proto_base.EncodeDecodeAble, i proto_base
packet = &proto.UnCodablePacket{Err: proto.PacketNotFullyDecodedErr, Data: packetData[indexafterid:], Info: i} packet = &proto.UnCodablePacket{Err: proto.PacketNotFullyDecodedErr, Data: packetData[indexafterid:], Info: i}
return return
} }
trackState(packet, i)
return 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) { func Run(args []string) (err error) {
err = cmd.Parse(args) err = cmd.Parse(args)
if err != nil { if err != nil {
@@ -147,12 +162,16 @@ func Run(args []string) (err error) {
return fmt.Errorf("no file specified") return fmt.Errorf("no file specified")
} }
switch *StateFlag { switch *StateFlag {
case "handshake":
state = proto_base.Handshaking
case "login": case "login":
state = proto_base.Login state = proto_base.Login
case "config": case "config":
state = proto_base.Configuration state = proto_base.Configuration
case "play": case "play":
state = proto_base.Play state = proto_base.Play
case "status":
state = proto_base.Status
default: default:
return fmt.Errorf("unknown state: %s", *StateFlag) return fmt.Errorf("unknown state: %s", *StateFlag)
} }
-253
View File
@@ -1,253 +0,0 @@
package raw_capture
import (
"flag"
"fmt"
"log/slog"
"os"
"time"
"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/proto"
"github.com/admin-else/strom/mc/proto/replay"
"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("raw-capture", flag.ContinueOnError)
var OfflineNameFlag = Cmd.String("name", "raw_capture", "offline player name")
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 ClientboundFile = Cmd.String("clientbound-file", "clientbound.tmcpr", "file to write clientbound packets to")
var ServerboundFile = Cmd.String("serverbound-file", "serverbound.tmcpr", "file to write serverbound packets to")
var Version = Cmd.String("version", "", "client protocol version (e.g. 26.1); if empty, detect from server status")
var StatusResponse = server.StatusResponse{
Version: server.StatusResponseVersion{
Name: text.Pretty("STROM raw-capture"),
Protocol: 0,
},
Players: server.StatusResponsePlayers{},
Description: text.Pretty("STROM raw-capture proxy"),
Favicon: "",
EnforcesSecureChat: false,
}
type Proxy struct {
Client, Servee *proto.Conn
clientboundFile *os.File
serverboundFile *os.File
clientboundWriter *replay.Writer
serverboundWriter *replay.Writer
startTime time.Time
}
func (p *Proxy) Start() (err error) {
p.startTime = time.Now()
p.Client.RegisterCritical(p.OnAnything)
p.Servee.RegisterCritical(p.OnAnything)
p.Client.RegisterCriticalUntilLatest(p.OnCompress)
p.Servee.RegisterCriticalUntilLatest(p.OnHandshake)
p.Servee.RegisterCriticalUntilLatest(p.OnLoginAcknowledged)
p.Servee.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
errChan := make(chan error)
go func() {
errChan <- p.Client.StartConn()
}()
go func() {
errChan <- p.Servee.StartConn()
}()
err = <-errChan
return
}
func (p *Proxy) timestamp() uint32 {
return uint32(time.Since(p.startTime).Milliseconds())
}
func (p *Proxy) OnAnything(e event.Anything) (err error) {
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
if !ok {
return
}
packetInfo, ok := proto.LookupPacketInfoByType(packet)
if !ok {
return
}
if packetInfo.Name == "encryption_begin" {
return fmt.Errorf("encryption requested by remote server; raw capture cannot continue")
}
switch packetInfo.Direction {
case proto_base.ToServer:
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Client.Send(packet)
case proto_base.ToClient:
err = p.clientboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Servee.Send(packet)
}
return
}
func (p *Proxy) OnCompress(packet *v1_21_11.LoginToClientPacketCompress) (err error) {
err = p.clientboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Servee.Send(packet)
if err != nil {
return
}
p.Servee.SetCompressionThreshold(packet.Threshold)
p.Client.SetCompressionThreshold(packet.Threshold)
return event.DontForwardErr
}
func (p *Proxy) OnHandshake(packet *v1_21_11.HandshakingToServerPacketSetProtocol) (err error) {
state := proto_base.State(packet.NextState)
p.Servee.SetState(state)
return event.DontForwardErr
}
func (p *Proxy) OnLoginAcknowledged(packet *v1_21_11.LoginToServerPacketLoginAcknowledged) (err error) {
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Client.Send(packet)
if err != nil {
return
}
p.Servee.SetState(proto_base.Configuration)
p.Client.SetState(proto_base.Configuration)
return event.DontForwardErr
}
func (p *Proxy) OnFinishConfiguration(packet *v1_21_11.ConfigurationToServerPacketFinishConfiguration) (err error) {
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Client.Send(packet)
if err != nil {
return
}
p.Servee.SetState(proto_base.Play)
p.Client.SetState(proto_base.Play)
return event.DontForwardErr
}
func Run(args []string) (err error) {
err = Cmd.Parse(args)
if err != nil {
return
}
clientboundFile, err := os.Create(*ClientboundFile)
if err != nil {
return fmt.Errorf("create clientbound file: %w", err)
}
defer clientboundFile.Close()
serverboundFile, err := os.Create(*ServerboundFile)
if err != nil {
return fmt.Errorf("create serverbound file: %w", err)
}
defer serverboundFile.Close()
resolvedAddr, _, _, err := client.DoDNSChecked(nil, *TargetAddr, nil)
if err != nil {
return
}
var targetVersion string
var targetProtocol int32
if *Version == "" {
status, err := client.Status(nil, resolvedAddr)
if err != nil {
return fmt.Errorf("failed to detect server version: %w", err)
}
targetProtocol = status.Version.Protocol
versionInfo, err := data.LookUpVersionByProtocolVersion(targetProtocol)
if err != nil {
return fmt.Errorf("unknown protocol version %d: %w", targetProtocol, err)
}
targetVersion = versionInfo.MinecraftVersion
} else {
versionInfo, err := data.LookUpVersionByName(*Version)
if err != nil {
return fmt.Errorf("unknown version %s: %w", *Version, err)
}
targetVersion = versionInfo.MinecraftVersion
targetProtocol = versionInfo.Version
}
StatusResponse.Version.Name = text.Pretty("STROM raw-capture " + targetVersion)
StatusResponse.Version.Protocol = targetProtocol
StatusResponse.Description = text.Pretty(fmt.Sprintf("Raw capture proxy forwards to %s (%s)", resolvedAddr, targetVersion))
slog.Info("Starting raw capture", "listen", *ListenAddr, "target", resolvedAddr, "version", targetVersion, "protocol", targetProtocol, "clientbound", *ClientboundFile, "serverbound", *ServerboundFile)
err = server.StartServerWithOnConn(*ListenAddr, func(serveeConn *proto.Conn) (err error) {
proxy := &Proxy{
Servee: serveeConn,
clientboundFile: clientboundFile,
serverboundFile: serverboundFile,
clientboundWriter: replay.NewWriter(clientboundFile),
serverboundWriter: replay.NewWriter(serverboundFile),
}
acc := api.NewOfflineAccount(*OfflineNameFlag)
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
}
c, err := client.Connect(nil, resolvedAddr, targetVersion)
if err != nil {
return
}
defer c.Close()
err = client.LoginRaw(c, acc)
if err != nil {
return
}
proxy.Client = c
proxy.Client.AlwaysDecode = true
proxy.Servee.AlwaysDecode = true
return proxy.Start()
})
return
}
+243
View File
@@ -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
}
+5
View File
@@ -289,6 +289,9 @@ func (a *Account) CheckGiftCodeValidity(giftCode string) (valid bool, err error)
return return
} }
// NewAccountFromYGG creates an Account by authenticating the YGG token against
// Mojang's API. It makes an HTTP call to fetch the player profile and returns
// an Account with name and UUID populated, or an error if authentication fails.
func NewAccountFromYGG(ygg string) (a *Account, err error) { func NewAccountFromYGG(ygg string) (a *Account, err error) {
a = &Account{ a = &Account{
Ygg: ygg, Ygg: ygg,
@@ -303,6 +306,8 @@ func NewAccountFromYGG(ygg string) (a *Account, err error) {
return return
} }
// NewAccount creates an Account from explicit name, UUID, and YGG token without
// making any HTTP calls. The account uses Mojang's default API servers.
func NewAccount(name string, uuid uuid.UUID, ygg string) (a *Account) { func NewAccount(name string, uuid uuid.UUID, ygg string) (a *Account) {
return &Account{ return &Account{
Name: name, Name: name,
+2
View File
@@ -4,6 +4,8 @@ import (
"github.com/admin-else/strom/mc/crypto" "github.com/admin-else/strom/mc/crypto"
) )
// NewOfflineAccount creates an offline-mode Account with a UUID derived from the
// player name. No HTTP calls are made; the UUID is computed locally.
func NewOfflineAccount(name string) *Account { func NewOfflineAccount(name string) *Account {
return &Account{ return &Account{
Name: name, Name: name,
+13
View File
@@ -9,6 +9,8 @@ import (
"strings" "strings"
) )
// BlockedServers fetches the list of SHA-1 hashes of blocked server IPs from
// Mojang's session server. The returned strings are hex-encoded hashes.
func BlockedServers() (s []string, err error) { func BlockedServers() (s []string, err error) {
resp, err := http.Get("https://sessionserver.mojang.com/blockedservers") resp, err := http.Get("https://sessionserver.mojang.com/blockedservers")
if err != nil { if err != nil {
@@ -36,6 +38,9 @@ type ProfileResp struct {
Properties []ProfileProperty `json:"properties"` Properties []ProfileProperty `json:"properties"`
} }
// HasJoined checks with Mojang's session server whether a player has joined a
// given server. name is the player name, server is the server hash, and ip is
// the optional player IP address. Returns the player's profile on success.
func HasJoined(name, server, ip string) (p ProfileResp, err error) { func HasJoined(name, server, ip string) (p ProfileResp, err error) {
url := "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + name + "&serverId=" + server url := "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + name + "&serverId=" + server
if ip != "" { if ip != "" {
@@ -62,6 +67,8 @@ type MojangPublicKeysResp struct {
AuthenticationKeys []string `json:"authenticationKeys"` AuthenticationKeys []string `json:"authenticationKeys"`
} }
// MojangPublicKeys fetches Mojang's public keys for profile property signing and
// player certificate verification from the session server.
func MojangPublicKeys() (keys MojangPublicKeysResp, err error) { func MojangPublicKeys() (keys MojangPublicKeysResp, err error) {
resp, err := http.Get("https://sessionserver.mojang.com/session/minecraft/profile/public_key_by_token") resp, err := http.Get("https://sessionserver.mojang.com/session/minecraft/profile/public_key_by_token")
if err != nil { if err != nil {
@@ -72,6 +79,8 @@ func MojangPublicKeys() (keys MojangPublicKeysResp, err error) {
return return
} }
// Profile fetches a player's profile by UUID from Mojang's session server.
// If unsigned is false, signed profile properties are included in the response.
func Profile(id string, unsinged bool) (p ProfileResp, err error) { func Profile(id string, unsinged bool) (p ProfileResp, err error) {
url := "https://sessionserver.mojang.com/session/minecraft/profile/" + id url := "https://sessionserver.mojang.com/session/minecraft/profile/" + id
if !unsinged { if !unsinged {
@@ -91,6 +100,8 @@ type IdName struct {
Name string `json:"name"` Name string `json:"name"`
} }
// NameToId resolves a Minecraft player name to a UUID and canonical name by
// making an HTTP GET to Mojang's API.
func NameToId(name string) (idName IdName, err error) { func NameToId(name string) (idName IdName, err error) {
// may also use these: // may also use these:
//https://api.mojang.com/users/profiles/minecraft/<player name> //https://api.mojang.com/users/profiles/minecraft/<player name>
@@ -105,6 +116,8 @@ func NameToId(name string) (idName IdName, err error) {
return return
} }
// NameToIdBulk resolves multiple Minecraft player names to UUIDs and canonical
// names in a single HTTP POST to Mojang's API.
func NameToIdBulk(names []string) (idNames []IdName, err error) { func NameToIdBulk(names []string) (idNames []IdName, err error) {
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
err = json.NewEncoder(b).Encode(names) err = json.NewEncoder(b).Encode(names)
+1
View File
@@ -13,6 +13,7 @@ func (m *mod) onKeepAlive(p *v1_21_8.PlayToClientPacketKeepAlive) (err error) {
return m.Send(&v1_21_8.PlayToServerPacketKeepAlive{KeepAliveId: p.KeepAliveId}) return m.Send(&v1_21_8.PlayToServerPacketKeepAlive{KeepAliveId: p.KeepAliveId})
} }
// Start begins responding to keep-alive packets on the connection.
func Start(c *proto.Conn) { func Start(c *proto.Conn) {
m := &mod{Conn: c} m := &mod{Conn: c}
m.RegisterUntilLatest(m.onKeepAlive) m.RegisterUntilLatest(m.onKeepAlive)
+25
View File
@@ -2,6 +2,7 @@ package client
import ( import (
"context" "context"
"errors"
"net" "net"
"github.com/admin-else/strom/mc/data" "github.com/admin-else/strom/mc/data"
@@ -21,6 +22,10 @@ func Connect(ctx context.Context, addr string, version string) (ret *proto.Conn,
if err != nil { if err != nil {
return return
} }
if s.Version.Protocol == 0 {
err = errors.New("server didnt return protocol version")
return
}
protocolVersion = s.Version.Protocol protocolVersion = s.Version.Protocol
} else { } else {
var v data.VersionInfo var v data.VersionInfo
@@ -55,3 +60,23 @@ func ConnectVersionLess(ctx context.Context, addr string) (ret *proto.Conn, err
ret.W = ret.Conn ret.W = ret.Conn
return return
} }
// ConnectWithConn wraps an existing net.Conn into a proto.Conn with the given
// Minecraft version. The version must be non-empty.
func ConnectWithConn(ctx context.Context, conn net.Conn, version string) (ret *proto.Conn, err error) {
if ctx == nil {
ctx = context.Background()
}
ret = proto.NewConnCtx(ctx)
ret.Actor = proto_base.Client
ret.Conn = conn
ret.R = conn
ret.W = conn
v, err := data.LookUpVersionByName(version)
if err != nil {
return
}
ret.ProtocolVersion = v.Version
ret.Version = version
return
}
+1
View File
@@ -47,6 +47,7 @@ func (c *ConfigIgnorer) OnKeepAlive(packet *v1_21_8.ConfigurationToClientPacketK
return return
} }
// IgnoreConfig acknowledges configuration packets on the connection and transitions it to the Play state once configuration is complete.
func IgnoreConfig(c *proto.Conn) (err error) { func IgnoreConfig(c *proto.Conn) (err error) {
ci := &ConfigIgnorer{c} ci := &ConfigIgnorer{c}
ci.RegisterCritical(ci.Default) ci.RegisterCritical(ci.Default)
+1
View File
@@ -9,6 +9,7 @@ import (
"github.com/admin-else/strom/mc/proto_generated/v1_21_8" "github.com/admin-else/strom/mc/proto_generated/v1_21_8"
) )
// MakeHandshakePacket creates a handshake packet using the connection's remote address as the server host.
func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) { func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) {
return MakeHandshakePacketAddr(c, nextState, c.RemoteAddr().String()) return MakeHandshakePacketAddr(c, nextState, c.RemoteAddr().String())
} }
+132 -14
View File
@@ -7,6 +7,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"errors" "errors"
"net"
"github.com/admin-else/strom/mc/api" "github.com/admin-else/strom/mc/api"
"github.com/admin-else/strom/mc/crypto" "github.com/admin-else/strom/mc/crypto"
@@ -14,8 +15,10 @@ import (
"github.com/admin-else/strom/mc/proto" "github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base" "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/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/proto_generated/v1_8"
"github.com/admin-else/strom/mc/proto_generated/v26_2" "github.com/admin-else/strom/mc/proto_generated/v26_2"
"github.com/admin-else/strom/mc/text" "github.com/admin-else/strom/mc/text"
"github.com/google/uuid"
) )
type LoginClient struct { type LoginClient struct {
@@ -111,13 +114,42 @@ func (s *LoginClient) OnDisconnect(packet *v1_21_8.LoginToClientPacketDisconnect
return return
} }
func (s *LoginClient) OnSuccess(success *v1_21_8.LoginToClientPacketSuccess) (err error) { func (s *LoginClient) OnDisconnectV1_8(packet *v1_8.LoginToClientPacketDisconnect) (err error) {
s.GivenAccount = success var reason text.Component
err = s.Send(&v1_21_8.LoginToServerPacketLoginAcknowledged{}) err = json.Unmarshal([]byte(packet.Reason), &reason)
if err != nil { if err != nil {
return return
} }
s.SetState(proto_base.Configuration) err = KickedDuringLoginErr{reason}
return
}
func (s *LoginClient) OnCompressV1_8(packet *v1_8.LoginToClientPacketCompress) (err error) {
s.SetCompressionThreshold(packet.Threshold)
return
}
func (s *LoginClient) OnSuccessV1_8(success *v1_8.LoginToClientPacketSuccess) (err error) {
s.GivenAccount = &v1_21_8.LoginToClientPacketSuccess{
Uuid: uuid.MustParse(success.Uuid),
Username: success.Username,
}
s.SetState(proto_base.Play)
err = event.HandlerDoneErr{}
return
}
func (s *LoginClient) OnSuccess(success *v1_21_8.LoginToClientPacketSuccess) (err error) {
s.GivenAccount = success
if s.ProtocolVersion >= 764 {
err = s.Send(&v1_21_8.LoginToServerPacketLoginAcknowledged{})
if err != nil {
return
}
s.SetState(proto_base.Configuration)
} else {
s.SetState(proto_base.Play)
}
err = event.HandlerDoneErr{} err = event.HandlerDoneErr{}
return return
} }
@@ -134,7 +166,15 @@ func (s *LoginClient) OnStart() (err error) {
return return
} }
// LoginRaw performs the login handshake on an already-established connection without DNS resolution or configuration handling.
// The handshake address is derived from the connection's RemoteAddr.
func LoginRaw(c *proto.Conn, account *api.Account) (err error) { func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
return LoginRawAddr(c, account, "")
}
// LoginRawAddr performs the login handshake on an already-established connection using the given address for the handshake packet.
// If hostAddr is empty, the handshake address is derived from the connection's RemoteAddr.
func LoginRawAddr(c *proto.Conn, account *api.Account, hostAddr string) (err error) {
lc := &LoginClient{ lc := &LoginClient{
Conn: c, Conn: c,
Account: account, Account: account,
@@ -146,8 +186,16 @@ func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
lc.RegisterCriticalUntilLatest(lc.OnDisconnect) lc.RegisterCriticalUntilLatest(lc.OnDisconnect)
lc.RegisterCriticalUntil("26.1", lc.OnSuccess) lc.RegisterCriticalUntil("26.1", lc.OnSuccess)
lc.RegisterCriticalUntilLatest(lc.OnSuccess26_2) lc.RegisterCriticalUntilLatest(lc.OnSuccess26_2)
lc.RegisterCritical(lc.OnCompressV1_8)
lc.RegisterCritical(lc.OnDisconnectV1_8)
lc.RegisterCritical(lc.OnSuccessV1_8)
p, err := MakeHandshakePacket(lc.Conn, proto_base.Login) var p proto_base.EncodeDecodeAble
if hostAddr != "" {
p, err = MakeHandshakePacketAddr(lc.Conn, proto_base.Login, hostAddr)
} else {
p, err = MakeHandshakePacket(lc.Conn, proto_base.Login)
}
if err != nil { if err != nil {
return return
} }
@@ -157,7 +205,11 @@ func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
return return
} }
lc.SetState(proto_base.Login) lc.SetState(proto_base.Login)
err = lc.Send(&v1_21_8.LoginToServerPacketLoginStart{Username: lc.Account.Name, PlayerUUID: lc.Account.Uuid}) if lc.Conn.ProtocolVersion < 759 {
err = lc.Send(&v1_8.LoginToServerPacketLoginStart{Username: lc.Account.Name})
} else {
err = lc.Send(&v1_21_8.LoginToServerPacketLoginStart{Username: lc.Account.Name, PlayerUUID: lc.Account.Uuid})
}
err = lc.StartConn() err = lc.StartConn()
if err != nil { if err != nil {
@@ -166,31 +218,67 @@ func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
return return
} }
// WithoutDns returns a login setting that disables SRV DNS resolution during login.
func WithoutDns() func(ls *loginSettings) { func WithoutDns() func(ls *loginSettings) {
return func(ls *loginSettings) { return func(ls *loginSettings) {
ls.NoDns = true ls.NoDns = true
} }
} }
// WithContext returns a login setting that sets the context used for the connection dial and DNS resolution.
func WithContext(ctx context.Context) func(ls *loginSettings) { func WithContext(ctx context.Context) func(ls *loginSettings) {
return func(ls *loginSettings) { return func(ls *loginSettings) {
ls.Ctx = ctx ls.Ctx = ctx
} }
} }
// WithVersion returns a login setting that forces a specific Minecraft version string for the connection.
func WithVersion(version string) func(ls *loginSettings) { func WithVersion(version string) func(ls *loginSettings) {
return func(ls *loginSettings) { return func(ls *loginSettings) {
ls.Version = version ls.Version = version
} }
} }
func WithIgnoreConfig() func(ls *loginSettings) {
return func(ls *loginSettings) {
ls.IgnoreConfig = true
}
}
// DialFunc is a context-aware dial function used by Login to establish a TCP connection.
type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
// WithConn returns a login setting that uses an already-established net.Conn instead of dialling.
// WithVersion must also be set; auto-detection is not possible with a pre-established connection.
func WithConn(conn net.Conn) func(ls *loginSettings) {
return func(ls *loginSettings) {
ls.Conn = conn
}
}
// WithDialer returns a login setting that uses the given dial function instead of net.Dialer.
// Auto version detection via status ping is still supported when no explicit version is set.
func WithDialer(d DialFunc) func(ls *loginSettings) {
return func(ls *loginSettings) {
ls.Dialer = d
}
}
var (
ErrWithConnRequiresVersion = errors.New("WithVersion is required when using WithConn")
ErrWithConnNoStatus = errors.New("auto version detection is not supported with WithConn")
)
type loginSettings struct { type loginSettings struct {
NoDns bool NoDns bool
Ctx context.Context Ctx context.Context
Version string Version string
IgnoreConfig bool IgnoreConfig bool
Conn net.Conn
Dialer DialFunc
} }
// Login resolves the address with SRV DNS, connects, performs the login handshake, and handles the configuration phase, returning an authenticated connection ready for play.
func Login(connectTo string, account *api.Account, settings ...func(ls *loginSettings)) (c *proto.Conn, err error) { func Login(connectTo string, account *api.Account, settings ...func(ls *loginSettings)) (c *proto.Conn, err error) {
ls := &loginSettings{} ls := &loginSettings{}
@@ -198,22 +286,52 @@ func Login(connectTo string, account *api.Account, settings ...func(ls *loginSet
setting(ls) setting(ls)
} }
if !ls.NoDns { if ls.Conn != nil {
connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil) if ls.Version == "" {
return nil, ErrWithConnRequiresVersion
}
c, err = ConnectWithConn(ls.Ctx, ls.Conn, ls.Version)
if err != nil {
return
}
} else if ls.Dialer != nil {
if ls.Version == "" {
return nil, ErrWithConnRequiresVersion
}
if !ls.NoDns {
connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil)
if err != nil {
return
}
}
var conn net.Conn
conn, err = ls.Dialer(ls.Ctx, "tcp", connectTo)
if err != nil {
return
}
c, err = ConnectWithConn(ls.Ctx, conn, ls.Version)
if err != nil {
conn.Close()
return
}
} else {
if !ls.NoDns {
connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil)
if err != nil {
return
}
}
c, err = Connect(ls.Ctx, connectTo, ls.Version)
if err != nil { if err != nil {
return return
} }
} }
c, err = Connect(ls.Ctx, connectTo, ls.Version) err = LoginRawAddr(c, account, connectTo)
if err != nil { if err != nil {
return return
} }
err = LoginRaw(c, account) if !ls.IgnoreConfig && c.ProtocolVersion >= 764 {
if err != nil {
return
}
if !ls.IgnoreConfig {
err = IgnoreConfig(c) err = IgnoreConfig(c)
} }
return return
+30 -4
View File
@@ -9,6 +9,7 @@ import (
"github.com/admin-else/strom/mc/proto" "github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto_base" "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/proto_generated/v1_21_8"
"github.com/admin-else/strom/mc/proto_generated/v1_8"
"github.com/admin-else/strom/mc/server" "github.com/admin-else/strom/mc/server"
) )
@@ -36,16 +37,38 @@ func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
return return
} }
func (s *StatusClient) OnStatusV1_8(p *v1_8.StatusToClientPacketServerInfo) (err error) {
s.Status = p.Response
s.PingSendTime = time.Now()
if s.DoPingRoundTripTime {
err = s.Send(&v1_21_8.StatusToServerPacketPing{Time: s.PingSendTime.UnixMilli()})
} else {
err = event.HandlerDoneErr{}
}
return
}
func (s *StatusClient) OnPongV1_8(p *v1_8.StatusToClientPacketPing) (err error) {
s.PingReceiveTime = time.Now()
err = event.HandlerDoneErr{}
return
}
// StatusRaw returns a StatusClient that is not connected to a server. // StatusRaw returns a StatusClient that is not connected to a server.
// So ignore the resource leak warning. And maybe attach a warn ignore comment. // So ignore the resource leak warning. And maybe attach a warn ignore comment.
// It does not resolve SRV records. // It does not resolve SRV records.
func StatusRaw(ctx context.Context, addr string) (s *StatusClient, err error) { func StatusRaw(ctx context.Context, addr string) (s *StatusClient, err error) {
return StatusRawWithVersion(ctx, addr, "1.21.8")
}
// StatusRawWithVersion is like StatusRaw but uses the given Minecraft version for the handshake.
func StatusRawWithVersion(ctx context.Context, addr, version string) (s *StatusClient, err error) {
c, err := ConnectVersionLess(ctx, addr) c, err := ConnectVersionLess(ctx, addr)
if err != nil { if err != nil {
return return
} }
defer c.Close() defer c.Close()
err = c.SetVersion("1.21.8") err = c.SetVersion(version)
if err != nil { if err != nil {
return return
} }
@@ -54,6 +77,8 @@ func StatusRaw(ctx context.Context, addr string) (s *StatusClient, err error) {
} }
s.RegisterCriticalUntilLatest(s.OnStatus) s.RegisterCriticalUntilLatest(s.OnStatus)
s.RegisterCriticalUntilLatest(s.OnPong) s.RegisterCriticalUntilLatest(s.OnPong)
s.RegisterCritical(s.OnStatusV1_8)
s.RegisterCritical(s.OnPongV1_8)
p, err := MakeHandshakePacketAddr(s.Conn, proto_base.Status, addr) p, err := MakeHandshakePacketAddr(s.Conn, proto_base.Status, addr)
if err != nil { if err != nil {
@@ -71,11 +96,12 @@ func StatusRaw(ctx context.Context, addr string) (s *StatusClient, err error) {
} }
// StatusNoDns is like Status but does not resolve SRV records. // StatusNoDns is like Status but does not resolve SRV records.
func StatusNoDns(addr string) (status server.StatusResponse, err error) { func StatusNoDns(ctx context.Context, addr string) (status server.StatusResponse, err error) {
s, err := StatusRaw(context.Background(), addr) s, err := StatusRaw(ctx, addr)
if err != nil { if err != nil {
return return
} }
defer s.Close()
err = json.Unmarshal([]byte(s.Status), &status) err = json.Unmarshal([]byte(s.Status), &status)
return return
} }
@@ -86,5 +112,5 @@ func Status(ctx context.Context, addr string) (status server.StatusResponse, err
if err != nil { if err != nil {
return return
} }
return StatusNoDns(addr) return StatusNoDns(ctx, addr)
} }
+2
View File
@@ -41,10 +41,12 @@ type CFB8 struct {
mutex sync.Mutex mutex sync.Mutex
} }
// NewCFB8Decrypt creates a new CFB8 stream cipher in decrypt mode.
func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 { func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 {
return newCFB8(c, iv, true) return newCFB8(c, iv, true)
} }
// NewCFB8Encrypt creates a new CFB8 stream cipher in encrypt mode.
func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 { func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 {
return newCFB8(c, iv, false) return newCFB8(c, iv, false)
} }
+1
View File
@@ -21,6 +21,7 @@ func uuidV3(space []byte, data []byte) uuid.UUID {
return u return u
} }
// FromOfflinePlayer generates a UUID for an offline-mode (cracked) player from their display name.
func FromOfflinePlayer(displayName string) uuid.UUID { func FromOfflinePlayer(displayName string) uuid.UUID {
return uuidV3([]byte("OfflinePlayer:"), []byte(displayName)) return uuidV3([]byte("OfflinePlayer:"), []byte(displayName))
} }
+3
View File
@@ -28,6 +28,7 @@ type Biome struct {
var BiomesCache = make(map[string][]*Biome) var BiomesCache = make(map[string][]*Biome)
// BiomesForVersion returns the biome definitions for the given Minecraft version, caching them after the first load.
func BiomesForVersion(v string) (ret []*Biome) { func BiomesForVersion(v string) (ret []*Biome) {
var ok bool var ok bool
if ret, ok = BiomesCache[v]; ok { if ret, ok = BiomesCache[v]; ok {
@@ -39,6 +40,7 @@ func BiomesForVersion(v string) (ret []*Biome) {
return b return b
} }
// LookupBiomeById returns the biome with the given numeric ID in the given version.
func LookupBiomeById(version string, id int32) (biome *Biome, ok bool) { func LookupBiomeById(version string, id int32) (biome *Biome, ok bool) {
for _, b := range BiomesForVersion(version) { for _, b := range BiomesForVersion(version) {
if b.Id == id { if b.Id == id {
@@ -52,6 +54,7 @@ func LookupBiomeById(version string, id int32) (biome *Biome, ok bool) {
var UnknownBiomeNameErr = errors.New("unknown biome Name") var UnknownBiomeNameErr = errors.New("unknown biome Name")
// LookupBiomeByName returns the biome with the given name in the given version.
func LookupBiomeByName(version string, name string) (biome *Biome, ok bool) { func LookupBiomeByName(version string, name string) (biome *Biome, ok bool) {
for _, b := range BiomesForVersion(version) { for _, b := range BiomesForVersion(version) {
if b.Name == name { if b.Name == name {
+5
View File
@@ -59,6 +59,7 @@ type BlockState struct {
var BlocksCache = make(map[string][]*Block) var BlocksCache = make(map[string][]*Block)
var BlocksCacheIdMap = make(map[string]map[int32]*Block) var BlocksCacheIdMap = make(map[string]map[int32]*Block)
// BlocksForVersion returns the block definitions for the given Minecraft version, caching them after the first load.
func BlocksForVersion(v string) (ret []*Block) { func BlocksForVersion(v string) (ret []*Block) {
var ok bool var ok bool
if ret, ok = BlocksCache[v]; ok { if ret, ok = BlocksCache[v]; ok {
@@ -84,6 +85,7 @@ func BlocksForVersion(v string) (ret []*Block) {
return b return b
} }
// LookupBlockByStateId returns the block for the given global palette state ID in the given version.
func LookupBlockByStateId(version string, stateId int32) (block *Block, ok bool) { func LookupBlockByStateId(version string, stateId int32) (block *Block, ok bool) {
idmap, found := BlocksCacheIdMap[version] idmap, found := BlocksCacheIdMap[version]
if !found { if !found {
@@ -101,6 +103,7 @@ func LookupBlockByStateId(version string, stateId int32) (block *Block, ok bool)
var UnknownBlockNameErr = errors.New("unknown block Name") var UnknownBlockNameErr = errors.New("unknown block Name")
// LookupBlockByName returns the block with the given name in the given version.
func LookupBlockByName(version string, name string) (block *Block, ok bool) { func LookupBlockByName(version string, name string) (block *Block, ok bool) {
for _, b := range BlocksForVersion(version) { for _, b := range BlocksForVersion(version) {
if b.Name == name { if b.Name == name {
@@ -111,6 +114,7 @@ func LookupBlockByName(version string, name string) (block *Block, ok bool) {
return return
} }
// StateIdFromBlocKAndStateMap returns the global palette state ID for the given block name and state property map in the given version.
func StateIdFromBlocKAndStateMap(version string, name string, stateMap map[string]string) (stateId int32, err error) { func StateIdFromBlocKAndStateMap(version string, name string, stateMap map[string]string) (stateId int32, err error) {
b, ok := LookupBlockByName(version, name) b, ok := LookupBlockByName(version, name)
if !ok { if !ok {
@@ -135,6 +139,7 @@ var BadBlockStateTypeErr = errors.New("bad block state type")
var BadEnumErr = errors.New("bad enum") var BadEnumErr = errors.New("bad enum")
var SupplyAllStatesErr = errors.New("supply all states") var SupplyAllStatesErr = errors.New("supply all states")
// FromBlockState converts a global palette state ID back into a block definition and a map of state properties.
func FromBlockState(version string, stateId int32) (b *Block, stateData map[string]string, err error) { func FromBlockState(version string, stateId int32) (b *Block, stateData map[string]string, err error) {
b, ok := LookupBlockByStateId(version, stateId) b, ok := LookupBlockByStateId(version, stateId)
if !ok { if !ok {
+2
View File
@@ -12,6 +12,7 @@ import (
//go:embed minecraft-data/pc minecraft-data/dataPaths.json //go:embed minecraft-data/pc minecraft-data/dataPaths.json
var MinecraftData embed.FS var MinecraftData embed.FS
// LoadJson reads a JSON file from the embedded minecraft-data FS and unmarshals it into data.
func LoadJson(path string, data any) (err error) { func LoadJson(path string, data any) (err error) {
b, err := MinecraftData.ReadFile(path) b, err := MinecraftData.ReadFile(path)
if err != nil { if err != nil {
@@ -21,6 +22,7 @@ func LoadJson(path string, data any) (err error) {
return return
} }
// LoadVersionedJson loads a version-specific JSON data file from the embedded minecraft-data FS and unmarshals it into data.
func LoadVersionedJson(version, dataName string, data any) (err error) { func LoadVersionedJson(version, dataName string, data any) (err error) {
b, err := MinecraftData.ReadFile("minecraft-data/" + Paths.Data[version][dataName] + "/" + dataName + ".json") b, err := MinecraftData.ReadFile("minecraft-data/" + Paths.Data[version][dataName] + "/" + dataName + ".json")
if err != nil { if err != nil {
+3
View File
@@ -19,6 +19,7 @@ type Item struct {
var ItemsCache = make(map[string][]*Item) var ItemsCache = make(map[string][]*Item)
var ItemsCacheMutex = &sync.RWMutex{} var ItemsCacheMutex = &sync.RWMutex{}
// ItemsForVersion returns the item definitions for the given Minecraft version, caching them after the first load.
func ItemsForVersion(v string) (ret []*Item) { func ItemsForVersion(v string) (ret []*Item) {
var ok bool var ok bool
ItemsCacheMutex.RLock() ItemsCacheMutex.RLock()
@@ -35,6 +36,7 @@ func ItemsForVersion(v string) (ret []*Item) {
return b return b
} }
// LookupItemById returns the item with the given numeric ID in the given version.
func LookupItemById(version string, id int32) (item *Item, ok bool) { func LookupItemById(version string, id int32) (item *Item, ok bool) {
for _, i := range ItemsForVersion(version) { for _, i := range ItemsForVersion(version) {
if i.Id == id { if i.Id == id {
@@ -46,6 +48,7 @@ func LookupItemById(version string, id int32) (item *Item, ok bool) {
return return
} }
// LookupItemByName returns the item with the given name in the given version.
func LookupItemByName(version string, name string) (item *Item, ok bool) { func LookupItemByName(version string, name string) (item *Item, ok bool) {
for _, i := range ItemsForVersion(version) { for _, i := range ItemsForVersion(version) {
if i.Name == name { if i.Name == name {
@@ -0,0 +1,86 @@
[
{
"name": "maxHealth",
"resource": "generic.maxHealth",
"default": 20,
"min": 1,
"max": 1024
},
{
"name": "followRange",
"resource": "generic.followRange",
"default": 32,
"min": 0,
"max": 2048
},
{
"name": "knockbackResistance",
"resource": "generic.knockbackResistance",
"default": 0,
"min": 0,
"max": 1
},
{
"name": "movementSpeed",
"resource": "generic.movementSpeed",
"default": 0.7,
"min": 0,
"max": 1024
},
{
"name": "attackDamage",
"resource": "generic.attackDamage",
"default": 2,
"min": 0,
"max": 2048
},
{
"name": "horseJumpStrength",
"resource": "horse.jumpStrength",
"default": 0.7,
"min": 0,
"max": 2
},
{
"name": "zombieSpawnReinforcements",
"resource": "zombie.spawnReinforcements",
"default": 0,
"min": 0,
"max": 1
},
{
"name": "attackSpeed",
"resource": "generic.attackSpeed",
"default": 4,
"min": 0,
"max": 1024
},
{
"name": "flyingSpeed",
"resource": "generic.flyingSpeed",
"default": 0.4,
"min": 0,
"max": 1024
},
{
"name": "attackKnockback",
"resource": "generic.attackKnockback",
"default": 0,
"min": 0,
"max": 5
},
{
"name": "armorHealth",
"resource": "generic.armor",
"default": 0,
"min": 0,
"max": 30
},
{
"name": "armorToughness",
"resource": "generic.armorToughness",
"default": 0,
"min": 0,
"max": 20
}
]
+817
View File
@@ -0,0 +1,817 @@
[
{
"id": 0,
"name": "ocean",
"name_legacy": "ocean",
"category": "ocean",
"temperature": 0.5,
"precipitation": "rain",
"depth": -1,
"dimension": "overworld",
"displayName": "Ocean",
"color": 112,
"rainfall": 0.5
},
{
"id": 1,
"name": "plains",
"name_legacy": "plains",
"category": "plains",
"temperature": 0.8,
"precipitation": "rain",
"depth": 0.125,
"dimension": "overworld",
"displayName": "Plains",
"color": 9286496,
"rainfall": 0.4
},
{
"id": 2,
"name": "desert",
"name_legacy": "desert",
"category": "desert",
"temperature": 2,
"precipitation": "none",
"depth": 0.125,
"dimension": "overworld",
"displayName": "Desert",
"color": 16421912,
"rainfall": 0
},
{
"id": 3,
"name": "mountains",
"name_legacy": "mountains",
"category": "extreme_hills",
"temperature": 0.2,
"precipitation": "rain",
"depth": 1,
"dimension": "overworld",
"displayName": "Extreme Hills",
"color": 6316128,
"rainfall": 0.3
},
{
"id": 4,
"name": "forest",
"name_legacy": "forest",
"category": "forest",
"temperature": 0.7,
"precipitation": "rain",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Forest",
"color": 353825,
"rainfall": 0.8
},
{
"id": 5,
"name": "taiga",
"name_legacy": "taiga",
"category": "taiga",
"temperature": 0.25,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Taiga",
"color": 747097,
"rainfall": 0.8
},
{
"id": 6,
"name": "swamp",
"name_legacy": "swamp",
"category": "swamp",
"temperature": 0.8,
"precipitation": "rain",
"depth": -0.2,
"dimension": "overworld",
"displayName": "Swampland",
"color": 522674,
"rainfall": 0.9
},
{
"id": 7,
"name": "river",
"name_legacy": "river",
"category": "river",
"temperature": 0.5,
"precipitation": "rain",
"depth": -0.5,
"dimension": "overworld",
"displayName": "River",
"color": 255,
"rainfall": 0.5
},
{
"id": 8,
"name": "nether_wastes",
"name_legacy": "nether_wastes",
"category": "nether",
"temperature": 2,
"precipitation": "none",
"depth": 0.1,
"dimension": "nether",
"displayName": "Hell",
"color": 12532539,
"rainfall": 0,
"climates": [
{
"temperature": 0,
"humidity": 0,
"altitude": 0,
"weirdness": 0,
"offset": 0
}
]
},
{
"id": 9,
"name": "the_end",
"name_legacy": "the_end",
"category": "the_end",
"temperature": 0.5,
"precipitation": "none",
"depth": 0.1,
"dimension": "end",
"displayName": "The End",
"color": 8421631,
"rainfall": 0.5
},
{
"id": 10,
"name": "frozen_ocean",
"name_legacy": "frozen_ocean",
"category": "ocean",
"temperature": 0,
"precipitation": "snow",
"depth": -1,
"dimension": "overworld",
"displayName": "Frozen Ocean",
"color": 7368918,
"rainfall": 0.5
},
{
"id": 11,
"name": "frozen_river",
"name_legacy": "frozen_river",
"category": "river",
"temperature": 0,
"precipitation": "snow",
"depth": -0.5,
"dimension": "overworld",
"displayName": "Frozen River",
"color": 10526975,
"rainfall": 0.5
},
{
"id": 12,
"name": "snowy_tundra",
"name_legacy": "snowy_tundra",
"category": "icy",
"temperature": 0,
"precipitation": "snow",
"depth": 0.125,
"dimension": "overworld",
"displayName": "Ice Plains",
"color": 16777215,
"rainfall": 0.5
},
{
"id": 13,
"name": "snowy_mountains",
"name_legacy": "snowy_mountains",
"category": "icy",
"temperature": 0,
"precipitation": "snow",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Ice Mountains",
"color": 10526880,
"rainfall": 0.5
},
{
"id": 14,
"name": "mushroom_fields",
"name_legacy": "mushroom_fields",
"category": "mushroom",
"temperature": 0.9,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Mushroom Island",
"color": 16711935,
"rainfall": 1
},
{
"id": 15,
"name": "mushroom_field_shore",
"name_legacy": "mushroom_field_shore",
"category": "mushroom",
"temperature": 0.9,
"precipitation": "rain",
"depth": 0,
"dimension": "overworld",
"displayName": "Mushroom Island Shore",
"color": 10486015,
"rainfall": 1
},
{
"id": 16,
"name": "beach",
"name_legacy": "beach",
"category": "beach",
"temperature": 0.8,
"precipitation": "rain",
"depth": 0,
"dimension": "overworld",
"displayName": "Beach",
"color": 16440917,
"rainfall": 0.4
},
{
"id": 17,
"name": "desert_hills",
"name_legacy": "desert_hills",
"category": "desert",
"temperature": 2,
"precipitation": "none",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Desert Hills",
"color": 13786898,
"rainfall": 0
},
{
"id": 18,
"name": "wooded_hills",
"name_legacy": "wooded_hills",
"category": "forest",
"temperature": 0.7,
"precipitation": "rain",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Forest Hills",
"color": 2250012,
"rainfall": 0.8
},
{
"id": 19,
"name": "taiga_hills",
"name_legacy": "taiga_hills",
"category": "taiga",
"temperature": 0.25,
"precipitation": "rain",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Taiga Hills",
"color": 1456435,
"rainfall": 0.8
},
{
"id": 20,
"name": "mountain_edge",
"name_legacy": "mountain_edge",
"category": "extreme_hills",
"temperature": 0.2,
"precipitation": "rain",
"depth": 0.8,
"dimension": "overworld",
"displayName": "Extreme Hills Edge",
"color": 7501978,
"rainfall": 0.3
},
{
"id": 21,
"name": "jungle",
"name_legacy": "jungle",
"category": "jungle",
"temperature": 0.95,
"precipitation": "rain",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Jungle",
"color": 5470985,
"rainfall": 0.9
},
{
"id": 22,
"name": "jungle_hills",
"name_legacy": "jungle_hills",
"category": "jungle",
"temperature": 0.95,
"precipitation": "rain",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Jungle Hills",
"color": 2900485,
"rainfall": 0.9
},
{
"id": 23,
"name": "jungle_edge",
"name_legacy": "jungle_edge",
"category": "jungle",
"temperature": 0.95,
"precipitation": "rain",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Jungle Edge",
"color": 6458135,
"rainfall": 0.8
},
{
"id": 24,
"name": "deep_ocean",
"name_legacy": "deep_ocean",
"category": "ocean",
"temperature": 0.5,
"precipitation": "rain",
"depth": -1.8,
"dimension": "overworld",
"displayName": "Deep Ocean",
"color": 48,
"rainfall": 0.5
},
{
"id": 25,
"name": "stone_shore",
"name_legacy": "stone_shore",
"category": "none",
"temperature": 0.2,
"precipitation": "rain",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Stone Beach",
"color": 10658436,
"rainfall": 0.3
},
{
"id": 26,
"name": "snowy_beach",
"name_legacy": "snowy_beach",
"category": "beach",
"temperature": 0.05,
"precipitation": "snow",
"depth": 0,
"dimension": "overworld",
"displayName": "Cold Beach",
"color": 16445632,
"rainfall": 0.3
},
{
"id": 27,
"name": "birch_forest",
"name_legacy": "birch_forest",
"category": "forest",
"temperature": 0.6,
"precipitation": "rain",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Birch Forest",
"color": 3175492,
"rainfall": 0.6
},
{
"id": 28,
"name": "birch_forest_hills",
"name_legacy": "birch_forest_hills",
"category": "forest",
"temperature": 0.6,
"precipitation": "rain",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Birch Forest Hills",
"color": 2055986,
"rainfall": 0.6
},
{
"id": 29,
"name": "dark_forest",
"name_legacy": "dark_forest",
"category": "forest",
"temperature": 0.7,
"precipitation": "rain",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Roofed Forest",
"color": 4215066,
"rainfall": 0.8
},
{
"id": 30,
"name": "snowy_taiga",
"name_legacy": "snowy_taiga",
"category": "taiga",
"temperature": -0.5,
"precipitation": "snow",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Cold Taiga",
"color": 3233098,
"rainfall": 0.4
},
{
"id": 31,
"name": "snowy_taiga_hills",
"name_legacy": "snowy_taiga_hills",
"category": "taiga",
"temperature": -0.5,
"precipitation": "snow",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Cold Taiga Hills",
"color": 2375478,
"rainfall": 0.4
},
{
"id": 32,
"name": "giant_tree_taiga",
"name_legacy": "giant_tree_taiga",
"category": "taiga",
"temperature": 0.3,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Mega Taiga",
"color": 5858897,
"rainfall": 0.8
},
{
"id": 33,
"name": "giant_tree_taiga_hills",
"name_legacy": "giant_tree_taiga_hills",
"category": "taiga",
"temperature": 0.3,
"precipitation": "rain",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Mega Taiga Hills",
"color": 4542270,
"rainfall": 0.8
},
{
"id": 34,
"name": "wooded_mountains",
"name_legacy": "wooded_mountains",
"category": "extreme_hills",
"temperature": 0.2,
"precipitation": "rain",
"depth": 1,
"dimension": "overworld",
"displayName": "Extreme Hills+",
"color": 5271632,
"rainfall": 0.3
},
{
"id": 35,
"name": "savanna",
"name_legacy": "savanna",
"category": "savanna",
"temperature": 1.2,
"precipitation": "none",
"depth": 0.125,
"dimension": "overworld",
"displayName": "Savanna",
"color": 12431967,
"rainfall": 0
},
{
"id": 36,
"name": "savanna_plateau",
"name_legacy": "savanna_plateau",
"category": "savanna",
"temperature": 1,
"precipitation": "none",
"depth": 1.5,
"dimension": "overworld",
"displayName": "Savanna Plateau",
"color": 10984804,
"rainfall": 0
},
{
"id": 37,
"name": "badlands",
"name_legacy": "badlands",
"category": "mesa",
"temperature": 2,
"precipitation": "none",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Mesa",
"color": 14238997,
"rainfall": 0
},
{
"id": 38,
"name": "wooded_badlands_plateau",
"name_legacy": "wooded_badlands_plateau",
"category": "mesa",
"temperature": 2,
"precipitation": "none",
"depth": 1.5,
"dimension": "overworld",
"displayName": "Mesa Plateau F",
"color": 11573093,
"rainfall": 0
},
{
"id": 39,
"name": "badlands_plateau",
"name_legacy": "badlands_plateau",
"category": "mesa",
"temperature": 2,
"precipitation": "none",
"depth": 1.5,
"dimension": "overworld",
"displayName": "Mesa Plateau",
"color": 13274213,
"rainfall": 0
},
{
"id": 129,
"name": "sunflower_plains",
"name_legacy": "mutated_plains",
"category": "plains",
"temperature": 0.8,
"precipitation": "rain",
"depth": 0.125,
"dimension": "overworld",
"displayName": "Plains M",
"color": 11918216,
"rainfall": 0.4
},
{
"id": 130,
"name": "desert_lakes",
"name_legacy": "mutated_desert",
"category": "desert",
"temperature": 2,
"precipitation": "none",
"depth": 0.125,
"dimension": "overworld",
"displayName": "Desert M",
"color": 16759872,
"rainfall": 0
},
{
"id": 131,
"name": "gravelly_mountains",
"name_legacy": "mutated_extreme_hills",
"category": "extreme_hills",
"temperature": 0.2,
"precipitation": "rain",
"depth": 1,
"dimension": "overworld",
"displayName": "Extreme Hills M",
"color": 8947848,
"rainfall": 0.3
},
{
"id": 132,
"name": "flower_forest",
"name_legacy": "mutated_forest",
"category": "forest",
"temperature": 0.7,
"precipitation": "rain",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Forest M",
"color": 2985545,
"rainfall": 0.8
},
{
"id": 133,
"name": "taiga_mountains",
"name_legacy": "mutated_taiga",
"category": "taiga",
"temperature": 0.25,
"precipitation": "rain",
"depth": 0.3,
"dimension": "overworld",
"displayName": "Taiga M",
"color": 3378817,
"rainfall": 0.8
},
{
"id": 134,
"name": "swamp_hills",
"name_legacy": "mutated_swampland",
"category": "swamp",
"temperature": 0.8,
"precipitation": "rain",
"depth": -0.1,
"dimension": "overworld",
"displayName": "Swampland M",
"color": 3145690,
"rainfall": 0.9
},
{
"id": 140,
"name": "ice_spikes",
"name_legacy": "mutated_ice_flats",
"category": "icy",
"temperature": 0,
"precipitation": "snow",
"depth": 0.425,
"dimension": "overworld",
"displayName": "Ice Plains M",
"color": 11853020,
"rainfall": 0.5
},
{
"id": 149,
"name": "modified_jungle",
"name_legacy": "mutated_jungle",
"category": "jungle",
"temperature": 0.95,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Jungle M",
"color": 8102705,
"rainfall": 0.9
},
{
"id": 151,
"name": "modified_jungle_edge",
"name_legacy": "mutated_jungle_edge",
"category": "jungle",
"temperature": 0.95,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "JungleEdge M",
"color": 9089855,
"rainfall": 0.8
},
{
"id": 155,
"name": "tall_birch_forest",
"name_legacy": "mutated_birch_forest",
"category": "forest",
"temperature": 0.6,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Birch Forest M",
"color": 5807212,
"rainfall": 0.6
},
{
"id": 156,
"name": "tall_birch_hills",
"name_legacy": "mutated_birch_forest_hills",
"category": "forest",
"temperature": 0.6,
"precipitation": "rain",
"depth": 0.55,
"dimension": "overworld",
"displayName": "Birch Forest Hills M",
"color": 4687706,
"rainfall": 0.6
},
{
"id": 157,
"name": "dark_forest_hills",
"name_legacy": "mutated_roofed_forest",
"category": "forest",
"temperature": 0.7,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Roofed Forest M",
"color": 6846786,
"rainfall": 0.8
},
{
"id": 158,
"name": "snowy_taiga_mountains",
"name_legacy": "mutated_taiga_cold",
"category": "taiga",
"temperature": -0.5,
"precipitation": "snow",
"depth": 0.3,
"dimension": "overworld",
"displayName": "Cold Taiga M",
"color": 5864818,
"rainfall": 0.4
},
{
"id": 160,
"name": "giant_spruce_taiga",
"name_legacy": "mutated_redwood_taiga",
"category": "taiga",
"temperature": 0.25,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Mega Taiga M",
"color": 8490617,
"rainfall": 0.8
},
{
"id": 161,
"name": "giant_spruce_taiga_hills",
"name_legacy": "mutated_redwood_taiga_hills",
"category": "taiga",
"temperature": 0.25,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Mega Taiga Hills M",
"color": 7173990,
"rainfall": 0.8
},
{
"id": 161,
"name": "giant_spruce_taiga_hills",
"name_legacy": "mutated_redwood_taiga_hills",
"category": "taiga",
"temperature": 0.25,
"precipitation": "rain",
"depth": 0.2,
"dimension": "overworld",
"displayName": "Redwood Taiga Hills M",
"color": 7173990,
"rainfall": 0.8
},
{
"id": 162,
"name": "modified_gravelly_mountains",
"name_legacy": "mutated_extreme_hills_with_trees",
"category": "extreme_hills",
"temperature": 0.2,
"precipitation": "rain",
"depth": 1,
"dimension": "overworld",
"displayName": "Extreme Hills+ M",
"color": 7903352,
"rainfall": 0.3
},
{
"id": 163,
"name": "shattered_savanna",
"name_legacy": "mutated_savanna",
"category": "savanna",
"temperature": 1.1,
"precipitation": "none",
"depth": 0.3625,
"dimension": "overworld",
"displayName": "Savanna M",
"color": 15063687,
"rainfall": 0
},
{
"id": 164,
"name": "shattered_savanna_plateau",
"name_legacy": "mutated_savanna_rock",
"category": "savanna",
"temperature": 1,
"precipitation": "none",
"depth": 1.05,
"dimension": "overworld",
"displayName": "Savanna Plateau M",
"color": 13616524,
"rainfall": 0
},
{
"id": 165,
"name": "eroded_badlands",
"name_legacy": "mutated_mesa",
"category": "mesa",
"temperature": 2,
"precipitation": "none",
"depth": 0.1,
"dimension": "overworld",
"displayName": "Mesa M",
"color": 16739645,
"rainfall": 0
},
{
"id": 166,
"name": "modified_wooded_badlands_plateau",
"name_legacy": "mutated_mesa_rock",
"category": "mesa",
"temperature": 2,
"precipitation": "none",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Mesa Plateau F M",
"color": 14204813,
"rainfall": 0
},
{
"id": 167,
"name": "modified_badlands_plateau",
"name_legacy": "mutated_mesa_clear_rock",
"category": "mesa",
"temperature": 2,
"precipitation": "none",
"depth": 0.45,
"dimension": "overworld",
"displayName": "Mesa Plateau M",
"color": 15905933,
"rainfall": 0
}
]
@@ -0,0 +1,274 @@
{"blocks":{"air":0,
"stone":1,
"grass":1,
"dirt":1,
"cobblestone":1,
"planks":1,
"sapling":0,
"bedrock":1,
"flowing_water":0,
"water":0,
"flowing_lava":0,
"lava":0,
"sand":1,
"gravel":1,
"gold_ore":1,
"iron_ore":1,
"coal_ore":1,
"log":1,
"leaves":1,
"sponge":1,
"glass":1,
"lapis_ore":1,
"lapis_block":1,
"dispenser":1,
"sandstone":1,
"noteblock":1,
"bed":2,
"golden_rail":0,
"detector_rail":0,
"sticky_piston":[1,1,1,1,1,1,0,0,3,4,5,6,7,8,0,0],
"web":0,
"tallgrass":0,
"deadbush":0,
"piston":[1,1,1,1,1,1,0,0,3,4,5,6,7,8,0,0],
"piston_head":[10,11,9,12,13,14,0,0,10,11,9,12,13,14,0,0],
"wool":1,
"piston_extension":0,
"yellow_flower":0,
"red_flower":0,
"brown_mushroom":0,
"red_mushroom":0,
"gold_block":1,
"iron_block":1,
"double_stone_slab":1,
"stone_slab":[15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16],
"brick_block":1,
"tnt":1,
"bookshelf":1,
"mossy_cobblestone":1,
"obsidian":1,
"torch":0,
"fire":0,
"mob_spawner":1,
"oak_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"chest":25,
"redstone_wire":0,
"diamond_ore":1,
"diamond_block":1,
"crafting_table":1,
"wheat":0,
"farmland":26,
"furnace":1,
"lit_furnace":1,
"standing_sign":0,
"wooden_door":[28,29,30,27,29,30,27,28,28,29,30,27,29,30,27,28],
"ladder":[27,27,27,29,30,28,27,27,27,29,30,28,27,27,27,29],
"rail":0,
"stone_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"wall_sign":0,
"lever":0,
"stone_pressure_plate":0,
"iron_door":[28,29,30,27,29,30,27,28,28,29,30,27,29,30,27,28],
"wooden_pressure_plate":0,
"redstone_ore":1,
"lit_redstone_ore":1,
"unlit_redstone_torch":0,
"redstone_torch":0,
"stone_button":0,
"snow_layer":[0,31,32,33,15,34,4,35,0,31,32,33,15,34,4,35],
"ice":1,
"snow":1,
"cactus":36,
"clay":1,
"reeds":0,
"jukebox":1,
"fence":37,
"pumpkin":1,
"netherrack":1,
"soul_sand":35,
"glowstone":1,
"portal":0,
"lit_pumpkin":1,
"cake":[38,39,40,41,42,43,44,0,0,0,0,0,0,0,0,0],
"unpowered_repeater":31,
"powered_repeater":31,
"stained_glass":1,
"trapdoor":[45,45,45,45,27,29,30,28,46,46,46,46,27,29,30,28],
"monster_egg":1,
"stonebrick":1,
"brown_mushroom_block":1,
"red_mushroom_block":1,
"iron_bars":47,
"glass_pane":47,
"melon_block":1,
"pumpkin_stem":0,
"melon_stem":0,
"vine":0,
"fence_gate":[48,49,48,49,0,0,0,0,48,49,48,49,0,0,0,0],
"brick_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"stone_brick_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"mycelium":1,
"waterlily":50,
"nether_brick":1,
"nether_brick_fence":37,
"nether_brick_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"nether_wart":0,
"enchanting_table":4,
"brewing_stand":51,
"cauldron":52,
"end_portal":0,
"end_portal_frame":[81,81,81,81,82,82,82,82,81,81,81,81,82,82,82,82],
"end_stone":1,
"dragon_egg":53,
"redstone_lamp":1,
"lit_redstone_lamp":1,
"double_wooden_slab":1,
"wooden_slab":[15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16],
"cocoa":[55,56,54,57,58,59,60,61,62,63,64,65,0,0,0,0],
"sandstone_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"emerald_ore":1,
"ender_chest":25,
"tripwire_hook":0,
"tripwire":0,
"emerald_block":1,
"spruce_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"birch_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"jungle_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"command_block":1,
"beacon":1,
"cobblestone_wall":66,
"flower_pot":67,
"carrots":0,
"potatoes":0,
"wooden_button":0,
"skull":[69,69,68,70,71,72,69,69,69,69,68,70,71,72,69,69],
"anvil":[73,74,73,74,73,74,73,74,73,74,73,74,0,0,0,0],
"trapped_chest":25,
"light_weighted_pressure_plate":0,
"heavy_weighted_pressure_plate":0,
"unpowered_comparator":31,
"powered_comparator":31,
"daylight_detector":33,
"redstone_block":1,
"quartz_ore":1,
"hopper":75,
"quartz_block":1,
"quartz_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"activator_rail":0,
"dropper":1,
"stained_hardened_clay":1,
"stained_glass_pane":47,
"leaves2":1,"log2":1,"acacia_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"dark_oak_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"slime":1,
"barrier":1,
"iron_trapdoor":[45,45,45,45,27,29,30,28,46,46,46,46,27,29,30,28],
"prismarine":1,
"sea_lantern":1,
"hay_block":1,
"carpet":76,
"hardened_clay":1,
"coal_block":1,
"packed_ice":1,
"double_plant":0,
"standing_banner":0,
"wall_banner":0,
"daylight_detector_inverted":33,
"red_sandstone":1,
"red_sandstone_stairs":[18,19,20,17,21,22,23,24,18,19,20,17,21,22,23,24],
"double_stone_slab2":1,"stone_slab2":[15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16],
"spruce_fence_gate":[48,49,48,49,0,0,0,0,48,49,48,49,0,0,0,0],
"birch_fence_gate":[48,49,48,49,0,0,0,0,48,49,48,49,0,0,0,0],
"jungle_fence_gate":[48,49,48,49,0,0,0,0,48,49,48,49,0,0,0,0],
"dark_oak_fence_gate":[48,49,48,49,0,0,0,0,48,49,48,49,0,0,0,0],
"acacia_fence_gate":[48,49,48,49,0,0,0,0,48,49,48,49,0,0,0,0],
"spruce_fence":37,
"birch_fence":37,
"jungle_fence":37,
"dark_oak_fence":37,
"acacia_fence":37,
"spruce_door":[28,29,30,27,29,30,27,28,28,29,30,27,29,30,27,28],
"birch_door":[28,29,30,27,29,30,27,28,28,29,30,27,29,30,27,28],
"jungle_door":[28,29,30,27,29,30,27,28,28,29,30,27,29,30,27,28],
"acacia_door":[28,29,30,27,29,30,27,28,28,29,30,27,29,30,27,28],
"dark_oak_door":[28,29,30,27,29,30,27,28,28,29,30,27,29,30,27,28]},
"shapes":{"0":[],
"1":[[0.0,0.0,0.0,1.0,1.0,1.0]],
"2":[[0.0,0.0,0.0,1.0,0.5625,1.0]],
"3":[[0.0,0.25,0.0,1.0,1.0,1.0]],
"4":[[0.0,0.0,0.0,1.0,0.75,1.0]],
"5":[[0.0,0.0,0.25,1.0,1.0,1.0]],
"6":[[0.0,0.0,0.0,1.0,1.0,0.75]],
"7":[[0.25,0.0,0.0,1.0,1.0,1.0]],
"8":[[0.0,0.0,0.0,0.75,1.0,1.0]],
"9":[[0.0,0.0,0.0,1.0,1.0,0.25],[0.375,0.375,0.25,0.625,0.625,1.25]],
"10":[[0.0,0.0,0.0,1.0,0.25,1.0],[0.375,0.25,0.375,0.625,1.25,0.625]],
"11":[[0.0,0.75,0.0,1.0,1.0,1.0],[0.375,-0.25,0.375,0.625,0.75,0.625]],
"12":[[0.0,0.0,0.75,1.0,1.0,1.0],[0.375,0.375,-0.25,0.625,0.625,0.75]],
"13":[[0.0,0.0,0.0,0.25,1.0,1.0],[0.25,0.375,0.375,1.25,0.625,0.625]],
"14":[[0.75,0.0,0.0,1.0,1.0,1.0],[-0.25,0.375,0.375,0.75,0.625,0.625]],
"15":[[0.0,0.0,0.0,1.0,0.5,1.0]],
"16":[[0.0,0.5,0.0,1.0,1.0,1.0]],
"17":[[0.0,0.0,0.0,1.0,0.5,1.0],[0.0,0.5,0.0,1.0,1.0,0.5]],
"18":[[0.0,0.0,0.0,1.0,0.5,1.0],[0.5,0.5,0.0,1.0,1.0,1.0]],
"19":[[0.0,0.0,0.0,1.0,0.5,1.0],[0.0,0.5,0.0,0.5,1.0,1.0]],
"20":[[0.0,0.0,0.0,1.0,0.5,1.0],[0.0,0.5,0.5,1.0,1.0,1.0]],
"21":[[0.0,0.5,0.0,1.0,1.0,1.0],[0.5,0.0,0.0,1.0,0.5,1.0]],
"22":[[0.0,0.5,0.0,1.0,1.0,1.0],[0.0,0.0,0.0,0.5,0.5,1.0]],
"23":[[0.0,0.5,0.0,1.0,1.0,1.0],[0.0,0.0,0.5,1.0,0.5,1.0]],
"24":[[0.0,0.5,0.0,1.0,1.0,1.0],[0.0,0.0,0.0,1.0,0.5,0.5]],
"25":[[0.0625,0.0,0.0625,0.9375,0.875,0.9375]],
"26":[[0.0,0.0,0.0,1.0,0.9375,1.0]],
"27":[[0.0,0.0,0.8125,1.0,1.0,1.0]],
"28":[[0.0,0.0,0.0,0.1875,1.0,1.0]],
"29":[[0.0,0.0,0.0,1.0,1.0,0.1875]],
"30":[[0.8125,0.0,0.0,1.0,1.0,1.0]],
"31":[[0.0,0.0,0.0,1.0,0.125,1.0]],
"32":[[0.0,0.0,0.0,1.0,0.25,1.0]],
"33":[[0.0,0.0,0.0,1.0,0.375,1.0]],
"34":[[0.0,0.0,0.0,1.0,0.625,1.0]],
"35":[[0.0,0.0,0.0,1.0,0.875,1.0]],
"36":[[0.0625,0.0,0.0625,0.9375,0.9375,0.9375]],
"37":[[0.375,0.0,0.375,0.625,1.5,0.625]],
"38":[[0.0625,0.0,0.0625,0.9375,0.5,0.9375]],
"39":[[0.1875,0.0,0.0625,0.9375,0.5,0.9375]],
"40":[[0.3125,0.0,0.0625,0.9375,0.5,0.9375]],
"41":[[0.4375,0.0,0.0625,0.9375,0.5,0.9375]],
"42":[[0.5625,0.0,0.0625,0.9375,0.5,0.9375]],
"43":[[0.6875,0.0,0.0625,0.9375,0.5,0.9375]],
"44":[[0.8125,0.0,0.0625,0.9375,0.5,0.9375]],
"45":[[0.0,0.0,0.0,1.0,0.1875,1.0]],
"46":[[0.0,0.8125,0.0,1.0,1.0,1.0]],
"47":[[0.4375,0.0,0.4375,0.5625,1.0,0.5625]],
"48":[[0.0,0.0,0.375,1.0,1.5,0.625]],
"49":[[0.375,0.0,0.0,0.625,1.5,1.0]],
"50":[[0.0625,0.0,0.0625,0.9375,0.09375,0.9375]],
"51":[[0.4375,0.0,0.4375,0.5625,0.875,0.5625],[0.0,0.0,0.0,1.0,0.125,1.0]],
"52":[[0.0,0.0,0.0,1.0,0.3125,1.0],[0.0,0.0,0.0,0.125,1.0,1.0],[0.0,0.0,0.0,1.0,1.0,0.125],[0.875,0.0,0.0,1.0,1.0,1.0],[0.0,0.0,0.875,1.0,1.0,1.0]],
"53":[[0.0625,0.0,0.0625,0.9375,1.0,0.9375]],
"54":[[0.375,0.4375,0.0625,0.625,0.75,0.3125]],
"55":[[0.375,0.4375,0.6875,0.625,0.75,0.9375]],
"56":[[0.0625,0.4375,0.375,0.3125,0.75,0.625]],
"57":[[0.6875,0.4375,0.375,0.9375,0.75,0.625]],
"58":[[0.3125,0.3125,0.5625,0.6875,0.75,0.9375]],
"59":[[0.0625,0.3125,0.3125,0.4375,0.75,0.6875]],
"60":[[0.3125,0.3125,0.0625,0.6875,0.75,0.4375]],
"61":[[0.5625,0.3125,0.3125,0.9375,0.75,0.6875]],
"62":[[0.25,0.1875,0.4375,0.75,0.75,0.9375]],
"63":[[0.0625,0.1875,0.25,0.5625,0.75,0.75]],
"64":[[0.25,0.1875,0.0625,0.75,0.75,0.5625]],
"65":[[0.4375,0.1875,0.25,0.9375,0.75,0.75]],
"66":[[0.25,0.0,0.25,0.75,1.5,0.75]],
"67":[[0.3125,0.0,0.3125,0.6875,0.375,0.6875]],
"68":[[0.25,0.25,0.5,0.75,0.75,1.0]],
"69":[[0.25,0.0,0.25,0.75,0.5,0.75]],
"70":[[0.25,0.25,0.0,0.75,0.75,0.5]],
"71":[[0.5,0.25,0.25,1.0,0.75,0.75]],
"72":[[0.0,0.25,0.25,0.5,0.75,0.75]],
"73":[[0.125,0.0,0.0,0.875,1.0,1.0]],
"74":[[0.0,0.0,0.125,1.0,1.0,0.875]],
"75":[[0.0,0.0,0.0,1.0,0.625,1.0],[0.0,0.0,0.0,0.125,1.0,1.0],[0.875,0.0,0.0,1.0,1.0,1.0],[0.0,0.0,0.0,1.0,1.0,0.125],[0.0,0.0,0.875,1.0,1.0,1.0]],
"76":[[0.0,0.0,0.0,1.0,0.0625,1.0]],
"81":[[0.0,0.0,0.0,1.0,0.8125,1.0]],
"82":[[0.0,0.0,0.0,1.0,0.8125,1.0],[0.3125,0.8125,0.3125,0.6875,1.0,0.6875]]}}
File diff suppressed because it is too large Load Diff
+140
View File
@@ -0,0 +1,140 @@
[
{
"id": 1,
"name": "Speed",
"displayName": "Speed",
"type": "good"
},
{
"id": 2,
"name": "Slowness",
"displayName": "Slowness",
"type": "bad"
},
{
"id": 3,
"name": "Haste",
"displayName": "Haste",
"type": "good"
},
{
"id": 4,
"name": "MiningFatigue",
"displayName": "Mining Fatigue",
"type": "bad"
},
{
"id": 5,
"name": "Strength",
"displayName": "Strength",
"type": "good"
},
{
"id": 6,
"name": "InstantHealth",
"displayName": "Instant Health",
"type": "good"
},
{
"id": 7,
"name": "InstantDamage",
"displayName": "Instant Damage",
"type": "bad"
},
{
"id": 8,
"name": "JumpBoost",
"displayName": "Jump Boost",
"type": "good"
},
{
"id": 9,
"name": "Nausea",
"displayName": "Nausea",
"type": "bad"
},
{
"id": 10,
"name": "Regeneration",
"displayName": "Regeneration",
"type": "good"
},
{
"id": 11,
"name": "Resistance",
"displayName": "Resistance",
"type": "good"
},
{
"id": 12,
"name": "FireResistance",
"displayName": "Fire Resistance",
"type": "good"
},
{
"id": 13,
"name": "WaterBreathing",
"displayName": "Water Breathing",
"type": "good"
},
{
"id": 14,
"name": "Invisibility",
"displayName": "Invisibility",
"type": "good"
},
{
"id": 15,
"name": "Blindness",
"displayName": "Blindness",
"type": "bad"
},
{
"id": 16,
"name": "NightVision",
"displayName": "Night Vision",
"type": "good"
},
{
"id": 17,
"name": "Hunger",
"displayName": "Hunger",
"type": "bad"
},
{
"id": 18,
"name": "Weakness",
"displayName": "Weakness",
"type": "bad"
},
{
"id": 19,
"name": "Poison",
"displayName": "Poison",
"type": "bad"
},
{
"id": 20,
"name": "Wither",
"displayName": "Wither",
"type": "bad"
},
{
"id": 21,
"name": "HealthBoost",
"displayName": "Health Boost",
"type": "good"
},
{
"id": 22,
"name": "Absorption",
"displayName": "Absorption",
"type": "good"
},
{
"id": 23,
"name": "Saturation",
"displayName": "Saturation",
"type": "good"
}
]
@@ -0,0 +1,560 @@
[
{
"id": 0,
"name": "protection",
"displayName": "Protection",
"maxLevel": 4,
"minCost": {
"a": 11,
"b": -10
},
"maxCost": {
"a": 11,
"b": 1
},
"exclude": [
"blast_protection",
"fire_protection",
"projectile_protection"
],
"category": "armor",
"weight": 10,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 1,
"name": "fire_protection",
"displayName": "Fire Protection",
"maxLevel": 4,
"minCost": {
"a": 8,
"b": 2
},
"maxCost": {
"a": 8,
"b": 10
},
"exclude": [
"blast_protection",
"protection",
"projectile_protection"
],
"category": "armor",
"weight": 5,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 2,
"name": "feather_falling",
"displayName": "Feather Falling",
"maxLevel": 4,
"minCost": {
"a": 6,
"b": -1
},
"maxCost": {
"a": 6,
"b": 5
},
"exclude": [],
"category": "armor_feet",
"weight": 5,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 3,
"name": "blast_protection",
"displayName": "Blast Protection",
"maxLevel": 4,
"minCost": {
"a": 8,
"b": -3
},
"maxCost": {
"a": 8,
"b": 5
},
"exclude": [
"fire_protection",
"protection",
"projectile_protection"
],
"category": "armor",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 4,
"name": "projectile_protection",
"displayName": "Projectile Protection",
"maxLevel": 4,
"minCost": {
"a": 6,
"b": -3
},
"maxCost": {
"a": 6,
"b": 3
},
"exclude": [
"protection",
"blast_protection",
"fire_protection"
],
"category": "armor",
"weight": 5,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 5,
"name": "respiration",
"displayName": "Respiration",
"maxLevel": 3,
"minCost": {
"a": 10,
"b": 0
},
"maxCost": {
"a": 10,
"b": 30
},
"exclude": [],
"category": "armor_head",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 6,
"name": "aqua_affinity",
"displayName": "Aqua Affinity",
"maxLevel": 1,
"minCost": {
"a": 0,
"b": 1
},
"maxCost": {
"a": 0,
"b": 41
},
"exclude": [],
"category": "armor_head",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 7,
"name": "thorns",
"displayName": "Thorns",
"maxLevel": 3,
"minCost": {
"a": 20,
"b": -10
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [],
"category": "armor_chest",
"weight": 1,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 8,
"name": "depth_strider",
"displayName": "Depth Strider",
"maxLevel": 3,
"minCost": {
"a": 10,
"b": 0
},
"maxCost": {
"a": 10,
"b": 15
},
"exclude": [
"frost_walker"
],
"category": "armor_feet",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 16,
"name": "sharpness",
"displayName": "Sharpness",
"maxLevel": 5,
"minCost": {
"a": 11,
"b": -10
},
"maxCost": {
"a": 11,
"b": 10
},
"exclude": [
"smite",
"bane_of_arthropods"
],
"category": "weapon",
"weight": 10,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 17,
"name": "smite",
"displayName": "Smite",
"maxLevel": 5,
"minCost": {
"a": 8,
"b": -3
},
"maxCost": {
"a": 8,
"b": 17
},
"exclude": [
"sharpness",
"bane_of_arthropods"
],
"category": "weapon",
"weight": 5,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 18,
"name": "bane_of_arthropods",
"displayName": "Bane of Arthropods",
"maxLevel": 5,
"minCost": {
"a": 8,
"b": -3
},
"maxCost": {
"a": 8,
"b": 17
},
"exclude": [
"smite",
"sharpness"
],
"category": "weapon",
"weight": 5,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 19,
"name": "knockback",
"displayName": "Knockback",
"maxLevel": 2,
"minCost": {
"a": 20,
"b": -15
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [],
"category": "weapon",
"weight": 5,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 20,
"name": "fire_aspect",
"displayName": "Fire Aspect",
"maxLevel": 2,
"minCost": {
"a": 20,
"b": -10
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [],
"category": "weapon",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 21,
"name": "looting",
"displayName": "Looting",
"maxLevel": 3,
"minCost": {
"a": 9,
"b": 6
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [],
"category": "weapon",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 32,
"name": "efficiency",
"displayName": "Efficiency",
"maxLevel": 5,
"minCost": {
"a": 10,
"b": -9
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [],
"category": "digger",
"weight": 10,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 33,
"name": "silk_touch",
"displayName": "Silk Touch",
"maxLevel": 1,
"minCost": {
"a": 0,
"b": 15
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [
"fortune"
],
"category": "digger",
"weight": 1,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 34,
"name": "unbreaking",
"displayName": "Unbreaking",
"maxLevel": 3,
"minCost": {
"a": 8,
"b": -3
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [],
"category": "breakable",
"weight": 5,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 35,
"name": "fortune",
"displayName": "Fortune",
"maxLevel": 3,
"minCost": {
"a": 9,
"b": 6
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [
"silk_touch"
],
"category": "digger",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 48,
"name": "power",
"displayName": "Power",
"maxLevel": 5,
"minCost": {
"a": 10,
"b": -9
},
"maxCost": {
"a": 10,
"b": 6
},
"exclude": [],
"category": "bow",
"weight": 10,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 49,
"name": "punch",
"displayName": "Punch",
"maxLevel": 2,
"minCost": {
"a": 20,
"b": -8
},
"maxCost": {
"a": 20,
"b": 17
},
"exclude": [],
"category": "bow",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 50,
"name": "flame",
"displayName": "Flame",
"maxLevel": 1,
"minCost": {
"a": 0,
"b": 20
},
"maxCost": {
"a": 0,
"b": 50
},
"exclude": [],
"category": "bow",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 51,
"name": "infinity",
"displayName": "Infinity",
"maxLevel": 1,
"minCost": {
"a": 0,
"b": 20
},
"maxCost": {
"a": 0,
"b": 50
},
"exclude": [
"mending"
],
"category": "bow",
"weight": 1,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 61,
"name": "luck_of_the_sea",
"displayName": "Luck of the Sea",
"maxLevel": 3,
"minCost": {
"a": 9,
"b": 6
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [],
"category": "fishing_rod",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
},
{
"id": 62,
"name": "lure",
"displayName": "Lure",
"maxLevel": 3,
"minCost": {
"a": 9,
"b": 6
},
"maxCost": {
"a": 10,
"b": 51
},
"exclude": [],
"category": "fishing_rod",
"weight": 2,
"treasureOnly": false,
"curse": false,
"tradeable": true,
"discoverable": true
}
]
+580
View File
@@ -0,0 +1,580 @@
[
{
"id": 48,
"internalId": 48,
"name": "Mob",
"displayName": "Mob",
"type": "mob",
"width": null,
"height": null,
"category": "Generic"
},
{
"id": 49,
"internalId": 49,
"name": "Monster",
"displayName": "Monster",
"type": "mob",
"width": null,
"height": null,
"category": "Generic"
},
{
"id": 50,
"internalId": 50,
"name": "Creeper",
"displayName": "Creeper",
"type": "mob",
"width": 0.6,
"height": 1.8,
"category": "Hostile mobs"
},
{
"id": 51,
"internalId": 51,
"name": "Skeleton",
"displayName": "Skeleton",
"type": "mob",
"width": 0.6,
"height": 1.95,
"category": "Hostile mobs"
},
{
"id": 52,
"internalId": 52,
"name": "Spider",
"displayName": "Spider",
"type": "mob",
"width": 1.4,
"height": 0.9,
"category": "Hostile mobs"
},
{
"id": 53,
"internalId": 53,
"name": "Giant",
"displayName": "Giant",
"type": "mob",
"width": 3.5999999999999996,
"height": 10.8,
"category": "Hostile mobs"
},
{
"id": 54,
"internalId": 54,
"name": "Zombie",
"displayName": "Zombie",
"type": "mob",
"width": 0.6,
"height": 1.8,
"category": "Hostile mobs"
},
{
"id": 55,
"internalId": 55,
"name": "Slime",
"displayName": "Slime",
"type": "mob",
"width": 0.51000005,
"height": 0.51000005,
"category": "Hostile mobs"
},
{
"id": 56,
"internalId": 56,
"name": "Ghast",
"displayName": "Ghast",
"type": "mob",
"width": 4,
"height": 4,
"category": "Hostile mobs"
},
{
"id": 57,
"internalId": 57,
"name": "PigZombie",
"displayName": "Zombie Pigman",
"type": "mob",
"width": 0.6,
"height": 1.8,
"category": "Hostile mobs"
},
{
"id": 58,
"internalId": 58,
"name": "Enderman",
"displayName": "Enderman",
"type": "mob",
"width": 0.6,
"height": 2.9,
"category": "Hostile mobs"
},
{
"id": 59,
"internalId": 59,
"name": "CaveSpider",
"displayName": "Cave Spider",
"type": "mob",
"width": 0.7,
"height": 0.5,
"category": "Hostile mobs"
},
{
"id": 60,
"internalId": 60,
"name": "Silverfish",
"displayName": "Silverfish",
"type": "mob",
"width": 0.4,
"height": 0.3,
"category": "Hostile mobs"
},
{
"id": 61,
"internalId": 61,
"name": "Blaze",
"displayName": "Blaze",
"type": "mob",
"width": 0.6,
"height": 1.8,
"category": "Hostile mobs"
},
{
"id": 62,
"internalId": 62,
"name": "LavaSlime",
"displayName": "Magma Cube",
"type": "mob",
"width": 0.51000005,
"height": 0.51000005,
"category": "Hostile mobs"
},
{
"id": 63,
"internalId": 63,
"name": "EnderDragon",
"displayName": "Ender Dragon",
"type": "mob",
"width": 16,
"height": 8,
"category": "Hostile mobs"
},
{
"id": 64,
"internalId": 64,
"name": "WitherBoss",
"displayName": "Wither",
"type": "mob",
"width": 0.9,
"height": 3.5,
"category": "Hostile mobs"
},
{
"id": 65,
"internalId": 65,
"name": "Bat",
"displayName": "Bat",
"type": "mob",
"width": 0.5,
"height": 0.9,
"category": "Passive mobs"
},
{
"id": 66,
"internalId": 66,
"name": "Witch",
"displayName": "Witch",
"type": "mob",
"width": 0.6,
"height": 1.8,
"category": "Hostile mobs"
},
{
"id": 67,
"internalId": 67,
"name": "Endermite",
"displayName": "Endermite",
"type": "mob",
"width": 0.4,
"height": 0.3,
"category": "Hostile mobs"
},
{
"id": 68,
"internalId": 68,
"name": "Guardian",
"displayName": "Guardian",
"type": "mob",
"width": 0.85,
"height": 0.85,
"category": "Hostile mobs"
},
{
"id": 90,
"internalId": 90,
"name": "Pig",
"displayName": "Pig",
"type": "mob",
"width": 0.9,
"height": 0.9,
"category": "Passive mobs"
},
{
"id": 91,
"internalId": 91,
"name": "Sheep",
"displayName": "Sheep",
"type": "mob",
"width": 0.9,
"height": 1.3,
"category": "Passive mobs"
},
{
"id": 92,
"internalId": 92,
"name": "Cow",
"displayName": "Cow",
"type": "mob",
"width": 0.9,
"height": 1.3,
"category": "Passive mobs"
},
{
"id": 93,
"internalId": 93,
"name": "Chicken",
"displayName": "Chicken",
"type": "mob",
"width": 0.4,
"height": 0.7,
"category": "Passive mobs"
},
{
"id": 94,
"internalId": 94,
"name": "Squid",
"displayName": "Squid",
"type": "mob",
"width": 0.95,
"height": 0.95,
"category": "Passive mobs"
},
{
"id": 95,
"internalId": 95,
"name": "Wolf",
"displayName": "Wolf",
"type": "mob",
"width": 0.6,
"height": 0.8,
"category": "Passive mobs"
},
{
"id": 96,
"internalId": 96,
"name": "MushroomCow",
"displayName": "Mooshroom",
"type": "mob",
"width": 0.9,
"height": 1.3,
"category": "Passive mobs"
},
{
"id": 97,
"internalId": 97,
"name": "SnowMan",
"displayName": "Snow Golem",
"type": "mob",
"width": 0.7,
"height": 1.9,
"category": "Passive mobs"
},
{
"id": 98,
"internalId": 98,
"name": "Ozelot",
"displayName": "Ocelot",
"type": "mob",
"width": 0.6,
"height": 0.8,
"category": "Passive mobs"
},
{
"id": 99,
"internalId": 99,
"name": "VillagerGolem",
"displayName": "Iron Golem",
"type": "mob",
"width": 1.4,
"height": 2.9,
"category": "Passive mobs"
},
{
"id": 100,
"internalId": 100,
"name": "EntityHorse",
"displayName": "Horse",
"type": "mob",
"width": 1.4,
"height": 1.6,
"category": "Passive mobs"
},
{
"id": 101,
"internalId": 101,
"name": "Rabbit",
"displayName": "Rabbit",
"type": "mob",
"width": 0.6,
"height": 0.7,
"category": "Passive mobs"
},
{
"id": 120,
"internalId": 120,
"name": "Villager",
"displayName": "Villager",
"type": "mob",
"width": 0.6,
"height": 1.8,
"category": "NPCs"
},
{
"id": 1,
"internalId": 41,
"name": "Boat",
"displayName": "Boat",
"type": "object",
"width": 1.5,
"height": 0.6,
"category": "Vehicles"
},
{
"id": 2,
"internalId": 1,
"name": "Item",
"displayName": "Dropped item",
"type": "object",
"width": 0.25,
"height": 0.25,
"category": "Drops"
},
{
"id": 10,
"internalId": 42,
"name": "MinecartRideable",
"displayName": "Minecart",
"type": "object",
"width": 0.98,
"height": 0.7,
"category": "Vehicles"
},
{
"id": 11,
"internalId": 42,
"name": "MinecartRideable",
"displayName": "Minecart",
"type": "object",
"width": 0.98,
"height": 0.7,
"category": "Vehicles"
},
{
"id": 12,
"internalId": 42,
"name": "MinecartRideable",
"displayName": "Minecart",
"type": "object",
"width": 0.98,
"height": 0.7,
"category": "Vehicles"
},
{
"id": 50,
"internalId": 20,
"name": "PrimedTnt",
"displayName": "Primed TNT",
"type": "object",
"width": 0.98,
"height": 0.98,
"category": "Blocks"
},
{
"id": 51,
"internalId": 200,
"name": "EnderCrystal",
"displayName": "Ender Crystal",
"type": "object",
"width": 2,
"height": 2,
"category": "Immobile"
},
{
"id": 60,
"internalId": 10,
"name": "Arrow",
"displayName": "Shot arrow",
"type": "object",
"width": 0.5,
"height": 0.5,
"category": "Projectiles"
},
{
"id": 61,
"internalId": 11,
"name": "Snowball",
"displayName": "Thrown snowball",
"type": "object",
"width": 0.25,
"height": 0.25,
"category": "Projectiles"
},
{
"id": 62,
"internalId": 7,
"name": "ThrownEgg",
"displayName": "Thrown egg",
"type": "object",
"width": 0.25,
"height": 0.25,
"category": "Projectiles"
},
{
"id": 63,
"internalId": 12,
"name": "Fireball",
"displayName": "Ghast fireball",
"type": "object",
"width": 1,
"height": 1,
"category": "Projectiles"
},
{
"id": 64,
"internalId": 13,
"name": "SmallFireball",
"displayName": "Blaze fireball",
"type": "object",
"width": 0.3125,
"height": 0.3125,
"category": "Projectiles"
},
{
"id": 65,
"internalId": 14,
"name": "ThrownEnderpearl",
"displayName": "Thrown Ender Pearl",
"type": "object",
"width": 0.25,
"height": 0.25,
"category": "Projectiles"
},
{
"id": 66,
"internalId": 19,
"name": "WitherSkull",
"displayName": "Wither Skull",
"type": "object",
"width": 0.3125,
"height": 0.3125,
"category": "Projectiles"
},
{
"id": 70,
"internalId": 21,
"name": "FallingSand",
"displayName": "Falling block",
"type": "object",
"width": 0.98,
"height": 0.98,
"category": "Blocks"
},
{
"id": 71,
"internalId": 18,
"name": "ItemFrame",
"displayName": "Item Frame",
"type": "object",
"width": null,
"height": null,
"category": "Immobile"
},
{
"id": 72,
"internalId": 15,
"name": "EyeOfEnderSignal",
"displayName": "Thrown Eye of Ender",
"type": "object",
"width": 0.25,
"height": 0.25,
"category": "Projectiles"
},
{
"id": 73,
"internalId": 16,
"name": "ThrownPotion",
"displayName": "Thrown splash potion",
"type": "object",
"width": 0.25,
"height": 0.25,
"category": "Projectiles"
},
{
"id": 74,
"internalId": 21,
"name": "FallingSand",
"displayName": "Falling block",
"type": "object",
"width": 0.98,
"height": 0.98,
"category": "Blocks"
},
{
"id": 75,
"internalId": 17,
"name": "ThrownExpBottle",
"displayName": "Thrown Bottle o' Enchanting",
"type": "object",
"width": 0.25,
"height": 0.25,
"category": "Projectiles"
},
{
"id": 76,
"internalId": 22,
"name": "FireworksRocketEntity",
"displayName": "Firework Rocket",
"type": "object",
"width": 0.25,
"height": 0.25,
"category": "Projectiles"
},
{
"id": 77,
"internalId": 8,
"name": "LeashKnot",
"displayName": "Lead knot",
"type": "object",
"width": 0.5,
"height": 0.5,
"category": "Immobile"
},
{
"id": 78,
"internalId": 30,
"name": "ArmorStand",
"displayName": "Armor Stand",
"type": "object",
"width": 0.5,
"height": 2,
"category": "Immobile"
},
{
"id": 90,
"name": "Fishing Float",
"displayName": "Fishing Float",
"type": "object",
"width": 0.25,
"height": 0.25
}
]
+310
View File
@@ -0,0 +1,310 @@
[
{
"id": 103,
"displayName": "Melon",
"name": "melon_block",
"stackSize": 64,
"foodPoints": 2,
"saturation": 1.2,
"effectiveQuality": 3.2,
"saturationRatio": 0.6
},
{
"id": 141,
"displayName": "Carrot",
"name": "carrots",
"stackSize": 64,
"foodPoints": 3,
"saturation": 4.8,
"effectiveQuality": 7.8,
"saturationRatio": 1.5999999999999999
},
{
"id": 142,
"displayName": "Potato",
"name": "potatoes",
"stackSize": 64,
"foodPoints": 1,
"saturation": 0.6,
"effectiveQuality": 1.6,
"saturationRatio": 0.6
},
{
"id": 260,
"displayName": "Apple",
"stackSize": 64,
"name": "apple",
"foodPoints": 4,
"saturation": 2.4,
"effectiveQuality": 6.4,
"saturationRatio": 0.6
},
{
"id": 282,
"displayName": "Mushroom Stew",
"stackSize": 1,
"name": "mushroom_stew",
"foodPoints": 6,
"saturation": 7.2,
"effectiveQuality": 13.2,
"saturationRatio": 1.2
},
{
"id": 297,
"displayName": "Bread",
"stackSize": 64,
"name": "bread",
"foodPoints": 5,
"saturation": 6,
"effectiveQuality": 11,
"saturationRatio": 1.2
},
{
"id": 319,
"displayName": "Raw Porkchop",
"stackSize": 64,
"name": "porkchop",
"foodPoints": 3,
"saturation": 1.8,
"effectiveQuality": 4.8,
"saturationRatio": 0.6
},
{
"id": 320,
"displayName": "Cooked Porkchop",
"stackSize": 64,
"name": "cooked_porkchop",
"foodPoints": 8,
"saturation": 12.8,
"effectiveQuality": 20.8,
"saturationRatio": 1.6
},
{
"id": 322,
"displayName": "Golden Apple",
"stackSize": 64,
"name": "golden_apple",
"variations": [
{
"metadata": 0,
"displayName": "Golden Apple"
},
{
"metadata": 1,
"displayName": "Enchanted Golden Apple"
}
],
"foodPoints": 4,
"saturation": 9.6,
"effectiveQuality": 13.6,
"saturationRatio": 2.4
},
{
"id": 349,
"displayName": "Raw Fish",
"stackSize": 64,
"name": "fish",
"variations": [
{
"metadata": 0,
"displayName": "Raw Fish"
},
{
"metadata": 1,
"displayName": "Raw Salmon"
},
{
"metadata": 2,
"displayName": "Clownfish"
},
{
"metadata": 3,
"displayName": "Pufferfish"
}
],
"foodPoints": 2,
"saturation": 0.4,
"effectiveQuality": 2.4,
"saturationRatio": 0.2
},
{
"id": 350,
"displayName": "Cooked Fish",
"stackSize": 64,
"name": "cooked_fish",
"foodPoints": 5,
"saturation": 6,
"effectiveQuality": 11,
"saturationRatio": 1.2
},
{
"id": 354,
"displayName": "Cake",
"stackSize": 1,
"name": "cake",
"foodPoints": 2,
"saturation": 0.4,
"effectiveQuality": 2.4,
"saturationRatio": 0.2
},
{
"id": 357,
"displayName": "Cookie",
"stackSize": 64,
"name": "cookie",
"foodPoints": 2,
"saturation": 0.4,
"effectiveQuality": 2.4,
"saturationRatio": 0.2
},
{
"id": 363,
"displayName": "Raw Beef",
"stackSize": 64,
"name": "beef",
"foodPoints": 3,
"saturation": 1.8,
"effectiveQuality": 4.8,
"saturationRatio": 0.6
},
{
"id": 364,
"displayName": "Steak",
"stackSize": 64,
"name": "cooked_beef",
"foodPoints": 8,
"saturation": 12.8,
"effectiveQuality": 20.8,
"saturationRatio": 1.6
},
{
"id": 365,
"displayName": "Raw Chicken",
"stackSize": 64,
"name": "chicken",
"foodPoints": 2,
"saturation": 1.2,
"effectiveQuality": 3.2,
"saturationRatio": 0.6
},
{
"id": 366,
"displayName": "Cooked Chicken",
"stackSize": 64,
"name": "cooked_chicken",
"foodPoints": 6,
"saturation": 7.2,
"effectiveQuality": 13.2,
"saturationRatio": 1.2
},
{
"id": 367,
"displayName": "Rotten Flesh",
"stackSize": 64,
"name": "rotten_flesh",
"foodPoints": 4,
"saturation": 0.8,
"effectiveQuality": 4.8,
"saturationRatio": 0.2
},
{
"id": 375,
"displayName": "Spider Eye",
"stackSize": 64,
"name": "spider_eye",
"foodPoints": 2,
"saturation": 3.2,
"effectiveQuality": 5.2,
"saturationRatio": 1.6
},
{
"id": 393,
"displayName": "Baked Potato",
"stackSize": 64,
"name": "baked_potato",
"foodPoints": 5,
"saturation": 7.2,
"effectiveQuality": 12.2,
"saturationRatio": 1.44
},
{
"id": 394,
"displayName": "Poisonous Potato",
"stackSize": 64,
"name": "poisonous_potato",
"foodPoints": 2,
"saturation": 1.2,
"effectiveQuality": 3.2,
"saturationRatio": 0.6
},
{
"id": 396,
"displayName": "Golden Carrot",
"stackSize": 64,
"name": "golden_carrot",
"foodPoints": 6,
"saturation": 14.4,
"effectiveQuality": 20.4,
"saturationRatio": 2.4
},
{
"id": 400,
"displayName": "Pumpkin Pie",
"stackSize": 64,
"name": "pumpkin_pie",
"foodPoints": 8,
"saturation": 4.8,
"effectiveQuality": 12.8,
"saturationRatio": 0.6
},
{
"id": 411,
"displayName": "Raw Rabbit",
"stackSize": 64,
"name": "rabbit",
"foodPoints": 3,
"saturation": 1.8,
"effectiveQuality": 4.8,
"saturationRatio": 0.6
},
{
"id": 412,
"displayName": "Cooked Rabbit",
"stackSize": 64,
"name": "cooked_rabbit",
"foodPoints": 5,
"saturation": 6,
"effectiveQuality": 11,
"saturationRatio": 1.2
},
{
"id": 413,
"displayName": "Rabbit Stew",
"stackSize": 1,
"name": "rabbit_stew",
"foodPoints": 10,
"saturation": 12,
"effectiveQuality": 22,
"saturationRatio": 1.2
},
{
"id": 423,
"displayName": "Raw Mutton",
"stackSize": 64,
"name": "mutton",
"foodPoints": 2,
"saturation": 1.2,
"effectiveQuality": 3.2,
"saturationRatio": 0.6
},
{
"id": 424,
"displayName": "Cooked Mutton",
"stackSize": 64,
"name": "cooked_mutton",
"foodPoints": 6,
"saturation": 9.6,
"effectiveQuality": 15.6,
"saturationRatio": 1.5999999999999999
}
]
@@ -0,0 +1,22 @@
[
{
"id": 0,
"name": "harp"
},
{
"id": 1,
"name": "doubleBass"
},
{
"id": 2,
"name": "snareDrum"
},
{
"id": 3,
"name": "sticks"
},
{
"id": 4,
"name": "bassDrum"
}
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
{
"rock": {
"257": 6,
"270": 2,
"274": 4,
"278": 8,
"285": 12
},
"wood": {
"258": 6,
"271": 2,
"275": 4,
"279": 8,
"286": 12
},
"plant": {
"258": 6,
"267": 1.5,
"268": 1.5,
"271": 2,
"272": 1.5,
"275": 4,
"276": 1.5,
"279": 8,
"283": 1.5,
"286": 12
},
"melon": {
"267": 1.5,
"268": 1.5,
"272": 1.5,
"276": 1.5,
"283": 1.5
},
"leaves": {
"267": 1.5,
"268": 1.5,
"272": 1.5,
"276": 1.5,
"283": 1.5,
"359": 6
},
"dirt": {
"256": 6,
"269": 2,
"273": 4,
"277": 8,
"284": 12
},
"web": {
"267": 15,
"268": 15,
"272": 15,
"276": 15,
"283": 15,
"359": 15
},
"wool": {
"359": 4.8
}
}
@@ -0,0 +1,170 @@
[
{
"id": 0,
"name": "explode"
},
{
"id": 1,
"name": "largeexplode"
},
{
"id": 2,
"name": "hugeexplosion"
},
{
"id": 3,
"name": "fireworksSpark"
},
{
"id": 4,
"name": "bubble"
},
{
"id": 5,
"name": "splash"
},
{
"id": 6,
"name": "wake"
},
{
"id": 7,
"name": "suspended"
},
{
"id": 8,
"name": "depthsuspend"
},
{
"id": 9,
"name": "crit"
},
{
"id": 10,
"name": "magicCrit"
},
{
"id": 11,
"name": "smoke"
},
{
"id": 12,
"name": "largesmoke"
},
{
"id": 13,
"name": "spell"
},
{
"id": 14,
"name": "instantSpell"
},
{
"id": 15,
"name": "mobSpell"
},
{
"id": 16,
"name": "mobSpellAmbient"
},
{
"id": 17,
"name": "witchMagic"
},
{
"id": 18,
"name": "dripWater"
},
{
"id": 19,
"name": "dripLava"
},
{
"id": 20,
"name": "angryVillager"
},
{
"id": 21,
"name": "happyVillager"
},
{
"id": 22,
"name": "townaura"
},
{
"id": 23,
"name": "note"
},
{
"id": 24,
"name": "portal"
},
{
"id": 25,
"name": "enchantmenttable"
},
{
"id": 26,
"name": "flame"
},
{
"id": 27,
"name": "lava"
},
{
"id": 28,
"name": "footstep"
},
{
"id": 29,
"name": "cloud"
},
{
"id": 30,
"name": "reddust"
},
{
"id": 31,
"name": "snowballpoof"
},
{
"id": 32,
"name": "snowshovel"
},
{
"id": 33,
"name": "slime"
},
{
"id": 34,
"name": "heart"
},
{
"id": 35,
"name": "barrier"
},
{
"id": 36,
"name": "iconcrack_"
},
{
"id": 37,
"name": "blockcrack_"
},
{
"id": 38,
"name": "blockdust_"
},
{
"id": 39,
"name": "droplet"
},
{
"id": 40,
"name": "take"
},
{
"id": 41,
"name": "mobappearance"
}
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
{
"version":47,
"minecraftVersion":"1.8.8",
"majorVersion":"1.8"
}
+229
View File
@@ -0,0 +1,229 @@
[
{
"id": "",
"name": "Player",
"slots": [
{
"name": "craft result",
"index": 0
},
{
"name": "craft grid",
"index": 1,
"size": 4
},
{
"name": "armor",
"index": 5,
"size": 4
},
{
"name": "helmet",
"index": 5
},
{
"name": "chestplate",
"index": 6
},
{
"name": "leggings",
"index": 7
},
{
"name": "boots",
"index": 8
}
]
},
{
"id": "EntityHorse",
"name": "Horse",
"slots": [
{
"name": "saddle",
"index": 0
},
{
"name": "armor",
"index": 1
},
{
"name": "storage",
"index": 2,
"size": 15
}
],
"openedWith": [
{
"type": "entity",
"id": 100
}
]
},
{
"id": "minecraft:anvil",
"name": "Anvil",
"slots": [
{
"name": "tool",
"index": 0
},
{
"name": "combined",
"index": 1
},
{
"name": "result",
"index": 2
}
],
"properties": [
"repair cost"
]
},
{
"id": "minecraft:beacon",
"name": "Beacon",
"slots": [
{
"name": "input",
"index": 0
}
],
"properties": [
"level",
"effect 1",
"effect 2"
],
"openedWith": [
{
"type": "block",
"id": 138
}
]
},
{
"id": "minecraft:brewing_stand",
"name": "Brewing Stand",
"slots": [
{
"name": "ingredient",
"index": 3
},
{
"name": "result",
"index": 0,
"size": 3
}
],
"properties": [
"brew time"
],
"openedWith": [
{
"type": "block",
"id": 117
}
]
},
{
"id": "minecraft:chest",
"name": "Chest"
},
{
"id": "minecraft:container",
"name": "Container"
},
{
"id": "minecraft:crafting_table",
"name": "Workbench",
"slots": [
{
"name": "craft result",
"index": 0
},
{
"name": "craft grid",
"index": 1,
"size": 9
}
]
},
{
"id": "minecraft:dispenser",
"name": "Dispenser"
},
{
"id": "minecraft:dropper",
"name": "Dropper"
},
{
"id": "minecraft:enchanting_table",
"name": "Enchantment Table",
"slots": [
{
"name": "enchanted",
"index": 0
},
{
"name": "lapis",
"index": 1
}
],
"properties": [
"xp 1",
"xp 2",
"xp 3",
"seed",
"tooltip 1",
"tooltip 2",
"tooltip 3"
]
},
{
"id": "minecraft:furnace",
"name": "Furnace",
"slots": [
{
"name": "smelted",
"index": 0
},
{
"name": "fuel",
"index": 1
},
{
"name": "result",
"index": 2
}
],
"properties": [
"fuel",
"fuel max",
"progress",
"progress max"
]
},
{
"id": "minecraft:hopper",
"name": "Hopper"
},
{
"id": "minecraft:villager",
"name": "NPC Trade",
"slots": [
{
"name": "give 1",
"index": 0
},
{
"name": "give 2",
"index": 1
},
{
"name": "take",
"index": 2
}
]
}
]
+3
View File
@@ -20,6 +20,7 @@ type VersionInfo struct {
var ProtocolVersions []VersionInfo var ProtocolVersions []VersionInfo
// LookUpVersionByName returns the VersionInfo for the given Minecraft version name (e.g. "1.21.11").
func LookUpVersionByName(name string) (ret VersionInfo, err error) { func LookUpVersionByName(name string) (ret VersionInfo, err error) {
for _, v := range ProtocolVersions { for _, v := range ProtocolVersions {
if v.MinecraftVersion == name { if v.MinecraftVersion == name {
@@ -31,6 +32,7 @@ func LookUpVersionByName(name string) (ret VersionInfo, err error) {
return return
} }
// LookUpVersionByProtocolVersion returns the VersionInfo for the given protocol version number.
func LookUpVersionByProtocolVersion(version int32) (ret VersionInfo, err error) { func LookUpVersionByProtocolVersion(version int32) (ret VersionInfo, err error) {
for _, v := range ProtocolVersions { for _, v := range ProtocolVersions {
if v.Version == version { if v.Version == version {
@@ -42,6 +44,7 @@ func LookUpVersionByProtocolVersion(version int32) (ret VersionInfo, err error)
return return
} }
// LookUpVersionByDataVersion returns the VersionInfo for the given Minecraft data version number.
func LookUpVersionByDataVersion(version int32) (ret VersionInfo, err error) { func LookUpVersionByDataVersion(version int32) (ret VersionInfo, err error) {
for _, v := range ProtocolVersions { for _, v := range ProtocolVersions {
if v.DataVersion == version { if v.DataVersion == version {
+1
View File
@@ -11,6 +11,7 @@ import (
//go:embed registry/* //go:embed registry/*
var Registry embed.FS var Registry embed.FS
// LoadRegistry loads a registry NBT file by name from the embedded registry data.
func LoadRegistry(name string) (n *nbt2.Tag, err error) { func LoadRegistry(name string) (n *nbt2.Tag, err error) {
r, err := Registry.Open("registry/" + name + ".nbt") r, err := Registry.Open("registry/" + name + ".nbt")
if err != nil { if err != nil {
+8 -3
View File
@@ -69,6 +69,8 @@ var WhileClosingErr = errors.New("error while closing")
var ContextDoneErr = errors.New("context done") var ContextDoneErr = errors.New("context done")
var RecvNotOkErr = errors.New("recv not ok") var RecvNotOkErr = errors.New("recv not ok")
// ValidateHandler validates that h is a function taking a single argument and returning an error.
// It panics if the handler does not match the expected signature.
func ValidateHandler(h any) (eventType reflect.Type, hv reflect.Value) { func ValidateHandler(h any) (eventType reflect.Type, hv reflect.Value) {
hv = reflect.ValueOf(h) hv = reflect.ValueOf(h)
if hv.Kind() != reflect.Func { if hv.Kind() != reflect.Func {
@@ -139,9 +141,11 @@ func (l *Loop) RegisterLow(eventType reflect.Type, hv reflect.Value, critical bo
// RegisterCritical registers a handler that will close the loop if it returns an error. // RegisterCritical registers a handler that will close the loop if it returns an error.
// Warning: If a bad handler is passed in the program will panic. // Warning: If a bad handler is passed in the program will panic.
func (l *Loop) RegisterCritical(h any) { func (l *Loop) RegisterCritical(hs ...any) {
eventType, hv := ValidateHandler(h) for _, h := range hs {
l.HandlerFunctions[eventType] = append(l.HandlerFunctions[eventType], hv) eventType, hv := ValidateHandler(h)
l.HandlerFunctions[eventType] = append(l.HandlerFunctions[eventType], hv)
}
} }
// FireFound triggers all handlers registered for the specified event type and returns if handlers were found or an error occurred. // FireFound triggers all handlers registered for the specified event type and returns if handlers were found or an error occurred.
@@ -233,6 +237,7 @@ func (l *Loop) StartLoop() (err error) {
return return
} }
// NewLoop creates a new Loop with a background context.
func NewLoop() (l *Loop) { func NewLoop() (l *Loop) {
return NewLoopCtx(context.Background()) return NewLoopCtx(context.Background())
} }
+1
View File
@@ -48,6 +48,7 @@ type BiomeNoise struct {
Temperature, Humidity, ContinentalNess, Erosion, Shift, Weirdness DoublePerlin Temperature, Humidity, ContinentalNess, Erosion, Shift, Weirdness DoublePerlin
} }
// BiomeNoiseFromXoroshiro creates a BiomeNoise from the given Xoroshiro source.
func BiomeNoiseFromXoroshiro(x Xoroshiro, large bool) (v BiomeNoise) { func BiomeNoiseFromXoroshiro(x Xoroshiro, large bool) (v BiomeNoise) {
x = x.Split() x = x.Split()
+1
View File
@@ -6,6 +6,7 @@ type DoublePerlin struct {
B Octave B Octave
} }
// DoublePerlinFromXoroshiro creates a DoublePerlin from the given Xoroshiro source.
func DoublePerlinFromXoroshiro(x *Xoroshiro, amplitudes []float64, omin int, lenn int, nmax int) (d DoublePerlin) { func DoublePerlinFromXoroshiro(x *Xoroshiro, amplitudes []float64, omin int, lenn int, nmax int) (d DoublePerlin) {
na := -1 na := -1
nb := -1 nb := -1
+1
View File
@@ -116,6 +116,7 @@ func (n PerlinNoise) Sample(x, y, z float64, yamp, ymin float64) (v float64) {
return lerp(t3, l1, l5) return lerp(t3, l1, l5)
} }
// PerlinNoiseFromXoroshiro initializes a PerlinNoise from the given Xoroshiro source.
func PerlinNoiseFromXoroshiro(x Xoroshiro) (p PerlinNoise) { func PerlinNoiseFromXoroshiro(x Xoroshiro) (p PerlinNoise) {
p.xoffset = x.NextFloat64() * 256.0 p.xoffset = x.NextFloat64() * 256.0
p.yoffset = x.NextFloat64() * 256.0 p.yoffset = x.NextFloat64() * 256.0
+1
View File
@@ -28,6 +28,7 @@ var octaveLacunaInits = []float64{1, .5, .25, 1. / 8, 1. / 16, 1. / 32, 1. / 64,
var octavePersistInits = []float64{0, 1, 2. / 3, 4. / 7, 8. / 15, 16. / 31, 32. / 63, 64. / 127, 128. / 255, 256. / 511, 512. / 1023, var octavePersistInits = []float64{0, 1, 2. / 3, 4. / 7, 8. / 15, 16. / 31, 32. / 63, 64. / 127, 128. / 255, 256. / 511, 512. / 1023,
1024. / 2047, 2048. / 4095, 4096. / 8191, 8192. / 16383, 16384. / 32767, 32768. / 65535} 1024. / 2047, 2048. / 4095, 4096. / 8191, 8192. / 16383, 16384. / 32767, 32768. / 65535}
// OctaveFromXoroshiro creates an Octave (multi-octave Perlin noise) from the given Xoroshiro source.
func OctaveFromXoroshiro(x *Xoroshiro, amplitudes []float64, omin int, lenn int, nmax int) (o Octave) { func OctaveFromXoroshiro(x *Xoroshiro, amplitudes []float64, omin int, lenn int, nmax int) (o Octave) {
if (-omin < 0 || -omin >= len(octaveLacunaInits)) || (lenn < 0 || lenn >= len(octavePersistInits)) { if (-omin < 0 || -omin >= len(octaveLacunaInits)) || (lenn < 0 || lenn >= len(octavePersistInits)) {
panic("octave initialization out of range") panic("octave initialization out of range")
+1
View File
@@ -183,6 +183,7 @@ func landSpline(f, g, h, i, j, k float32, bl bool) Spline {
return s return s
} }
// CreateBiomeDepthSpline builds the Minecraft biome depth spline used for terrain generation.
func CreateBiomeDepthSpline() Spline { func CreateBiomeDepthSpline() Spline {
sp1 := landSpline(-0.15, 0.00, 0.0, 0.1, 0.00, -0.03, false) sp1 := landSpline(-0.15, 0.00, 0.0, 0.1, 0.00, -0.03, false)
sp2 := landSpline(-0.10, 0.03, 0.1, 0.1, 0.01, -0.03, false) sp2 := landSpline(-0.10, 0.03, 0.1, 0.1, 0.01, -0.03, false)
+2
View File
@@ -42,6 +42,7 @@ func (x *Xoroshiro) NextFloat64() float64 {
return float64(x.Next()>>(64-53)) * 1.1102230246251565e-16 return float64(x.Next()>>(64-53)) * 1.1102230246251565e-16
} }
// XoroshiroFromString creates an Xoroshiro from an MD5 hash of the given string.
func XoroshiroFromString(s string) (x Xoroshiro) { func XoroshiroFromString(s string) (x Xoroshiro) {
sum := md5.Sum([]byte(s)) sum := md5.Sum([]byte(s))
x.L = binary.BigEndian.Uint64(sum[:8]) x.L = binary.BigEndian.Uint64(sum[:8])
@@ -49,6 +50,7 @@ func XoroshiroFromString(s string) (x Xoroshiro) {
return return
} }
// XoroshiroFromSeed creates an Xoroshiro from an int64 seed.
func XoroshiroFromSeed(seed int64) (x Xoroshiro) { func XoroshiroFromSeed(seed int64) (x Xoroshiro) {
x.L = uint64(seed) ^ 0x6a09e667f3bcc909 x.L = uint64(seed) ^ 0x6a09e667f3bcc909
x.H = x.L + 0x9e3779b97f4a7c15 x.H = x.L + 0x9e3779b97f4a7c15
+1
View File
@@ -174,6 +174,7 @@ type Attribute struct {
Base float64 Base float64
} }
// ReadLevelData reads and decodes the level.dat NBT file from a reader.
func ReadLevelData(r io.Reader) (l Level, err error) { func ReadLevelData(r io.Reader) (l Level, err error) {
var lDataWrapped struct{ Data Level } var lDataWrapped struct{ Data Level }
err = nbt.ReadFile(r, &lDataWrapped) err = nbt.ReadFile(r, &lDataWrapped)
+1
View File
@@ -52,6 +52,7 @@ type ChunkHeader struct {
Chunk, Time [ChunksRegionFile]int32 Chunk, Time [ChunksRegionFile]int32
} }
// ReadChunkHeader reads the chunk header from an MCA region file.
func ReadChunkHeader(r *os.File) (ch *ChunkHeader, err error) { func ReadChunkHeader(r *os.File) (ch *ChunkHeader, err error) {
var h ChunkHeader var h ChunkHeader
err = binary.Read(r, binary.BigEndian, &h) err = binary.Read(r, binary.BigEndian, &h)
+1
View File
@@ -10,6 +10,7 @@ type Chunk struct {
Version string Version string
} }
// ReadChunkFromChunkPacketData decodes chunk packet data from a reader into a Chunk.
func ReadChunkFromChunkPacketData(r io.Reader, version string, worldHeight int) (c *Chunk, err error) { func ReadChunkFromChunkPacketData(r io.Reader, version string, worldHeight int) (c *Chunk, err error) {
c = &Chunk{} c = &Chunk{}
c.Version = version c.Version = version
+3
View File
@@ -94,6 +94,7 @@ type Structure struct {
Version string Version string
} }
// LoadFromPath loads a litematic file from disk.
func LoadFromPath(path string) (s *Structure, err error) { func LoadFromPath(path string) (s *Structure, err error) {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
@@ -104,6 +105,7 @@ func LoadFromPath(path string) (s *Structure, err error) {
return return
} }
// LoadFromFile reads and parses a litematic file from a reader.
func LoadFromFile(r io.Reader) (s *Structure, err error) { func LoadFromFile(r io.Reader) (s *Structure, err error) {
n, err := nbt.ReadUnstructuredFile(r) n, err := nbt.ReadUnstructuredFile(r)
if err != nil { if err != nil {
@@ -112,6 +114,7 @@ func LoadFromFile(r io.Reader) (s *Structure, err error) {
return Load(n.Value) return Load(n.Value)
} }
// Load decodes raw NBT data into a litematic Structure.
func Load(v any) (s *Structure, err error) { func Load(v any) (s *Structure, err error) {
s = &Structure{} s = &Structure{}
err = mapstructure.Decode(v, &s.File) err = mapstructure.Decode(v, &s.File)
+7
View File
@@ -30,6 +30,7 @@ type Section struct {
Blocks, Biomes *Storage Blocks, Biomes *Storage
} }
// MakeBiomeFormat returns the StorageFormat for biome data at the given version.
func MakeBiomeFormat(version string) StorageFormat { func MakeBiomeFormat(version string) StorageFormat {
directBpe := uint8(math.Ceil(math.Log2(float64(len(data2.BiomesForVersion(version)))))) directBpe := uint8(math.Ceil(math.Log2(float64(len(data2.BiomesForVersion(version))))))
return StorageFormat{ return StorageFormat{
@@ -39,6 +40,7 @@ func MakeBiomeFormat(version string) StorageFormat {
} }
} }
// MakeBlockFormat returns the StorageFormat for block data at the given version.
func MakeBlockFormat(version string) StorageFormat { func MakeBlockFormat(version string) StorageFormat {
directBpe := uint8(math.Ceil(math.Log2(float64(len(data2.BlocksForVersion(version)))))) directBpe := uint8(math.Ceil(math.Log2(float64(len(data2.BlocksForVersion(version))))))
return StorageFormat{ return StorageFormat{
@@ -51,6 +53,7 @@ func MakeBlockFormat(version string) StorageFormat {
var BadPaletteLenErr = errors.New("bad palette length") var BadPaletteLenErr = errors.New("bad palette length")
// ReadSectionStorage reads a packed section storage from a reader.
func ReadSectionStorage(r io.Reader, format StorageFormat) (s *Storage, err error) { func ReadSectionStorage(r io.Reader, format StorageFormat) (s *Storage, err error) {
var bpe uint8 var bpe uint8
err = binary.Read(r, binary.BigEndian, &bpe) err = binary.Read(r, binary.BigEndian, &bpe)
@@ -97,6 +100,7 @@ func ReadSectionStorage(r io.Reader, format StorageFormat) (s *Storage, err erro
return format.ImportFromReader(r, bpe, pallette) return format.ImportFromReader(r, bpe, pallette)
} }
// WriteSectionStorage writes a packed section storage to a writer.
func WriteSectionStorage(w io.Writer, s *Storage) (err error) { func WriteSectionStorage(w io.Writer, s *Storage) (err error) {
err = binary.Write(w, binary.BigEndian, s.bpe) err = binary.Write(w, binary.BigEndian, s.bpe)
if err != nil { if err != nil {
@@ -122,6 +126,7 @@ func WriteSectionStorage(w io.Writer, s *Storage) (err error) {
return binary.Write(w, binary.BigEndian, s.data) return binary.Write(w, binary.BigEndian, s.data)
} }
// SectionDecodePacket decodes a chunk section from the Minecraft packet wire format.
func SectionDecodePacket(r io.Reader, version string) (s Section, err error) { func SectionDecodePacket(r io.Reader, version string) (s Section, err error) {
s = Section{} s = Section{}
err = binary.Read(r, binary.BigEndian, &s.BlockCount) err = binary.Read(r, binary.BigEndian, &s.BlockCount)
@@ -136,6 +141,7 @@ func SectionDecodePacket(r io.Reader, version string) (s Section, err error) {
return return
} }
// SectionEncodePacket encodes a chunk section to the Minecraft packet wire format.
func SectionEncodePacket(w io.Writer, s Section) (err error) { func SectionEncodePacket(w io.Writer, s Section) (err error) {
err = binary.Write(w, binary.BigEndian, s.BlockCount) err = binary.Write(w, binary.BigEndian, s.BlockCount)
if err != nil { if err != nil {
@@ -149,6 +155,7 @@ func SectionEncodePacket(w io.Writer, s Section) (err error) {
return return
} }
// SectionEquals returns whether two sections are equal.
func SectionEquals(a, b Section) bool { func SectionEquals(a, b Section) bool {
if a.BlockCount != b.BlockCount { if a.BlockCount != b.BlockCount {
return false return false
+1
View File
@@ -250,6 +250,7 @@ func (r *Storage) Equals(other *Storage) bool {
return true return true
} }
// CountStorage counts the number of elements in the storage that match the given value.
func CountStorage(s *Storage, v int32) (a int) { func CountStorage(s *Storage, v int32) (a int) {
for i := range s.format.Len { for i := range s.format.Len {
if w, _ := s.Get(i); w == v { if w, _ := s.Get(i); w == v {
+1
View File
@@ -5,6 +5,7 @@ import (
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
// Load reads a structure NBT file and returns the decoded Structure.
func Load(fileName string) (s *Structure, err error) { func Load(fileName string) (s *Structure, err error) {
n, err := nbt.ReadUnstructuredFilePath(fileName) n, err := nbt.ReadUnstructuredFilePath(fileName)
if err != nil { if err != nil {
+1
View File
@@ -2,6 +2,7 @@ package mapstructure
var DefaultFormat = NewFormat("mapstructure", WithRequireAll(), WithTrySnakeCase(), WithTryLowCase()) var DefaultFormat = NewFormat("mapstructure", WithRequireAll(), WithTrySnakeCase(), WithTryLowCase())
// Decode is a stub placeholder. Use Format.Decode instead.
func Decode(data any, v any) error { func Decode(data any, v any) error {
return nil return nil
} }
+8
View File
@@ -25,6 +25,7 @@ type Format struct {
encoders map[reflect.Type]func(any, string) (any, error) encoders map[reflect.Type]func(any, string) (any, error)
} }
// NewFormat creates a new Format with the given name and options.
func NewFormat(name string, opts ...Option) *Format { func NewFormat(name string, opts ...Option) *Format {
f := &Format{ f := &Format{
Name: name, Name: name,
@@ -39,30 +40,35 @@ func NewFormat(name string, opts ...Option) *Format {
type Option func(*Format) type Option func(*Format)
// WithRequireAll returns an Option that requires all struct fields to be present in the map.
func WithRequireAll() Option { func WithRequireAll() Option {
return func(f *Format) { return func(f *Format) {
f.requireAll = true f.requireAll = true
} }
} }
// WithErrorOnExtra returns an Option that causes an error when the map contains unmatched keys.
func WithErrorOnExtra() Option { func WithErrorOnExtra() Option {
return func(f *Format) { return func(f *Format) {
f.errorOnExtra = true f.errorOnExtra = true
} }
} }
// WithTrySnakeCase returns an Option that tries to match struct fields using snake_case names.
func WithTrySnakeCase() Option { func WithTrySnakeCase() Option {
return func(f *Format) { return func(f *Format) {
f.trySnakeCase = true f.trySnakeCase = true
} }
} }
// WithTryLowCase returns an Option that tries to match struct fields using lowercase names.
func WithTryLowCase() Option { func WithTryLowCase() Option {
return func(f *Format) { return func(f *Format) {
f.tryLowCase = true f.tryLowCase = true
} }
} }
// WithTypeCodec returns an Option that registers custom decode/encode functions for a type.
func WithTypeCodec(decodeFn, encodeFn any) Option { func WithTypeCodec(decodeFn, encodeFn any) Option {
dt := reflect.TypeOf(decodeFn) dt := reflect.TypeOf(decodeFn)
et := reflect.TypeOf(encodeFn) et := reflect.TypeOf(encodeFn)
@@ -94,6 +100,7 @@ func WithTypeCodec(decodeFn, encodeFn any) Option {
} }
} }
// Decode populates target (a pointer to a struct) from data (a map[string]any).
func (f *Format) Decode(data any, target any) error { func (f *Format) Decode(data any, target any) error {
v := reflect.ValueOf(target) v := reflect.ValueOf(target)
if v.Kind() != reflect.Pointer || v.IsNil() { if v.Kind() != reflect.Pointer || v.IsNil() {
@@ -110,6 +117,7 @@ func (f *Format) Decode(data any, target any) error {
return f.decode(m, v) return f.decode(m, v)
} }
// Encode converts source (a struct or pointer to a struct) into a map[string]any.
func (f *Format) Encode(source any) (map[string]any, error) { func (f *Format) Encode(source any) (map[string]any, error) {
v := reflect.ValueOf(source) v := reflect.ValueOf(source)
if v.Kind() == reflect.Pointer { if v.Kind() == reflect.Pointer {
+5
View File
@@ -10,6 +10,7 @@ import (
var Format = mapstructure.NewFormat("nbt", mapstructure.WithRequireAll(), mapstructure.WithTrySnakeCase(), mapstructure.WithTryLowCase()) var Format = mapstructure.NewFormat("nbt", mapstructure.WithRequireAll(), mapstructure.WithTrySnakeCase(), mapstructure.WithTryLowCase())
// ReadFile reads a gzipped NBT file from r and decodes it into data using the package-level Format.
func ReadFile(file io.Reader, data any) (err error) { func ReadFile(file io.Reader, data any) (err error) {
n, err := ReadUnstructuredFile(file) n, err := ReadUnstructuredFile(file)
if err != nil { if err != nil {
@@ -18,6 +19,7 @@ func ReadFile(file io.Reader, data any) (err error) {
return Format.Decode(n.Value, data) return Format.Decode(n.Value, data)
} }
// WriteUnstructuredFilePath writes an NBT tag as a gzipped file to the given filesystem path.
func WriteUnstructuredFilePath(path string, n *Tag) (err error) { func WriteUnstructuredFilePath(path string, n *Tag) (err error) {
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {
@@ -27,6 +29,7 @@ func WriteUnstructuredFilePath(path string, n *Tag) (err error) {
return WriteUnstructuredFile(f, n) return WriteUnstructuredFile(f, n)
} }
// WriteUnstructuredFile writes an NBT tag as a gzipped file to w.
func WriteUnstructuredFile(file io.Writer, n *Tag) (err error) { func WriteUnstructuredFile(file io.Writer, n *Tag) (err error) {
gzw := gzip.NewWriter(file) gzw := gzip.NewWriter(file)
defer gzw.Close() defer gzw.Close()
@@ -34,6 +37,7 @@ func WriteUnstructuredFile(file io.Writer, n *Tag) (err error) {
return return
} }
// ReadUnstructuredFile reads a gzipped NBT file from r and returns the root Tag.
func ReadUnstructuredFile(file io.Reader) (n *Tag, err error) { func ReadUnstructuredFile(file io.Reader) (n *Tag, err error) {
r, err := gzip.NewReader(file) r, err := gzip.NewReader(file)
if err != nil { if err != nil {
@@ -45,6 +49,7 @@ func ReadUnstructuredFile(file io.Reader) (n *Tag, err error) {
return return
} }
// ReadUnstructuredFilePath reads a gzipped NBT file from the given filesystem path and returns the root Tag.
func ReadUnstructuredFilePath(path string) (n *Tag, err error) { func ReadUnstructuredFilePath(path string) (n *Tag, err error) {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
+14
View File
@@ -73,6 +73,7 @@ type Conn struct {
Log *slog.Logger Log *slog.Logger
} }
// StartConn starts the connection event loop, registering the receive routine and login tick handler.
func (c *Conn) StartConn() (err error) { func (c *Conn) StartConn() (err error) {
c.RegisterEventSource(c.ActivateReceiveRoutine()) c.RegisterEventSource(c.ActivateReceiveRoutine())
c.RegisterCritical(c.OnLoginTick) c.RegisterCritical(c.OnLoginTick)
@@ -81,6 +82,7 @@ func (c *Conn) StartConn() (err error) {
return return
} }
// OnLoginTick handles a login tick by receiving a packet and firing it as an event.
func (c *Conn) OnLoginTick(_ LoginTick) (err error) { func (c *Conn) OnLoginTick(_ LoginTick) (err error) {
packet, err := c.Receive() packet, err := c.Receive()
if err != nil { if err != nil {
@@ -90,30 +92,35 @@ func (c *Conn) OnLoginTick(_ LoginTick) (err error) {
return return
} }
// SetState sets the connection's protocol state.
func (c *Conn) SetState(state proto_base.State) { func (c *Conn) SetState(state proto_base.State) {
c.stateMutex.Lock() c.stateMutex.Lock()
defer c.stateMutex.Unlock() defer c.stateMutex.Unlock()
c.state = state c.state = state
} }
// State returns the current connection protocol state.
func (c *Conn) State() proto_base.State { func (c *Conn) State() proto_base.State {
c.stateMutex.RLock() c.stateMutex.RLock()
defer c.stateMutex.RUnlock() defer c.stateMutex.RUnlock()
return c.state return c.state
} }
// SetAsyncPackets enables or disables asynchronous packet handling in the receive routine.
func (c *Conn) SetAsyncPackets(asyncPackets bool) { func (c *Conn) SetAsyncPackets(asyncPackets bool) {
c.asyncPacketsMutex.Lock() c.asyncPacketsMutex.Lock()
defer c.asyncPacketsMutex.Unlock() defer c.asyncPacketsMutex.Unlock()
c.asyncPackets = asyncPackets c.asyncPackets = asyncPackets
} }
// GetAsyncPackets returns whether asynchronous packet handling is enabled.
func (c *Conn) GetAsyncPackets() bool { func (c *Conn) GetAsyncPackets() bool {
c.asyncPacketsMutex.RLock() c.asyncPacketsMutex.RLock()
defer c.asyncPacketsMutex.RUnlock() defer c.asyncPacketsMutex.RUnlock()
return c.asyncPackets return c.asyncPackets
} }
// SetVersion sets the connection's Minecraft version by version name and looks up the corresponding protocol version.
func (c *Conn) SetVersion(version string) (err error) { func (c *Conn) SetVersion(version string) (err error) {
versionData, err := data.LookUpVersionByName(version) versionData, err := data.LookUpVersionByName(version)
if err != nil { if err != nil {
@@ -124,6 +131,7 @@ func (c *Conn) SetVersion(version string) (err error) {
return return
} }
// SetProtocolVersion sets the connection's version by protocol version number and looks up the corresponding version name.
func (c *Conn) SetProtocolVersion(version int32) (err error) { func (c *Conn) SetProtocolVersion(version int32) (err error) {
versionData, err := data.LookUpVersionByProtocolVersion(version) versionData, err := data.LookUpVersionByProtocolVersion(version)
if err != nil { if err != nil {
@@ -202,6 +210,7 @@ func SimplePacketToBytes(packet proto_base.EncodeDecodeAble) (b []byte, err erro
return return
} }
// SimplePacketToBytesLenPrefix is like SimplePacketToBytes but also prepends the length-prefix framing.
func SimplePacketToBytesLenPrefix(packet proto_base.EncodeDecodeAble) (b []byte, err error) { func SimplePacketToBytesLenPrefix(packet proto_base.EncodeDecodeAble) (b []byte, err error) {
b, err = SimplePacketToBytes(packet) b, err = SimplePacketToBytes(packet)
if err != nil { if err != nil {
@@ -282,6 +291,7 @@ func (c *Conn) sendRegisteredPacket(packet proto_base.EncodeDecodeAble) (err err
return c.SendRaw(packetBuff.Bytes()) return c.SendRaw(packetBuff.Bytes())
} }
// Send encodes and sends a packet to the remote peer, translating protocol versions if needed.
func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) { func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) {
switch packet := packet.(type) { switch packet := packet.(type) {
case *UnCodablePacket: case *UnCodablePacket:
@@ -292,6 +302,7 @@ func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) {
return return
} }
// Receive reads and decodes a raw packet from the remote peer.
func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) { func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) {
if c.R == nil { if c.R == nil {
return return
@@ -335,10 +346,13 @@ func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) {
} }
return return
} }
// NewConn creates a new Conn with default settings and a background context.
func NewConn() (ret *Conn) { func NewConn() (ret *Conn) {
return NewConnCtx(context.Background()) return NewConnCtx(context.Background())
} }
// NewConnCtx creates a new Conn with the given context, setting the handshaking state and latest version.
func NewConnCtx(ctx context.Context) (ret *Conn) { func NewConnCtx(ctx context.Context) (ret *Conn) {
ret = &Conn{} ret = &Conn{}
ret.SetState(proto_base.Handshaking) ret.SetState(proto_base.Handshaking)
+6
View File
@@ -22,6 +22,7 @@ func init() {
} }
} }
// LookUpTypeByPacketInfo looks up a packet's type information by direction, state, packet id, and protocol version.
func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.State, pid, version int32) (p proto_base.PacketInfo, ok bool) { func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.State, pid, version int32) (p proto_base.PacketInfo, ok bool) {
pP, ok := pidMap[PidEtc{direction, state, pid, version}] pP, ok := pidMap[PidEtc{direction, state, pid, version}]
if !ok { if !ok {
@@ -39,6 +40,7 @@ func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.Sta
return return
} }
// LookUpTypeByPacketInfoAndCopyType looks up a packet and returns a newly allocated instance of its type.
func LookUpTypeByPacketInfoAndCopyType(direction proto_base.Direction, state proto_base.State, pid, version int32) (p proto_base.EncodeDecodeAble, ok bool) { func LookUpTypeByPacketInfoAndCopyType(direction proto_base.Direction, state proto_base.State, pid, version int32) (p proto_base.EncodeDecodeAble, ok bool) {
i, ok := LookUpTypeByPacketInfo(direction, state, pid, version) i, ok := LookUpTypeByPacketInfo(direction, state, pid, version)
if !ok { if !ok {
@@ -48,6 +50,7 @@ func LookUpTypeByPacketInfoAndCopyType(direction proto_base.Direction, state pro
return return
} }
// LookupPacketInfoByType looks up a packet's type information by matching against the type of the given packet value.
func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (p proto_base.PacketInfo, ok bool) { func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (p proto_base.PacketInfo, ok bool) {
undecodedPacket, isUndecoable := packet.(*UnCodablePacket) undecodedPacket, isUndecoable := packet.(*UnCodablePacket)
if isUndecoable { if isUndecoable {
@@ -64,6 +67,7 @@ func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (p proto_base.Pa
return return
} }
// LookupPacketInfoByTypeAndState looks up a packet's type information by type and state.
func LookupPacketInfoByTypeAndState(packet proto_base.EncodeDecodeAble, state proto_base.State) (p proto_base.PacketInfo, ok bool) { func LookupPacketInfoByTypeAndState(packet proto_base.EncodeDecodeAble, state proto_base.State) (p proto_base.PacketInfo, ok bool) {
for _, p := range proto_generated.Packets { for _, p := range proto_generated.Packets {
if state == p.State && reflect.TypeOf(p.Type) == reflect.TypeOf(packet) { if state == p.State && reflect.TypeOf(p.Type) == reflect.TypeOf(packet) {
@@ -73,6 +77,7 @@ func LookupPacketInfoByTypeAndState(packet proto_base.EncodeDecodeAble, state pr
return return
} }
// LookupPacketInfoByNameProtocolVersionAndState looks up a packet's type information by name, protocol version, and state.
func LookupPacketInfoByNameProtocolVersionAndState(name string, version int32, state proto_base.State) (p proto_base.PacketInfo, ok bool) { func LookupPacketInfoByNameProtocolVersionAndState(name string, version int32, state proto_base.State) (p proto_base.PacketInfo, ok bool) {
for _, p := range proto_generated.Packets { for _, p := range proto_generated.Packets {
if state == p.State && p.Name == name && p.ProtocolVersion == version { if state == p.State && p.Name == name && p.ProtocolVersion == version {
@@ -82,6 +87,7 @@ func LookupPacketInfoByNameProtocolVersionAndState(name string, version int32, s
return return
} }
// LookupPacketInfoByNameProtocolVersionStateAndDirection looks up a packet's type information by name, protocol version, state, and direction.
func LookupPacketInfoByNameProtocolVersionStateAndDirection(name string, version int32, state proto_base.State, direction proto_base.Direction) (p proto_base.PacketInfo, ok bool) { func LookupPacketInfoByNameProtocolVersionStateAndDirection(name string, version int32, state proto_base.State, direction proto_base.Direction) (p proto_base.PacketInfo, ok bool) {
for _, p := range proto_generated.Packets { for _, p := range proto_generated.Packets {
if state == p.State && p.Name == name && p.ProtocolVersion == version && p.Direction == direction { if state == p.State && p.Name == name && p.ProtocolVersion == version && p.Direction == direction {
+24 -1
View File
@@ -20,21 +20,33 @@ type RawConn struct {
W io.Writer W io.Writer
compressionThreshold int32 compressionThreshold int32
CompressionThresholdMut sync.RWMutex CompressionThresholdMut sync.RWMutex
// MaxPacketSize is the maximum size of a packet that can be sent or received.
// Applies to both raw and compressed packets.
// Should be set while the connection is not in use. Or in sync mode.
MaxPacketSize int32
} }
// SetCompressionThreshold sets the packet compression threshold. Values below 0 disable compression.
func (c *RawConn) SetCompressionThreshold(threshold int32) { func (c *RawConn) SetCompressionThreshold(threshold int32) {
c.CompressionThresholdMut.Lock() c.CompressionThresholdMut.Lock()
defer c.CompressionThresholdMut.Unlock() defer c.CompressionThresholdMut.Unlock()
c.compressionThreshold = threshold c.compressionThreshold = threshold
} }
// GetCompressionThreshold returns the current compression threshold.
func (c *RawConn) GetCompressionThreshold() int32 { func (c *RawConn) GetCompressionThreshold() int32 {
c.CompressionThresholdMut.RLock() c.CompressionThresholdMut.RLock()
defer c.CompressionThresholdMut.RUnlock() defer c.CompressionThresholdMut.RUnlock()
return c.compressionThreshold return c.compressionThreshold
} }
// SendRaw sends raw packet bytes over the connection, applying compression if the threshold is met.
func (c *RawConn) SendRaw(rawPacketBytes []byte) (err error) { func (c *RawConn) SendRaw(rawPacketBytes []byte) (err error) {
if c.MaxPacketSize > 0 && int32(len(rawPacketBytes)) > c.MaxPacketSize {
err = errors.New("packet too large")
return
}
//fmt.Println("send:", rawPacketBytes) //fmt.Println("send:", rawPacketBytes)
var packetBytes []byte var packetBytes []byte
threshold := c.GetCompressionThreshold() threshold := c.GetCompressionThreshold()
@@ -45,7 +57,7 @@ func (c *RawConn) SendRaw(rawPacketBytes []byte) (err error) {
if err != nil { if err != nil {
return return
} }
zW := zlib.NewWriter(packetBuffer) // ignore we don't care about the result if it errors zW := zlib.NewWriter(packetBuffer)
_, err = zW.Write(rawPacketBytes) _, err = zW.Write(rawPacketBytes)
if err != nil { if err != nil {
return return
@@ -81,11 +93,17 @@ func (c *RawConn) SendRaw(rawPacketBytes []byte) (err error) {
return return
} }
// ReceiveRaw reads and decompresses (if needed) a raw packet from the connection.
func (c *RawConn) ReceiveRaw() (packetBytes []byte, err error) { func (c *RawConn) ReceiveRaw() (packetBytes []byte, err error) {
rawPacketLen, err := proto_base.DecodeVarInt(c.R) rawPacketLen, err := proto_base.DecodeVarInt(c.R)
if err != nil { if err != nil {
return return
} }
if c.MaxPacketSize > 0 && rawPacketLen > c.MaxPacketSize {
err = errors.New("packet too large")
return
}
rawPacketBytes, err := io.ReadAll(io.LimitReader(c.R, int64(rawPacketLen))) rawPacketBytes, err := io.ReadAll(io.LimitReader(c.R, int64(rawPacketLen)))
if err != nil { if err != nil {
return return
@@ -101,6 +119,10 @@ func (c *RawConn) ReceiveRaw() (packetBytes []byte, err error) {
if err != nil { if err != nil {
return return
} }
if c.MaxPacketSize > 0 && packetLen > c.MaxPacketSize {
err = errors.New("deflated packet too large")
return
}
if packetLen == 0 { if packetLen == 0 {
packetBytes, err = io.ReadAll(rawPacketBuffer) packetBytes, err = io.ReadAll(rawPacketBuffer)
if err != nil { if err != nil {
@@ -124,6 +146,7 @@ func (c *RawConn) ReceiveRaw() (packetBytes []byte, err error) {
return return
} }
// SetSecret enables AES/CFB8 encryption on the connection using the shared secret.
func (c *RawConn) SetSecret(sharedSecret []byte) (err error) { func (c *RawConn) SetSecret(sharedSecret []byte) (err error) {
var b cipher.Block var b cipher.Block
b, err = aes.NewCipher(sharedSecret) b, err = aes.NewCipher(sharedSecret)
+1
View File
@@ -16,6 +16,7 @@ type Reader struct {
Version int32 Version int32
} }
// NewReader creates a new replay Reader from an io.Reader.
func NewReader(r io.Reader) *Reader { func NewReader(r io.Reader) *Reader {
return &Reader{r: r} return &Reader{r: r}
} }
+1
View File
@@ -13,6 +13,7 @@ type Writer struct {
w io.Writer w io.Writer
} }
// NewWriter creates a new replay Writer from an io.Writer.
func NewWriter(w io.Writer) *Writer { func NewWriter(w io.Writer) *Writer {
return &Writer{w: w} return &Writer{w: w}
} }
+2
View File
@@ -11,6 +11,7 @@ import (
"github.com/admin-else/strom/mc/proto_generated" "github.com/admin-else/strom/mc/proto_generated"
) )
// SmartConvertibleTo reports whether a value of type from can be recursively converted to type to.
func SmartConvertibleTo(from, to reflect.Type) bool { func SmartConvertibleTo(from, to reflect.Type) bool {
switch from.Kind() { switch from.Kind() {
case reflect.Struct: case reflect.Struct:
@@ -36,6 +37,7 @@ func SmartConvertibleTo(from, to reflect.Type) bool {
} }
} }
// SmartConvert recursively converts a reflect.Value from one type to another compatible type.
func SmartConvert(from reflect.Value, to reflect.Type) (ret reflect.Value) { func SmartConvert(from reflect.Value, to reflect.Type) (ret reflect.Value) {
switch from.Kind() { switch from.Kind() {
case reflect.Struct: case reflect.Struct:
+40 -2
View File
@@ -7,10 +7,12 @@ import (
"math" "math"
) )
// UnimplementedErr is returned when a code path is not yet implemented.
var ( var (
UnimplementedErr = errors.New("unimplemented") UnimplementedErr = errors.New("unimplemented")
) )
// Direction indicates whether a packet is sent to the server or to the client.
type Direction int type Direction int
const ( const (
@@ -18,6 +20,7 @@ const (
ToClient ToClient
) )
// String returns a human-readable representation of the Direction.
func (d Direction) String() string { func (d Direction) String() string {
switch d { switch d {
case ToServer: case ToServer:
@@ -29,10 +32,12 @@ func (d Direction) String() string {
} }
} }
// Opposite returns the reverse Direction.
func (d Direction) Opposite() Direction { func (d Direction) Opposite() Direction {
return d ^ 1 return d ^ 1
} }
// State represents the Minecraft connection state.
type State int32 type State int32
const ( const (
@@ -43,6 +48,7 @@ const (
Play Play
) )
// String returns a human-readable representation of the State.
func (s State) String() string { func (s State) String() string {
switch s { switch s {
case Handshaking: case Handshaking:
@@ -60,13 +66,16 @@ func (s State) String() string {
} }
} }
// Actor represents a participant in a Minecraft connection.
type Actor int type Actor int
const ( const (
Servee Actor = iota // Servee is the one being served // Servee is the one being served (e.g. the backend server in a proxy).
Servee Actor = iota
Client Client
) )
// String returns a human-readable representation of the Actor.
func (a Actor) String() string { func (a Actor) String() string {
switch a { switch a {
case Servee: case Servee:
@@ -78,6 +87,7 @@ func (a Actor) String() string {
} }
} }
// SendDirection returns the Direction in which this Actor sends packets.
func (a Actor) SendDirection() Direction { func (a Actor) SendDirection() Direction {
switch a { switch a {
case Servee: case Servee:
@@ -89,6 +99,7 @@ func (a Actor) SendDirection() Direction {
} }
} }
// ReceiveDirection returns the Direction from which this Actor receives packets.
func (a Actor) ReceiveDirection() Direction { func (a Actor) ReceiveDirection() Direction {
switch a { switch a {
case Servee: case Servee:
@@ -101,13 +112,19 @@ func (a Actor) ReceiveDirection() Direction {
} }
type ( type (
ToDo struct{} // ToDo is a placeholder type for unimplemented packet fields.
ToDo struct{}
// RestBuffer represents the remaining unread bytes in a packet buffer.
RestBuffer []byte RestBuffer []byte
) )
// ToDoError is returned when a ToDo placeholder is used.
var ToDoError = errors.New("to do") var ToDoError = errors.New("to do")
// BadTypeError is returned when an unexpected type is encountered.
var BadTypeError = errors.New("bad type") var BadTypeError = errors.New("bad type")
// Encode writes the RestBuffer to w.
func (b *RestBuffer) Encode(w io.Writer) (err error) { func (b *RestBuffer) Encode(w io.Writer) (err error) {
n, err := w.Write(*b) n, err := w.Write(*b)
if n != len(*b) { if n != len(*b) {
@@ -116,11 +133,13 @@ func (b *RestBuffer) Encode(w io.Writer) (err error) {
return return
} }
// Decode reads all remaining bytes from r into the RestBuffer.
func (b *RestBuffer) Decode(r io.Reader) (err error) { func (b *RestBuffer) Decode(r io.Reader) (err error) {
*b, err = io.ReadAll(r) *b, err = io.ReadAll(r)
return return
} }
// EncodeString encodes a length-prefixed UTF-8 string in Minecraft varint format.
func EncodeString(w io.Writer, s string) (err error) { func EncodeString(w io.Writer, s string) (err error) {
err = EncodeVarInt(w, int32(len(s))) err = EncodeVarInt(w, int32(len(s)))
if err != nil { if err != nil {
@@ -130,6 +149,7 @@ func EncodeString(w io.Writer, s string) (err error) {
return return
} }
// DecodeString decodes a length-prefixed UTF-8 string in Minecraft varint format.
func DecodeString(r io.Reader) (ret string, err error) { func DecodeString(r io.Reader) (ret string, err error) {
l, err := DecodeVarInt(r) l, err := DecodeVarInt(r)
if err != nil { if err != nil {
@@ -146,16 +166,19 @@ func DecodeString(r io.Reader) (ret string, err error) {
return string(rawString), nil return string(rawString), nil
} }
// Encode is a no-op stub that always returns ToDoError.
func (t ToDo) Encode(_ io.Writer) (err error) { func (t ToDo) Encode(_ io.Writer) (err error) {
err = ToDoError err = ToDoError
return return
} }
// Decode is a no-op stub that always returns ToDoError.
func (t ToDo) Decode(_ io.Reader) (ret ToDo, err error) { func (t ToDo) Decode(_ io.Reader) (ret ToDo, err error) {
err = ToDoError err = ToDoError
return return
} }
// EncodeVarInt encodes a signed 32-bit integer in Minecraft varint format.
func EncodeVarInt(w io.Writer, v int32) (err error) { func EncodeVarInt(w io.Writer, v int32) (err error) {
uv := uint32(v) uv := uint32(v)
for { for {
@@ -175,6 +198,7 @@ func EncodeVarInt(w io.Writer, v int32) (err error) {
return nil return nil
} }
// DecodeVarInt decodes a signed 32-bit integer in Minecraft varint format.
func DecodeVarInt(r io.Reader) (ret int32, err error) { func DecodeVarInt(r io.Reader) (ret int32, err error) {
for i := range 5 { // 32/7 for i := range 5 { // 32/7
var b uint8 = 0 var b uint8 = 0
@@ -191,6 +215,7 @@ func DecodeVarInt(r io.Reader) (ret int32, err error) {
return return
} }
// EncodeVarLong encodes a signed 64-bit integer in Minecraft varlong format.
func EncodeVarLong(w io.Writer, v int64) (err error) { func EncodeVarLong(w io.Writer, v int64) (err error) {
uv := uint64(v) uv := uint64(v)
for { for {
@@ -210,6 +235,7 @@ func EncodeVarLong(w io.Writer, v int64) (err error) {
return nil return nil
} }
// DecodeVarLong decodes a signed 64-bit integer in Minecraft varlong format.
func DecodeVarLong(r io.Reader) (ret int64, err error) { func DecodeVarLong(r io.Reader) (ret int64, err error) {
for i := range 10 { // ceil(64/7) for i := range 10 { // ceil(64/7)
var b uint8 var b uint8
@@ -226,6 +252,7 @@ func DecodeVarLong(r io.Reader) (ret int64, err error) {
return return
} }
// ErroringIndex looks up key k in map m and returns an error if the key is not found.
func ErroringIndex[K comparable, V any, M map[K]V](m M, i K) (v V, err error) { func ErroringIndex[K comparable, V any, M map[K]V](m M, i K) (v V, err error) {
var ok bool var ok bool
v, ok = m[i] v, ok = m[i]
@@ -235,11 +262,14 @@ func ErroringIndex[K comparable, V any, M map[K]V](m M, i K) (v V, err error) {
return return
} }
// EncodeDecodeAble is implemented by types that can be encoded and decoded for
// Minecraft protocol packets.
type EncodeDecodeAble interface { type EncodeDecodeAble interface {
Encode(w io.Writer) (err error) Encode(w io.Writer) (err error)
Decode(r io.ReadSeeker) (err error) Decode(r io.ReadSeeker) (err error)
} }
// PacketInfo holds metadata about a Minecraft protocol packet.
type PacketInfo struct { type PacketInfo struct {
Type EncodeDecodeAble Type EncodeDecodeAble
Name string Name string
@@ -249,6 +279,7 @@ type PacketInfo struct {
ProtocolVersion int32 ProtocolVersion int32
} }
// Bool2uint8 converts a bool to a uint8 (1 for true, 0 for false).
func Bool2uint8(b bool) uint8 { func Bool2uint8(b bool) uint8 {
if b { if b {
return 1 return 1
@@ -256,6 +287,7 @@ func Bool2uint8(b bool) uint8 {
return 0 return 0
} }
// Bool2uint16 converts a bool to a uint16 (1 for true, 0 for false).
func Bool2uint16(b bool) uint16 { func Bool2uint16(b bool) uint16 {
if b { if b {
return 1 return 1
@@ -263,6 +295,7 @@ func Bool2uint16(b bool) uint16 {
return 0 return 0
} }
// Bool2uint32 converts a bool to a uint32 (1 for true, 0 for false).
func Bool2uint32(b bool) uint32 { func Bool2uint32(b bool) uint32 {
if b { if b {
return 1 return 1
@@ -270,6 +303,7 @@ func Bool2uint32(b bool) uint32 {
return 0 return 0
} }
// Bool2uint64 converts a bool to a uint64 (1 for true, 0 for false).
func Bool2uint64(b bool) uint64 { func Bool2uint64(b bool) uint64 {
if b { if b {
return 1 return 1
@@ -277,13 +311,17 @@ func Bool2uint64(b bool) uint64 {
return 0 return 0
} }
// LpVec3d is a compressed 3D velocity vector used in Minecraft packets.
type LpVec3d struct { type LpVec3d struct {
X, Y, Z float64 X, Y, Z float64
} }
// Encode encodes the LpVec3d as a compressed velocity triple into w.
func (v *LpVec3d) Encode(w io.Writer) error { func (v *LpVec3d) Encode(w io.Writer) error {
return writeVelocity(w, *v) return writeVelocity(w, *v)
} }
// Decode decodes a compressed velocity triple from r into the LpVec3d.
func (v *LpVec3d) Decode(r io.ReadSeeker) error { func (v *LpVec3d) Decode(r io.ReadSeeker) error {
v3, err := readVelocity(r) v3, err := readVelocity(r)
if err != nil { if err != nil {
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -429,7 +429,7 @@ func GenerateVersions(versions []string) (err error) {
} }
func main() { func main() {
err := GenerateVersions([]string{"1.21.8", "1.21.9", "1.21.11", "26.1", "26.2"}) err := GenerateVersions([]string{"1.8", "1.21.8", "1.21.9", "1.21.11", "26.1", "26.2"})
if err != nil { if err != nil {
panic(err) panic(err)
} }
+10
View File
@@ -12,6 +12,10 @@ import (
) )
func (g *Generator) ParseCompareTo(compareTo string) (e ast.Expr, cet CaseExprType, err error) { func (g *Generator) ParseCompareTo(compareTo string) (e ast.Expr, cet CaseExprType, err error) {
if strings.HasPrefix(compareTo, "$") {
err = fmt.Errorf("parameterized type placeholder: %s", compareTo)
return
}
parts := strings.Split(compareTo, "/") parts := strings.Split(compareTo, "/")
downPrefixCount := 0 downPrefixCount := 0
for _, part := range parts { for _, part := range parts {
@@ -124,6 +128,12 @@ func (g *Generator) RegisterCompareToNatives() {
"bitfield": BitfieldCompareTo, "bitfield": BitfieldCompareTo,
"u8": ReturnInputCompareTo, "u8": ReturnInputCompareTo,
"i8": ReturnInputCompareTo, "i8": ReturnInputCompareTo,
"i16": ReturnInputCompareTo,
"i32": ReturnInputCompareTo,
"i64": ReturnInputCompareTo,
"u16": ReturnInputCompareTo,
"u32": ReturnInputCompareTo,
"string": ReturnInputCompareTo,
"bitflags": BitflagsCompareTo, "bitflags": BitflagsCompareTo,
"switch": ReturnInputCompareTo, "switch": ReturnInputCompareTo,
"varlong": ReturnInputCompareTo, "varlong": ReturnInputCompareTo,
+2
View File
@@ -634,6 +634,8 @@ func (g *Generator) RegisterDecoderNatives() {
"varlong": VarLongDecoder, "varlong": VarLongDecoder,
"anonymousNbt": DefaultDecoder, "anonymousNbt": DefaultDecoder,
"anonOptionalNbt": DefaultDecoder, // I have no idea what the difference is between these two "anonOptionalNbt": DefaultDecoder, // I have no idea what the difference is between these two
"optionalNbt": DefaultDecoder,
"nbt": DefaultDecoder,
"lpVec3": DefaultDecoder, "lpVec3": DefaultDecoder,
"UUID": UUIDDecoder, "UUID": UUIDDecoder,
"restBuffer": DefaultDecoder, "restBuffer": DefaultDecoder,
+6 -3
View File
@@ -11,15 +11,16 @@ import (
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
func DefaultEncoder(_ *Generator, varToSet ast.Expr, _ any, _ string) (s []ast.Stmt, err error) { func DefaultEncoder(_ *Generator, varToSet ast.Expr, _ any, name string) (s []ast.Stmt, err error) {
tmpName := name + "EncTmp"
s = Stmts( s = Stmts(
Define121(Ident(tmpName), varToSet),
Assign121( Assign121(
Ident("err"), Ident("err"),
Call(SelectorExprAndStr(varToSet, "Encode"), Ident("w")), Call(SelectorExprAndStr(Ident(tmpName), "Encode"), Ident("w")),
), ),
IfErrNil(), IfErrNil(),
) )
return return
} }
@@ -402,6 +403,8 @@ func (g *Generator) RegisterEncoderNatives() {
"varlong": VarLongEncoder, "varlong": VarLongEncoder,
"anonymousNbt": DefaultEncoder, "anonymousNbt": DefaultEncoder,
"anonOptionalNbt": DefaultEncoder, // I have no idea what the difference is between these two "anonOptionalNbt": DefaultEncoder, // I have no idea what the difference is between these two
"optionalNbt": DefaultEncoder,
"nbt": DefaultEncoder,
"UUID": UUIDEncoder, "UUID": UUIDEncoder,
"restBuffer": DefaultEncoder, "restBuffer": DefaultEncoder,
"lpVec3": DefaultEncoder, "lpVec3": DefaultEncoder,
+2
View File
@@ -200,6 +200,8 @@ func (g *Generator) RegisterNatives() {
"native": VisitDontGenerateType, "native": VisitDontGenerateType,
"anonymousNbt": MakeSelectorVisitor("nbt", "Anon"), "anonymousNbt": MakeSelectorVisitor("nbt", "Anon"),
"anonOptionalNbt": MakeSelectorVisitor("nbt", "Anon"), // I have no idea what the difference is between these two "anonOptionalNbt": MakeSelectorVisitor("nbt", "Anon"), // I have no idea what the difference is between these two
"optionalNbt": MakeSelectorVisitor("nbt", "Tag"),
"nbt": MakeSelectorVisitor("nbt", "Tag"),
"void": VoidType, "void": VoidType,
"bool": MakeIdentVisitor("bool"), "bool": MakeIdentVisitor("bool"),
"u8": MakeIdentVisitor("uint8"), "u8": MakeIdentVisitor("uint8"),
+10 -7
View File
@@ -14,6 +14,7 @@ type ConfigServer struct {
registryData map[string]any registryData map[string]any
} }
// ServeConfig runs the configuration phase: sends known packs, registry data, and tags, then transitions to play.
func ServeConfig(c *proto.Conn) (err error) { func ServeConfig(c *proto.Conn) (err error) {
cs := &ConfigServer{Conn: c} cs := &ConfigServer{Conn: c}
n, err := data.LoadRegistry(c.Version) n, err := data.LoadRegistry(c.Version)
@@ -26,13 +27,15 @@ func ServeConfig(c *proto.Conn) (err error) {
} }
cs.registryData = n.Value.(map[string]any) cs.registryData = n.Value.(map[string]any)
packet := &v1_21_8.ConfigurationToClientPacketCommonSelectKnownPacks{} packet := &v1_21_8.ConfigurationToClientPacketCommonSelectKnownPacks{}
for _, pAny := range cs.registryData["KnownPacks"].([]any) { if knownPacks, ok := cs.registryData["KnownPacks"]; ok {
p := pAny.(map[string]any) for _, pAny := range knownPacks.([]any) {
packet.Packs = append(packet.Packs, struct { p := pAny.(map[string]any)
Namespace string packet.Packs = append(packet.Packs, struct {
Id string Namespace string
Version string Id string
}{p["Namespace"].(string), p["Id"].(string), p["Version"].(string)}) Version string
}{p["Namespace"].(string), p["Id"].(string), p["Version"].(string)})
}
} }
err = c.Send(packet) err = c.Send(packet)
if err != nil { if err != nil {
+1
View File
@@ -17,6 +17,7 @@ var (
CantKickInStateErr = errors.New("cant kick in this state") CantKickInStateErr = errors.New("cant kick in this state")
) )
// Kick sends a disconnect/kick packet appropriate for the connection's current state.
func Kick(c *proto.Conn, reason *text.Component, status string) (err error) { func Kick(c *proto.Conn, reason *text.Component, status string) (err error) {
if c.Actor != proto_base.Servee { if c.Actor != proto_base.Servee {
err = NotClientErr err = NotClientErr
+8
View File
@@ -204,6 +204,7 @@ func (l *LoginServer) FinishLogin() (err error) {
type LoginServerSetting func(*LoginServer) type LoginServerSetting func(*LoginServer)
// WithOtherAccount returns a setting that overrides the login identity with the given account.
func WithOtherAccount(a *api2.Account) LoginServerSetting { func WithOtherAccount(a *api2.Account) LoginServerSetting {
return func(s *LoginServer) { return func(s *LoginServer) {
s.Given = &NameAndUUID{ s.Given = &NameAndUUID{
@@ -213,12 +214,14 @@ func WithOtherAccount(a *api2.Account) LoginServerSetting {
} }
} }
// WithRawStatus returns a setting that sets the raw JSON status response payload.
func WithRawStatus(status []byte) LoginServerSetting { func WithRawStatus(status []byte) LoginServerSetting {
return func(s *LoginServer) { return func(s *LoginServer) {
s.Status = status s.Status = status
} }
} }
// WithStatus marshals a StatusResponse to JSON and passes it to WithRawStatus.
func WithStatus(response StatusResponse) LoginServerSetting { func WithStatus(response StatusResponse) LoginServerSetting {
var status []byte var status []byte
status, err := json.Marshal(response) status, err := json.Marshal(response)
@@ -229,6 +232,7 @@ func WithStatus(response StatusResponse) LoginServerSetting {
return WithRawStatus(status) return WithRawStatus(status)
} }
// WithCompatibleVersionsRange returns a setting that restricts the server to protocol versions in [start, end].
func WithCompatibleVersionsRange(start, end int32) LoginServerSetting { func WithCompatibleVersionsRange(start, end int32) LoginServerSetting {
var versions []int32 var versions []int32
for i := start; i <= end; i++ { for i := start; i <= end; i++ {
@@ -237,24 +241,28 @@ func WithCompatibleVersionsRange(start, end int32) LoginServerSetting {
return WithCompatibleVersions(versions...) return WithCompatibleVersions(versions...)
} }
// WithCompatibleVersions returns a setting that restricts the server to the given protocol versions.
func WithCompatibleVersions(versions ...int32) LoginServerSetting { func WithCompatibleVersions(versions ...int32) LoginServerSetting {
return func(s *LoginServer) { return func(s *LoginServer) {
s.CompatibleVersions = versions s.CompatibleVersions = versions
} }
} }
// WithoutOnlineMode returns a setting that disables online-mode authentication.
func WithoutOnlineMode() LoginServerSetting { func WithoutOnlineMode() LoginServerSetting {
return func(s *LoginServer) { return func(s *LoginServer) {
s.OnlineMode = false s.OnlineMode = false
} }
} }
// WithoutEncryption returns a setting that disables connection encryption.
func WithoutEncryption() LoginServerSetting { func WithoutEncryption() LoginServerSetting {
return func(s *LoginServer) { return func(s *LoginServer) {
s.Encryption = false s.Encryption = false
} }
} }
// ServeLogin runs the full login sequence (handshake, encryption, auth, and configuration handoff) on a connection.
func ServeLogin(c *proto.Conn, settings ...LoginServerSetting) (ret *LoginServer, err error) { func ServeLogin(c *proto.Conn, settings ...LoginServerSetting) (ret *LoginServer, err error) {
ret = &LoginServer{Conn: c} ret = &LoginServer{Conn: c}
ret.OnlineMode = true ret.OnlineMode = true
+5
View File
@@ -9,6 +9,7 @@ import (
"github.com/admin-else/strom/mc/text" "github.com/admin-else/strom/mc/text"
) )
// Servee wraps a net.Conn in a new proto.Conn configured for the servee side.
func Servee(c net.Conn) (ret *proto.Conn) { func Servee(c net.Conn) (ret *proto.Conn) {
ret = proto.NewConn() ret = proto.NewConn()
ret.Actor = proto_base.Servee ret.Actor = proto_base.Servee
@@ -24,6 +25,7 @@ type ConnAcceptor func(c net.Conn) (err error)
type Factory func(c *proto.Conn) (h ConnAcceptor, err error) type Factory func(c *proto.Conn) (h ConnAcceptor, err error)
// ServeClient accepts a single client connection, creates a handler via the factory, and runs it.
func ServeClient(cNet net.Conn, factory Factory) { func ServeClient(cNet net.Conn, factory Factory) {
c := Servee(cNet) c := Servee(cNet)
defer c.Close() defer c.Close()
@@ -40,6 +42,7 @@ func ServeClient(cNet net.Conn, factory Factory) {
} }
} }
// StartServerWithFactory listens on the given address and spawns ServeClient for every accepted connection.
func StartServerWithFactory(listenAddr string, factory Factory) (err error) { func StartServerWithFactory(listenAddr string, factory Factory) (err error) {
l, err := net.Listen("tcp", listenAddr) l, err := net.Listen("tcp", listenAddr)
if err != nil { if err != nil {
@@ -57,6 +60,8 @@ func StartServerWithFactory(listenAddr string, factory Factory) (err error) {
var ShutDownServerErr = errors.New("server shutting down") var ShutDownServerErr = errors.New("server shutting down")
// StartServerWithOnConn listens on the given address and calls onConn for every accepted connection.
// The server loop stops when onConn returns ShutDownServerErr.
func StartServerWithOnConn(listenAddr string, onConn func(c *proto.Conn) (err error)) (err error) { func StartServerWithOnConn(listenAddr string, onConn func(c *proto.Conn) (err error)) (err error) {
l, err := net.Listen("tcp", listenAddr) l, err := net.Listen("tcp", listenAddr)
if err != nil { if err != nil {
+1
View File
@@ -56,6 +56,7 @@ func (p *StatusServer) OnStatusPing(packet *v1_21_8.StatusToServerPacketPing) (e
return p.Send(&v1_21_8.StatusToClientPacketPing{Time: packet.Time}) return p.Send(&v1_21_8.StatusToClientPacketPing{Time: packet.Time})
} }
// ServeStatus runs the server list ping sequence, responding with the given StatusResponse.
func ServeStatus(c *proto.Conn, s StatusResponse) (err error) { func ServeStatus(c *proto.Conn, s StatusResponse) (err error) {
status, err := json.Marshal(s) status, err := json.Marshal(s)
if err != nil { if err != nil {
+1
View File
@@ -526,6 +526,7 @@ func replacePlaceholders(s string, values map[string]any) string {
return result return result
} }
// PrettyPlaceholders parses a legacy formatted string with named placeholders replaced from the given map, returning a Component.
func PrettyPlaceholders(format string, placeholders map[string]any) *Component { func PrettyPlaceholders(format string, placeholders map[string]any) *Component {
s := replacePlaceholders(format, placeholders) s := replacePlaceholders(format, placeholders)
return Pretty(s) return Pretty(s)
+3
View File
@@ -7,6 +7,7 @@ import (
var boundaries = []rune{' ', '_', '-', '.', ',', ';', ':', '(', ')', '[', ']', '{', '}'} var boundaries = []rune{' ', '_', '-', '.', ',', ';', ':', '(', ')', '[', ']', '{', '}'}
// CamelCase converts a string with word boundaries to CamelCase.
func CamelCase(s string) string { func CamelCase(s string) string {
ret := "" ret := ""
atWordBoundary := true atWordBoundary := true
@@ -24,6 +25,7 @@ func CamelCase(s string) string {
return ret return ret
} }
// SnakeCase converts a CamelCase string to snake_case.
func SnakeCase(s string) string { func SnakeCase(s string) string {
ret := "" ret := ""
for i, c := range s { for i, c := range s {
@@ -38,6 +40,7 @@ func SnakeCase(s string) string {
return ret return ret
} }
// FirstLetterLower returns the string with the first rune lowercased.
func FirstLetterLower(s string) string { func FirstLetterLower(s string) string {
return string(unicode.ToLower(rune(s[0]))) + s[1:] return string(unicode.ToLower(rune(s[0]))) + s[1:]
} }
+2
View File
@@ -2,10 +2,12 @@ package util
import "math" import "math"
// FloorDiv returns floor division (rounds toward negative infinity).
func FloorDiv(a, b int32) int32 { func FloorDiv(a, b int32) int32 {
return (a - ((a%b + b) % b)) / b return (a - ((a%b + b) % b)) / b
} }
// BpeByNum returns the minimum bits per element needed to represent x distinct values.
func BpeByNum(x float64) uint8 { func BpeByNum(x float64) uint8 {
return uint8(math.Ceil(math.Log2(x))) return uint8(math.Ceil(math.Log2(x)))
} }
+5
View File
@@ -2,6 +2,7 @@ package util
import "fmt" import "fmt"
// MustOk returns v if ok is true, otherwise panics.
func MustOk[T any](v T, ok bool) T { func MustOk[T any](v T, ok bool) T {
if !ok { if !ok {
panic("not ok") panic("not ok")
@@ -9,6 +10,7 @@ func MustOk[T any](v T, ok bool) T {
return v return v
} }
// MustT returns v if err is nil, otherwise panics.
func MustT[T any](v T, err error) T { func MustT[T any](v T, err error) T {
if err != nil { if err != nil {
panic(err) panic(err)
@@ -16,12 +18,14 @@ func MustT[T any](v T, err error) T {
return v return v
} }
// Must panics if err is non-nil.
func Must(err error) { func Must(err error) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
// AssertTypeError asserts that v is of type T and returns it, or returns an error.
func AssertTypeError[T any](v any) (T, error) { func AssertTypeError[T any](v any) (T, error) {
var zero T var zero T
r, ok := v.(T) r, ok := v.(T)
@@ -31,6 +35,7 @@ func AssertTypeError[T any](v any) (T, error) {
return r, nil return r, nil
} }
// MapGetError returns the value for key k from map m, or an error if not present.
func MapGetError[K comparable, V any, M map[K]V](m M, k K) (V, error) { func MapGetError[K comparable, V any, M map[K]V](m M, k K) (V, error) {
v, ok := m[k] v, ok := m[k]
if !ok { if !ok {
+5
View File
@@ -6,6 +6,7 @@ import (
"slices" "slices"
) )
// OrderedKeys returns the keys of the map sorted in ascending order.
func OrderedKeys[Map map[K]V, K cmp.Ordered, V any](m Map) []K { func OrderedKeys[Map map[K]V, K cmp.Ordered, V any](m Map) []K {
keysIter := maps.Keys(m) keysIter := maps.Keys(m)
keys := slices.Collect(keysIter) keys := slices.Collect(keysIter)
@@ -13,6 +14,7 @@ func OrderedKeys[Map map[K]V, K cmp.Ordered, V any](m Map) []K {
return keys return keys
} }
// AssertAndConvertMapValues asserts and converts all values in a map[string]any to type T.
func AssertAndConvertMapValues[T any, M map[K]any, K cmp.Ordered](m M) map[K]T { func AssertAndConvertMapValues[T any, M map[K]any, K cmp.Ordered](m M) map[K]T {
ret := map[K]T{} ret := map[K]T{}
for k, v := range m { for k, v := range m {
@@ -21,6 +23,7 @@ func AssertAndConvertMapValues[T any, M map[K]any, K cmp.Ordered](m M) map[K]T {
return ret return ret
} }
// ReverseMap returns a new map with keys and values swapped.
func ReverseMap[M map[K]V, K cmp.Ordered, V cmp.Ordered](m M) map[V]K { func ReverseMap[M map[K]V, K cmp.Ordered, V cmp.Ordered](m M) map[V]K {
ret := map[V]K{} ret := map[V]K{}
for k, v := range m { for k, v := range m {
@@ -29,6 +32,7 @@ func ReverseMap[M map[K]V, K cmp.Ordered, V cmp.Ordered](m M) map[V]K {
return ret return ret
} }
// PopFront removes and returns the first element of the slice.
func PopFront[T any](s []T) (T, []T, bool) { func PopFront[T any](s []T) (T, []T, bool) {
if len(s) == 0 { if len(s) == 0 {
var zero T var zero T
@@ -37,6 +41,7 @@ func PopFront[T any](s []T) (T, []T, bool) {
return s[0], s[1:], true return s[0], s[1:], true
} }
// CombineNamAndData returns a slice containing name and data.
func CombineNamAndData(name string, data any) []any { func CombineNamAndData(name string, data any) []any {
return []any{name, data} return []any{name, data}
} }