From f97538fe1905d0af269a90baaece4ed6ee95bcff Mon Sep 17 00:00:00 2001 From: adman Date: Tue, 13 Jan 2026 00:22:11 +0100 Subject: [PATCH] le funny --- api/account.go | 6 +- client/dns.go | 2 + client/modules/login.go | 24 +- event/event_bus.go | 1 + proto/connection.go | 2 + text/text.go | 537 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 566 insertions(+), 6 deletions(-) diff --git a/api/account.go b/api/account.go index b48b50a..0962c79 100644 --- a/api/account.go +++ b/api/account.go @@ -78,8 +78,7 @@ func (a *Account) FetchKeys() (keys PlayerKeys, err error) { } func (a *Account) JoinServer(serverId string) (err error) { - var body []byte - body, err = json.Marshal(struct { + body, err := json.Marshal(struct { AccessToken string `json:"accessToken"` SelectedProfile string `json:"selectedProfile"` ServerId string `json:"serverId"` @@ -91,8 +90,7 @@ func (a *Account) JoinServer(serverId string) (err error) { if err != nil { return } - var resp *http.Response - resp, err = http.Post("https://sessionserver.mojang.com/session/minecraft/join", "application/json", bytes.NewReader(body)) + resp, err := http.Post("https://sessionserver.mojang.com/session/minecraft/join", "application/json", bytes.NewReader(body)) if err != nil { return } diff --git a/client/dns.go b/client/dns.go index 338a7f1..f1ff47a 100644 --- a/client/dns.go +++ b/client/dns.go @@ -6,6 +6,8 @@ import ( ) func LookupMinecraftSRV(host string, port uint16) (hostRet string, portRet uint16) { + hostRet = host // in case of error + portRet = port if port != 25565 { return } diff --git a/client/modules/login.go b/client/modules/login.go index 85bbfa0..0a2fead 100644 --- a/client/modules/login.go +++ b/client/modules/login.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/json" "errors" "fmt" "net" @@ -15,6 +16,7 @@ import ( "github.com/admin-else/strom/event" "github.com/admin-else/strom/proto" "github.com/admin-else/strom/proto_base" + "github.com/admin-else/strom/text" "github.com/admin-else/strom/proto_generated/v1_21_8" ) @@ -28,6 +30,14 @@ type LoginClient struct { GivenAccount *v1_21_8.LoginToClientPacketSuccess } +type KickedDuringLoginErr struct { + text.Component +} + +func (k KickedDuringLoginErr) Error() string { + return k.String() +} + func (s *LoginClient) Default(event event.Unhandled) (err error) { err = fmt.Errorf("unexpected event: %#v", event) return @@ -67,11 +77,11 @@ func (s *LoginClient) OnStart(_ event.OnStart) (err error) { } func (s *LoginClient) OnCompress(compress *v1_21_8.LoginToClientPacketCompress) (err error) { - s.CompressionThreshold = int32(compress.Threshold) + s.CompressionThreshold = compress.Threshold return } -func (s *LoginClient) OnEncrypt(packet v1_21_8.LoginToClientPacketEncryptionBegin) (err error) { +func (s *LoginClient) OnEncrypt(packet *v1_21_8.LoginToClientPacketEncryptionBegin) (err error) { sharedSecret := make([]byte, 16) _, _ = rand.Read(sharedSecret) //never fails @@ -114,6 +124,16 @@ func (s *LoginClient) OnEncrypt(packet v1_21_8.LoginToClientPacketEncryptionBegi } +func (s *LoginClient) OnDisconnect(packet *v1_21_8.LoginToClientPacketDisconnect) (err error) { + var reason text.Component + err = json.Unmarshal([]byte(packet.Reason), &reason) + if err != nil { + return + } + err = KickedDuringLoginErr{reason} + return +} + func (s *LoginClient) OnSuccess(success *v1_21_8.LoginToClientPacketSuccess) (err error) { s.GivenAccount = success err = s.Send(&v1_21_8.LoginToServerPacketLoginAcknowledged{}) diff --git a/event/event_bus.go b/event/event_bus.go index af43ee7..881f00d 100644 --- a/event/event_bus.go +++ b/event/event_bus.go @@ -51,6 +51,7 @@ func (l *Loop) FindHandlers() { } eventType := method.Type.In(1) l.HandlerFunctions[eventType] = append(l.HandlerFunctions[eventType], v.Method(i)) + //fmt.Println("Registered Handler:", t.String(), ".", method.Name, "(", eventType.String(), ")") } } } diff --git a/proto/connection.go b/proto/connection.go index 80b0998..647c7a7 100644 --- a/proto/connection.go +++ b/proto/connection.go @@ -65,6 +65,7 @@ func (c *Conn) SetVersion(version string) (err error) { } func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) { + //fmt.Println("send:", packet) switch packet := packet.(type) { case *UnCodablePacket: err = c.SendRaw(packet.Data) @@ -112,6 +113,7 @@ func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) { packet = &UnCodablePacket{Err: PacketNotFullyDecodedError, Data: packetBytes, Direction: c.Actor.ReceiveDirection()} return } + //fmt.Println("recv:", packet) return } diff --git a/text/text.go b/text/text.go index 915d733..f2bc4b4 100644 --- a/text/text.go +++ b/text/text.go @@ -1,3 +1,540 @@ package text +import ( + "encoding/json" + "fmt" + "strings" +) + +// Component represents a Minecraft text component. // https://minecraft.wiki/w/Text_component_format +type Component struct { + // Common fields + Bold *bool `json:"bold,omitempty" nbt:"bold,omitempty"` + Italic *bool `json:"italic,omitempty" nbt:"italic,omitempty"` + Underlined *bool `json:"underlined,omitempty" nbt:"underlined,omitempty"` + Strikethrough *bool `json:"strikethrough,omitempty" nbt:"strikethrough,omitempty"` + Obfuscated *bool `json:"obfuscated,omitempty" nbt:"obfuscated,omitempty"` + Font string `json:"font,omitempty" nbt:"font,omitempty"` + Color string `json:"color,omitempty" nbt:"color,omitempty"` + Insertion string `json:"insertion,omitempty" nbt:"insertion,omitempty"` + ClickEvent *ClickEvent `json:"clickEvent,omitempty" nbt:"clickEvent,omitempty"` + HoverEvent *HoverEvent `json:"hoverEvent,omitempty" nbt:"hoverEvent,omitempty"` + Extra []Component `json:"extra,omitempty" nbt:"extra,omitempty"` + + // Content fields (one of these should be set) + Text string `json:"text,omitempty" nbt:"text,omitempty"` + Translate string `json:"translate,omitempty" nbt:"translate,omitempty"` + With []Component `json:"with,omitempty" nbt:"with,omitempty"` + Score *Score `json:"score,omitempty" nbt:"score,omitempty"` + Selector string `json:"selector,omitempty" nbt:"selector,omitempty"` + Keybind string `json:"keybind,omitempty" nbt:"keybind,omitempty"` + NBT string `json:"nbt,omitempty" nbt:"nbt,omitempty"` + Interpret bool `json:"interpret,omitempty" nbt:"interpret,omitempty"` + Block string `json:"block,omitempty" nbt:"block,omitempty"` + Entity string `json:"entity,omitempty" nbt:"entity,omitempty"` + Storage string `json:"storage,omitempty" nbt:"storage,omitempty"` + Separator *Component `json:"separator,omitempty" nbt:"separator,omitempty"` +} + +type ClickEvent struct { + Action string `json:"action" nbt:"action"` + Value string `json:"value" nbt:"value"` +} + +type HoverEvent struct { + Action string `json:"action" nbt:"action"` + Contents any `json:"contents,omitempty" nbt:"contents,omitempty"` + Value any `json:"value,omitempty" nbt:"value,omitempty"` // Legacy +} + +type Score struct { + Name string `json:"name" nbt:"name"` + Objective string `json:"objective" nbt:"objective"` + Value string `json:"value,omitempty" nbt:"value,omitempty"` +} + +// MarshalJSON implements json.Marshaler. +func (c *Component) MarshalJSON() ([]byte, error) { + // If it's just a simple text component with no formatting, we can marshal it as a string + if c.isSimpleText() { + return json.Marshal(c.Text) + } + + type Alias Component + return json.Marshal(Alias(*c)) +} + +func (c *Component) isSimpleText() bool { + return c.Text != "" && + c.Bold == nil && c.Italic == nil && c.Underlined == nil && c.Strikethrough == nil && c.Obfuscated == nil && + c.Font == "" && c.Color == "" && c.Insertion == "" && c.ClickEvent == nil && c.HoverEvent == nil && + len(c.Extra) == 0 && c.Translate == "" && c.Score == nil && c.Selector == "" && c.Keybind == "" && + c.NBT == "" && !c.Interpret && c.Block == "" && c.Entity == "" && c.Storage == "" && c.Separator == nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (c *Component) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + + // Try string + var s string + if err := json.Unmarshal(data, &s); err == nil { + c.Text = s + return nil + } + + // Try array + var arr []Component + if err := json.Unmarshal(data, &arr); err == nil { + if len(arr) > 0 { + *c = arr[0] + if len(arr) > 1 { + c.Extra = append(c.Extra, arr[1:]...) + } + } + return nil + } + + // Try object + type Alias Component + var aux Alias + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + *c = Component(aux) + return nil +} + +// ToNBT converts the component to a format suitable for the nbt package. +func (c *Component) ToNBT() any { + if c.isSimpleText() { + return c.Text + } + + m := make(map[string]any) + if c.Bold != nil { + m["bold"] = boolToByte(*c.Bold) + } + if c.Italic != nil { + m["italic"] = boolToByte(*c.Italic) + } + if c.Underlined != nil { + m["underlined"] = boolToByte(*c.Underlined) + } + if c.Strikethrough != nil { + m["strikethrough"] = boolToByte(*c.Strikethrough) + } + if c.Obfuscated != nil { + m["obfuscated"] = boolToByte(*c.Obfuscated) + } + if c.Font != "" { + m["font"] = c.Font + } + if c.Color != "" { + m["color"] = c.Color + } + if c.Insertion != "" { + m["insertion"] = c.Insertion + } + if c.ClickEvent != nil { + m["clickEvent"] = map[string]any{ + "action": c.ClickEvent.Action, + "value": c.ClickEvent.Value, + } + } + if c.HoverEvent != nil { + he := map[string]any{ + "action": c.HoverEvent.Action, + } + if c.HoverEvent.Contents != nil { + he["contents"] = c.HoverEvent.Contents + } + if c.HoverEvent.Value != nil { + he["value"] = c.HoverEvent.Value + } + m["hoverEvent"] = he + } + if len(c.Extra) > 0 { + extra := make([]any, len(c.Extra)) + for i, e := range c.Extra { + extra[i] = e.ToNBT() + } + m["extra"] = extra + } + + if c.Text != "" { + m["text"] = c.Text + } + if c.Translate != "" { + m["translate"] = c.Translate + } + if len(c.With) > 0 { + with := make([]any, len(c.With)) + for i, e := range c.With { + with[i] = e.ToNBT() + } + m["with"] = with + } + if c.Score != nil { + s := map[string]any{ + "name": c.Score.Name, + "objective": c.Score.Objective, + } + if c.Score.Value != "" { + s["value"] = c.Score.Value + } + m["score"] = s + } + if c.Selector != "" { + m["selector"] = c.Selector + } + if c.Keybind != "" { + m["keybind"] = c.Keybind + } + if c.NBT != "" { + m["nbt"] = c.NBT + } + if c.Interpret { + m["interpret"] = boolToByte(c.Interpret) + } + if c.Block != "" { + m["block"] = c.Block + } + if c.Entity != "" { + m["entity"] = c.Entity + } + if c.Storage != "" { + m["storage"] = c.Storage + } + if c.Separator != nil { + m["separator"] = c.Separator.ToNBT() + } + + return m +} + +func boolToByte(b bool) int8 { + if b { + return 1 + } + return 0 +} + +// FromNBT populates the component from an NBT-like structure (map[string]any or string). +func (c *Component) FromNBT(v any) error { + switch v := v.(type) { + case string: + c.Text = v + return nil + case map[string]any: + if val, ok := v["bold"]; ok { + c.Bold = ptr(byteToBool(val)) + } + if val, ok := v["italic"]; ok { + c.Italic = ptr(byteToBool(val)) + } + if val, ok := v["underlined"]; ok { + c.Underlined = ptr(byteToBool(val)) + } + if val, ok := v["strikethrough"]; ok { + c.Strikethrough = ptr(byteToBool(val)) + } + if val, ok := v["obfuscated"]; ok { + c.Obfuscated = ptr(byteToBool(val)) + } + if val, ok := v["font"].(string); ok { + c.Font = val + } + if val, ok := v["color"].(string); ok { + c.Color = val + } + if val, ok := v["insertion"].(string); ok { + c.Insertion = val + } + if val, ok := v["clickEvent"].(map[string]any); ok { + c.ClickEvent = &ClickEvent{} + if a, ok := val["action"].(string); ok { + c.ClickEvent.Action = a + } + if v, ok := val["value"].(string); ok { + c.ClickEvent.Value = v + } + } + if val, ok := v["hoverEvent"].(map[string]any); ok { + c.HoverEvent = &HoverEvent{} + if a, ok := val["action"].(string); ok { + c.HoverEvent.Action = a + } + if co, ok := val["contents"]; ok { + c.HoverEvent.Contents = co + } + if v, ok := val["value"]; ok { + c.HoverEvent.Value = v + } + } + if val, ok := v["extra"].([]any); ok { + c.Extra = make([]Component, len(val)) + for i, e := range val { + if err := c.Extra[i].FromNBT(e); err != nil { + return err + } + } + } + if val, ok := v["text"].(string); ok { + c.Text = val + } + if val, ok := v["translate"].(string); ok { + c.Translate = val + } + if val, ok := v["with"].([]any); ok { + c.With = make([]Component, len(val)) + for i, e := range val { + if err := c.With[i].FromNBT(e); err != nil { + return err + } + } + } + if val, ok := v["score"].(map[string]any); ok { + c.Score = &Score{} + if n, ok := val["name"].(string); ok { + c.Score.Name = n + } + if o, ok := val["objective"].(string); ok { + c.Score.Objective = o + } + if v, ok := val["value"].(string); ok { + c.Score.Value = v + } + } + if val, ok := v["selector"].(string); ok { + c.Selector = val + } + if val, ok := v["keybind"].(string); ok { + c.Keybind = val + } + if val, ok := v["nbt"].(string); ok { + c.NBT = val + } + if val, ok := v["interpret"]; ok { + c.Interpret = byteToBool(val) + } + if val, ok := v["block"].(string); ok { + c.Block = val + } + if val, ok := v["entity"].(string); ok { + c.Entity = val + } + if val, ok := v["storage"].(string); ok { + c.Storage = val + } + if val, ok := v["separator"]; ok { + c.Separator = &Component{} + if err := c.Separator.FromNBT(val); err != nil { + return err + } + } + return nil + default: + return fmt.Errorf("unsupported NBT type for component: %T", v) + } +} + +func byteToBool(v any) bool { + switch v := v.(type) { + case int8: + return v != 0 + case int32: + return v != 0 + case bool: + return v + default: + return false + } +} + +func ptr[T any](v T) *T { + return &v +} + +// Pretty parses a legacy formatted string into a Component. +// It uses the § symbol for color and formatting codes. +func Pretty(s string) *Component { + parts := strings.Split(s, "§") + + root := Component{} + if parts[0] != "" { + root.Text = parts[0] + } + + var current = &root + for i := 1; i < len(parts); i++ { + part := parts[i] + if part == "" { + continue + } + + code := part[0] + text := part[1:] + + newComp := Component{} + // Inherit formatting from current if it's just a style change + if current != &root { + newComp.Bold = current.Bold + newComp.Italic = current.Italic + newComp.Underlined = current.Underlined + newComp.Strikethrough = current.Strikethrough + newComp.Obfuscated = current.Obfuscated + newComp.Color = current.Color + } + + switch code { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f': + newComp.Color = codeToColor[code] + // Colors reset formatting + newComp.Bold = nil + newComp.Italic = nil + newComp.Underlined = nil + newComp.Strikethrough = nil + newComp.Obfuscated = nil + case 'k': + newComp.Obfuscated = ptr(true) + case 'l': + newComp.Bold = ptr(true) + case 'm': + newComp.Strikethrough = ptr(true) + case 'n': + newComp.Underlined = ptr(true) + case 'o': + newComp.Italic = ptr(true) + case 'r': + newComp = Component{} // Reset + case 'x': + // Hex color §x§r§r§g§g§b§b + if i+6 < len(parts) { + hex := "#" + for j := 1; j <= 6; j++ { + if len(parts[i+j]) > 0 { + hex += string(parts[i+j][0]) + } + } + newComp.Color = hex + i += 6 + // Hex colors also reset formatting + newComp.Bold = nil + newComp.Italic = nil + newComp.Underlined = nil + newComp.Strikethrough = nil + newComp.Obfuscated = nil + } + } + + if text != "" { + newComp.Text = text + root.Extra = append(root.Extra, newComp) + current = &root.Extra[len(root.Extra)-1] + } else { + // Just a formatting code, update current state for next parts + current = &newComp + } + } + + return &root +} + +// PrettyF returns a Component from a formatted string. +func PrettyF(format string, args ...any) *Component { + return Pretty(fmt.Sprintf(format, args...)) +} + +var colorToCode = map[string]byte{ + "black": '0', + "dark_blue": '1', + "dark_green": '2', + "dark_aqua": '3', + "dark_red": '4', + "dark_purple": '5', + "gold": '6', + "gray": '7', + "dark_gray": '8', + "blue": '9', + "green": 'a', + "aqua": 'b', + "red": 'c', + "light_purple": 'd', + "yellow": 'e', + "white": 'f', +} + +var codeToColor = map[byte]string{ + '0': "black", + '1': "dark_blue", + '2': "dark_green", + '3': "dark_aqua", + '4': "dark_red", + '5': "dark_purple", + '6': "gold", + '7': "gray", + '8': "dark_gray", + '9': "blue", + 'a': "green", + 'b': "aqua", + 'c': "red", + 'd': "light_purple", + 'e': "yellow", + 'f': "white", +} + +func (c *Component) String() string { + var sb strings.Builder + c.writePretty(&sb) + return sb.String() +} + +func (c *Component) writePretty(sb *strings.Builder) { + if c.Color != "" { + if code, ok := colorToCode[strings.ToLower(c.Color)]; ok { + sb.WriteString("§") + sb.WriteByte(code) + } else if strings.HasPrefix(c.Color, "#") && len(c.Color) == 7 { + // Hex color: §x§r§r§g§g§b§b + sb.WriteString("§x") + for _, char := range c.Color[1:] { + sb.WriteString("§") + sb.WriteRune(char) + } + } + } + + if c.Bold != nil && *c.Bold { + sb.WriteString("§l") + } + if c.Italic != nil && *c.Italic { + sb.WriteString("§o") + } + if c.Underlined != nil && *c.Underlined { + sb.WriteString("§n") + } + if c.Strikethrough != nil && *c.Strikethrough { + sb.WriteString("§m") + } + if c.Obfuscated != nil && *c.Obfuscated { + sb.WriteString("§k") + } + + if c.Text != "" { + sb.WriteString(c.Text) + } + + if c.Translate != "" { + // This is a simplified version of translation handling + // In a real client, this would look up the translation key + sb.WriteString(c.Translate) + } + + for _, extra := range c.Extra { + extra.writePretty(sb) + } +}