1.19.2 chat support

This commit is contained in:
Tnze
2022-11-30 18:50:10 +08:00
parent 0b7ef620d9
commit 55d79f791a
13 changed files with 431 additions and 244 deletions

View File

@ -3,16 +3,15 @@ package basic
import (
"unsafe"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/registry"
"github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/nbt"
pk "github.com/Tnze/go-mc/net/packet"
)
// WorldInfo content player info in server.
type WorldInfo struct {
RegistryCodec RegistryCodec
RegistryCodec registry.NetworkCodec
DimensionType string
DimensionNames []string // Identifiers for all worlds on the server.
DimensionName string // Name of the world being spawned into.
@ -26,70 +25,6 @@ type WorldInfo struct {
IsFlat bool // True if the world is a superflat world; flat worlds have different void fog and a horizon at y=0 instead of y=63.
}
type Dimension struct {
FixedTime int64 `nbt:"fixed_time,omitempty"`
HasSkylight bool `nbt:"has_skylight"`
HasCeiling bool `nbt:"has_ceiling"`
Ultrawarm bool `nbt:"ultrawarm"`
Natural bool `nbt:"natural"`
CoordinateScale float64 `nbt:"coordinate_scale"`
BedWorks bool `nbt:"bed_works"`
RespawnAnchorWorks byte `nbt:"respawn_anchor_works"`
MinY int32 `nbt:"min_y"`
Height int32 `nbt:"height"`
LogicalHeight int32 `nbt:"logical_height"`
InfiniteBurn string `nbt:"infiniburn"`
Effects string `nbt:"effects"`
AmbientLight float64 `nbt:"ambient_light"`
PiglinSafe byte `nbt:"piglin_safe"`
HasRaids byte `nbt:"has_raids"`
MonsterSpawnLightLevel nbt.RawMessage `nbt:"monster_spawn_light_level"` // Tag_Int or {type:"minecraft:uniform", value:{min_inclusive: Tag_Int, max_inclusive: Tag_Int}}
MonsterSpawnBlockLightLimit int32 `nbt:"monster_spawn_block_light_limit"`
}
type ChatType struct {
Chat chat.Decoration `nbt:"chat"`
Narration chat.Decoration `nbt:"narration"`
}
type RegistryCodec struct {
// What is Below? (wiki.vg)
ChatType Registry[ChatType] `nbt:"minecraft:chat_type"`
DimensionType Registry[Dimension] `nbt:"minecraft:dimension_type"`
WorldGenBiome Registry[nbt.RawMessage] `nbt:"minecraft:worldgen/biome"`
}
type Registry[E any] struct {
Type string `nbt:"type"`
Value []struct {
Name string `nbt:"name"`
ID int32 `nbt:"id"`
Element E `nbt:"element"`
} `nbt:"value"`
}
func (r *Registry[E]) Find(name string) *E {
for i := range r.Value {
if r.Value[i].Name == name {
return &r.Value[i].Element
}
}
return nil
}
func (r *Registry[E]) FindByID(id int32) *E {
if id >= 0 && id < int32(len(r.Value)) && r.Value[id].ID == id {
return &r.Value[id].Element
}
for i := range r.Value {
if r.Value[i].ID == id {
return &r.Value[i].Element
}
}
return nil
}
type PlayerInfo struct {
EID int32 // The player's Entity ID (EID).
Hardcore bool // Is hardcore

View File

@ -1,16 +1,16 @@
package msg
import (
"encoding/hex"
"fmt"
"io"
"time"
"github.com/Tnze/go-mc/chat/sign"
"github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/bot/basic"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid"
)
type Manager struct {
@ -24,149 +24,40 @@ func New(c *bot.Client, p *basic.Player, events EventsHandler) *Manager {
}
func attachPlayerMsg(c *bot.Client, p *basic.Player, handler func(msg chat.Message) error) {
c.Events.AddListener(bot.PacketHandler{
c.Events.AddListener(
bot.PacketHandler{
Priority: 64, ID: packetid.ClientboundPlayerChatHeader,
F: func(packet pk.Packet) error {
fmt.Println(packetid.ClientboundPacketID(packet.ID))
fmt.Println(hex.Dump(packet.Data))
return nil
},
},
bot.PacketHandler{
Priority: 64, ID: packetid.ClientboundPlayerChat,
F: func(packet pk.Packet) error {
var message PlayerMessage
if err := packet.Scan(&message); err != nil {
var message sign.PlayerMessage
var chatType chat.Type
fmt.Println(packetid.ClientboundPacketID(packet.ID))
fmt.Println(hex.Dump(packet.Data))
if err := packet.Scan(&message, &chatType); err != nil {
return err
}
var content chat.Message
if message.content.formatted != nil {
content = *message.content.formatted
if message.MessageBody.Message != nil {
content = *message.MessageBody.Message
} else {
content = chat.Text(message.content.plainMsg)
content = chat.Text(message.MessageBody.PlainMessage)
}
ct := p.WorldInfo.RegistryCodec.ChatType.FindByID(message.chatType.ID)
ct := p.WorldInfo.RegistryCodec.ChatType.FindByID(chatType.ID)
if ct == nil {
return fmt.Errorf("chat type %d not found", message.chatType.ID)
return fmt.Errorf("chat type %d not found", chatType.ID)
}
msg := (*chat.Type)(&message.chatType).Decorate(content, &ct.Chat)
msg := chatType.Decorate(content, &ct.Chat)
return handler(msg)
},
})
}
type PlayerMessage struct {
// SignedMessageHeader
signature []byte
sender uuid.UUID
// MessageSignature
msgSignature []byte
// SignedMessageBody
content msgContent
timestamp time.Time
salt int64
prevMessages []prevMsg
// Optional<Component>
unsignedContent *chat.Message
// FilterMask
filterType int32
filterSet pk.BitSet
// ChatType
chatType chatType
}
func (p *PlayerMessage) String() string {
return p.content.plainMsg
}
func (p *PlayerMessage) ReadFrom(r io.Reader) (n int64, err error) {
var hasMsgSign, hasUnsignedContent pk.Boolean
var timestamp pk.Long
var unsignedContent chat.Message
n, err = pk.Tuple{
&hasMsgSign,
pk.Opt{
Has: &hasMsgSign,
Field: (*pk.ByteArray)(&p.signature),
},
(*pk.UUID)(&p.sender),
(*pk.ByteArray)(&p.msgSignature),
&p.content,
&timestamp,
(*pk.Long)(&p.salt),
pk.Array(&p.prevMessages),
&hasUnsignedContent,
pk.Opt{
Has: &hasUnsignedContent,
Field: &unsignedContent,
},
(*pk.VarInt)(&p.filterType),
pk.Opt{
Has: func() bool { return p.filterType == 2 },
Field: &p.filterSet,
},
&p.chatType,
}.ReadFrom(r)
if err != nil {
return
}
p.timestamp = time.UnixMilli(int64(timestamp))
if hasUnsignedContent {
p.unsignedContent = &unsignedContent
}
return n, err
}
type msgContent struct {
plainMsg string
formatted *chat.Message
}
func (m *msgContent) ReadFrom(r io.Reader) (n int64, err error) {
var hasFormatted pk.Boolean
n1, err := (*pk.String)(&m.plainMsg).ReadFrom(r)
if err != nil {
return n1, err
}
n2, err := hasFormatted.ReadFrom(r)
if err != nil {
return n1 + n2, err
}
if hasFormatted {
m.formatted = new(chat.Message)
n3, err := m.formatted.ReadFrom(r)
return n1 + n2 + n3, err
}
return n1 + n2, err
}
type prevMsg struct {
sender uuid.UUID
signature []byte
}
func (p *prevMsg) ReadFrom(r io.Reader) (n int64, err error) {
return pk.Tuple{
(*pk.UUID)(&p.sender),
(*pk.ByteArray)(&p.signature),
}.ReadFrom(r)
}
type chatType chat.Type
func (c *chatType) ReadFrom(r io.Reader) (n int64, err error) {
var hasTargetName pk.Boolean
n1, err := (*pk.VarInt)(&c.ID).ReadFrom(r)
if err != nil {
return n1, err
}
n2, err := c.SenderName.ReadFrom(r)
if err != nil {
return n1 + n2, err
}
n3, err := hasTargetName.ReadFrom(r)
if err != nil {
return n1 + n2 + n3, err
}
if hasTargetName {
c.TargetName = new(chat.Message)
n4, err := c.TargetName.ReadFrom(r)
return n1 + n2 + n3 + n4, err
}
return n1 + n2 + n3, nil
}

View File

@ -41,7 +41,7 @@ func (w *World) onPlayerSpawn(pk.Packet) error {
func (w *World) handleLevelChunkWithLightPacket(packet pk.Packet) error {
var pos level.ChunkPos
currentDimType := w.p.WorldInfo.RegistryCodec.DimensionType.Find(w.p.DimensionType)
_, currentDimType := w.p.WorldInfo.RegistryCodec.DimensionType.Find(w.p.DimensionType)
if currentDimType == nil {
return errors.New("dimension type " + w.p.DimensionType + " not found")
}

View File

@ -1,5 +1,11 @@
package chat
import (
"io"
pk "github.com/Tnze/go-mc/net/packet"
)
type Decoration struct {
TranslationKey string `nbt:"translation_key"`
Parameters []string `nbt:"parameters"`
@ -49,3 +55,46 @@ func (t *Type) Decorate(content Message, d *Decoration) (msg Message) {
Insertion: d.Style.Insertion,
}
}
func (t *Type) ReadFrom(r io.Reader) (n int64, err error) {
var hasTargetName pk.Boolean
n1, err := (*pk.VarInt)(&t.ID).ReadFrom(r)
if err != nil {
return n1, err
}
n2, err := t.SenderName.ReadFrom(r)
if err != nil {
return n1 + n2, err
}
n3, err := hasTargetName.ReadFrom(r)
if err != nil {
return n1 + n2 + n3, err
}
if hasTargetName {
t.TargetName = new(Message)
n4, err := t.TargetName.ReadFrom(r)
return n1 + n2 + n3 + n4, err
}
return n1 + n2 + n3, nil
}
func (t *Type) WriteTo(w io.Writer) (n int64, err error) {
hasTargetName := pk.Boolean(t.TargetName != nil)
n1, err := (*pk.VarInt)(&t.ID).WriteTo(w)
if err != nil {
return n1, err
}
n2, err := t.SenderName.WriteTo(w)
if err != nil {
return n1 + n2, err
}
n3, err := hasTargetName.WriteTo(w)
if err != nil {
return n1 + n2 + n3, err
}
if hasTargetName {
n4, err := t.TargetName.WriteTo(w)
return n1 + n2 + n3 + n4, err
}
return n1 + n2 + n3, nil
}

View File

@ -132,13 +132,13 @@ func (m *Message) UnmarshalJSON(raw []byte) (err error) {
// ReadFrom decode Message in a ChatMsg packet
func (m *Message) ReadFrom(r io.Reader) (int64, error) {
var Len pk.VarInt
if n, err := Len.ReadFrom(r); err != nil {
var code pk.String
n, err := code.ReadFrom(r)
if err != nil {
return n, err
}
lr := &io.LimitedReader{R: r, N: int64(Len)}
err := json.NewDecoder(lr).Decode(m)
return int64(Len) - lr.N, err
err = json.Unmarshal([]byte(code), m)
return n, err
}
// WriteTo encode Message into a ChatMsg packet

216
chat/sign/sign.go Normal file
View File

@ -0,0 +1,216 @@
package sign
import (
"crypto/rand"
"encoding/binary"
"io"
"time"
"github.com/Tnze/go-mc/chat"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid"
)
type MessageHeader struct {
Signature []byte
Sender uuid.UUID
}
func (m *MessageHeader) WriteTo(w io.Writer) (n int64, err error) {
hasSignature := pk.Boolean(len(m.Signature) > 0)
n, err = hasSignature.WriteTo(w)
if err != nil {
return
}
if hasSignature {
n2, err := pk.ByteArray(m.Signature).WriteTo(w)
n += n2
if err != nil {
return n, err
}
}
n3, err := pk.UUID(m.Sender).WriteTo(w)
return n + n3, err
}
func (m *MessageHeader) ReadFrom(r io.Reader) (n int64, err error) {
var hasSignature pk.Boolean
n, err = hasSignature.ReadFrom(r)
if err != nil {
return
}
if hasSignature {
n2, err := (*pk.ByteArray)(&m.Signature).ReadFrom(r)
n += n2
if err != nil {
return n, err
}
}
n3, err := (*pk.UUID)(&m.Sender).ReadFrom(r)
return n + n3, err
}
type MessageBody struct {
PlainMessage string
Message *chat.Message
Timestamp time.Time
Salt int64
History []HistoryMessage
}
func (m *MessageBody) WriteTo(w io.Writer) (n int64, err error) {
return pk.Tuple{
pk.String(m.PlainMessage),
pk.Boolean(m.Message != nil),
pk.Opt{
Has: m.Message != nil,
Field: m.Message,
},
pk.Long(m.Timestamp.UnixMilli()),
pk.Long(m.Salt),
pk.Array(&m.History),
}.WriteTo(w)
}
func (m *MessageBody) ReadFrom(r io.Reader) (n int64, err error) {
var hasContent pk.Boolean
var timestamp pk.Long
n, err = pk.Tuple{
(*pk.String)(&m.PlainMessage),
&hasContent,
pk.Opt{
Has: &hasContent,
Field: func() pk.FieldDecoder {
m.Message = new(chat.Message)
return m.Message
},
},
&timestamp,
(*pk.Long)(&m.Salt),
pk.Array(&m.History),
}.ReadFrom(r)
m.Timestamp = time.UnixMilli(int64(timestamp))
return
}
type FilterMask struct {
Type byte
Mask pk.BitSet
}
func (f *FilterMask) WriteTo(w io.Writer) (n int64, err error) {
n, err = pk.VarInt(f.Type).WriteTo(w)
if err != nil {
return
}
if f.Type == 2 {
var n1 int64
n1, err = f.Mask.WriteTo(w)
n += n1
}
return
}
func (f *FilterMask) ReadFrom(r io.Reader) (n int64, err error) {
var Type pk.VarInt
if n, err = Type.ReadFrom(r); err != nil {
return
}
f.Type = byte(Type)
if f.Type == 2 {
var n1 int64
n1, err = f.Mask.ReadFrom(r)
n += n1
}
return
}
type PlayerMessage struct {
MessageHeader
MessageSignature []byte
MessageBody
UnsignedContent *chat.Message
FilterMask
}
func (msg *PlayerMessage) ReadFrom(r io.Reader) (n int64, err error) {
var hasUnsignedContent pk.Boolean
return pk.Tuple{
&msg.MessageHeader,
(*pk.ByteArray)(&msg.MessageSignature),
&msg.MessageBody,
&hasUnsignedContent,
pk.Opt{
Has: &hasUnsignedContent,
Field: func() pk.FieldDecoder {
msg.UnsignedContent = new(chat.Message)
return msg.UnsignedContent
},
},
&msg.FilterMask,
}.ReadFrom(r)
}
func (msg *PlayerMessage) WriteTo(w io.Writer) (n int64, err error) {
return pk.Tuple{
&msg.MessageHeader,
pk.ByteArray(msg.MessageSignature),
&msg.MessageBody,
pk.Boolean(msg.UnsignedContent != nil),
pk.Opt{
Has: msg.UnsignedContent != nil,
Field: msg.UnsignedContent,
},
&msg.FilterMask,
}.WriteTo(w)
}
func genSalt() (salt int64) {
err := binary.Read(rand.Reader, binary.BigEndian, &salt)
if err != nil {
panic(err)
}
return
}
func Unsigned(id uuid.UUID, plain string, content *chat.Message) (msg PlayerMessage) {
return PlayerMessage{
MessageHeader: MessageHeader{
Signature: nil,
Sender: id,
},
MessageSignature: []byte{},
MessageBody: MessageBody{
PlainMessage: plain,
Message: nil,
Timestamp: time.Now(),
Salt: genSalt(),
History: nil,
},
UnsignedContent: nil,
FilterMask: FilterMask{Type: 0},
}
}
type HistoryMessage struct {
Sender uuid.UUID
Signature []byte
}
func (p *HistoryMessage) ReadFrom(r io.Reader) (n int64, err error) {
n, err = (*pk.UUID)(&p.Sender).ReadFrom(r)
if err != nil {
return
}
n2, err := (*pk.ByteArray)(&p.Signature).ReadFrom(r)
return n + n2, err
}
func (p *HistoryMessage) WriteTo(w io.Writer) (n int64, err error) {
n, err = pk.UUID(p.Sender).WriteTo(w)
if err != nil {
return
}
n2, err := pk.ByteArray(p.Signature).WriteTo(w)
return n + n2, err
}

View File

@ -28,10 +28,10 @@ func Marshal[ID ~int32 | int](id ID, fields ...FieldEncoder) (pk Packet) {
// Scan decode the packet and fill data into fields
func (p Packet) Scan(fields ...FieldDecoder) error {
r := bytes.NewReader(p.Data)
for _, v := range fields {
for i, v := range fields {
_, err := v.ReadFrom(r)
if err != nil {
return err
return fmt.Errorf("scanning packet field[%d] error: %w", i, err)
}
}
return nil

View File

@ -95,13 +95,13 @@ func (b Boolean) WriteTo(w io.Writer) (int64, error) {
}
func (b *Boolean) ReadFrom(r io.Reader) (n int64, err error) {
v, err := readByte(r)
n, v, err := readByte(r)
if err != nil {
return 1, err
return n, err
}
*b = v != 0
return 1, nil
return n, nil
}
func (s String) WriteTo(w io.Writer) (int64, error) {
@ -134,13 +134,14 @@ func (s *String) ReadFrom(r io.Reader) (n int64, err error) {
}
// readByte read one byte from io.Reader
func readByte(r io.Reader) (byte, error) {
func readByte(r io.Reader) (int64, byte, error) {
if r, ok := r.(io.ByteReader); ok {
return r.ReadByte()
v, err := r.ReadByte()
return 1, v, err
}
var v [1]byte
_, err := io.ReadFull(r, v[:])
return v[0], err
n, err := r.Read(v[:])
return int64(n), v[0], err
}
func (b Byte) WriteTo(w io.Writer) (n int64, err error) {
@ -149,12 +150,12 @@ func (b Byte) WriteTo(w io.Writer) (n int64, err error) {
}
func (b *Byte) ReadFrom(r io.Reader) (n int64, err error) {
v, err := readByte(r)
n, v, err := readByte(r)
if err != nil {
return 0, err
return n, err
}
*b = Byte(v)
return 1, nil
return n, nil
}
func (u UnsignedByte) WriteTo(w io.Writer) (n int64, err error) {
@ -163,12 +164,12 @@ func (u UnsignedByte) WriteTo(w io.Writer) (n int64, err error) {
}
func (u *UnsignedByte) ReadFrom(r io.Reader) (n int64, err error) {
v, err := readByte(r)
n, v, err := readByte(r)
if err != nil {
return 0, err
return n, err
}
*u = UnsignedByte(v)
return 1, nil
return n, nil
}
func (s Short) WriteTo(w io.Writer) (int64, error) {
@ -267,17 +268,19 @@ func (v VarInt) WriteTo(w io.Writer) (n int64, err error) {
func (v *VarInt) ReadFrom(r io.Reader) (n int64, err error) {
var V uint32
for sec := byte(0x80); sec&0x80 != 0; n++ {
if n > MaxVarIntLen {
var num, n2 int64
for sec := byte(0x80); sec&0x80 != 0; num++ {
if num > MaxVarIntLen {
return n, errors.New("VarInt is too big")
}
sec, err = readByte(r)
n2, sec, err = readByte(r)
n += n2
if err != nil {
return n, err
}
V |= uint32(sec&0x7F) << uint32(7*n)
V |= uint32(sec&0x7F) << uint32(7*num)
}
*v = VarInt(V)
@ -304,16 +307,18 @@ func (v VarLong) WriteTo(w io.Writer) (n int64, err error) {
func (v *VarLong) ReadFrom(r io.Reader) (n int64, err error) {
var V uint64
for sec := byte(0x80); sec&0x80 != 0; n++ {
if n >= MaxVarLongLen {
var num, n2 int64
for sec := byte(0x80); sec&0x80 != 0; num++ {
if num >= MaxVarLongLen {
return n, errors.New("VarLong is too big")
}
sec, err = readByte(r)
n2, sec, err = readByte(r)
n += n2
if err != nil {
return
}
V |= uint64(sec&0x7F) << uint64(7*n)
V |= uint64(sec&0x7F) << uint64(7*num)
}
*v = VarLong(V)

View File

@ -81,8 +81,8 @@ func Array(ary any) Field {
}
type Opt struct {
Has interface{} // Pointer of bool, or `func() bool`
Field interface{} // FieldEncoder, FieldDecoder or both (Field)
Has any // Pointer of bool, or `func() bool`
Field any // FieldEncoder, FieldDecoder, `func() FieldEncoder`, `func() FieldDecoder` or `func() Field`
}
func (o Opt) has() bool {
@ -103,14 +103,32 @@ func (o Opt) has() bool {
func (o Opt) WriteTo(w io.Writer) (int64, error) {
if o.has() {
return o.Field.(FieldEncoder).WriteTo(w)
switch field := o.Field.(type) {
case FieldEncoder:
return field.WriteTo(w)
case func() FieldEncoder:
return field().WriteTo(w)
case func() Field:
return field().WriteTo(w)
default:
panic("unsupported Field type: " + reflect.TypeOf(o.Field).String())
}
}
return 0, nil
}
func (o Opt) ReadFrom(r io.Reader) (int64, error) {
if o.has() {
return o.Field.(FieldDecoder).ReadFrom(r)
switch field := o.Field.(type) {
case FieldDecoder:
return field.ReadFrom(r)
case func() FieldDecoder:
return field().ReadFrom(r)
case func() Field:
return field().ReadFrom(r)
default:
panic("unsupported Field type: " + reflect.TypeOf(o.Field).String())
}
}
return 0, nil
}

8
registry/chattype.go Normal file
View File

@ -0,0 +1,8 @@
package registry
import "github.com/Tnze/go-mc/chat"
type ChatType struct {
Chat chat.Decoration `nbt:"chat"`
Narration chat.Decoration `nbt:"narration"`
}

9
registry/codec.go Normal file
View File

@ -0,0 +1,9 @@
package registry
import "github.com/Tnze/go-mc/nbt"
type NetworkCodec struct {
ChatType Registry[ChatType] `nbt:"minecraft:chat_type"`
DimensionType Registry[Dimension] `nbt:"minecraft:dimension_type"`
WorldGenBiome Registry[nbt.RawMessage] `nbt:"minecraft:worldgen/biome"`
}

25
registry/dimension.go Normal file
View File

@ -0,0 +1,25 @@
package registry
import "github.com/Tnze/go-mc/nbt"
type Dimension struct {
FixedTime int64 `nbt:"fixed_time,omitempty"`
HasSkylight bool `nbt:"has_skylight"`
HasCeiling bool `nbt:"has_ceiling"`
Ultrawarm bool `nbt:"ultrawarm"`
Natural bool `nbt:"natural"`
CoordinateScale float64 `nbt:"coordinate_scale"`
BedWorks bool `nbt:"bed_works"`
RespawnAnchorWorks byte `nbt:"respawn_anchor_works"`
MinY int32 `nbt:"min_y"`
Height int32 `nbt:"height"`
LogicalHeight int32 `nbt:"logical_height"`
InfiniteBurn string `nbt:"infiniburn"`
Effects string `nbt:"effects"`
AmbientLight float64 `nbt:"ambient_light"`
PiglinSafe byte `nbt:"piglin_safe"`
HasRaids byte `nbt:"has_raids"`
MonsterSpawnLightLevel nbt.RawMessage `nbt:"monster_spawn_light_level"` // Tag_Int or {type:"minecraft:uniform", value:{min_inclusive: Tag_Int, max_inclusive: Tag_Int}}
MonsterSpawnBlockLightLimit int32 `nbt:"monster_spawn_block_light_limit"`
}

31
registry/registry.go Normal file
View File

@ -0,0 +1,31 @@
package registry
type Registry[E any] struct {
Type string `nbt:"type"`
Value []struct {
Name string `nbt:"name"`
ID int32 `nbt:"id"`
Element E `nbt:"element"`
} `nbt:"value"`
}
func (r *Registry[E]) Find(name string) (int32, *E) {
for i := range r.Value {
if r.Value[i].Name == name {
return int32(i), &r.Value[i].Element
}
}
return -1, nil
}
func (r *Registry[E]) FindByID(id int32) *E {
if id >= 0 && id < int32(len(r.Value)) && r.Value[id].ID == id {
return &r.Value[id].Element
}
for i := range r.Value {
if r.Value[i].ID == id {
return &r.Value[i].Element
}
}
return nil
}