3 Commits
49 changed files with 191 additions and 4 deletions
+5
View File
@@ -289,6 +289,9 @@ func (a *Account) CheckGiftCodeValidity(giftCode string) (valid bool, err error)
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) {
a = &Account{
Ygg: ygg,
@@ -303,6 +306,8 @@ func NewAccountFromYGG(ygg string) (a *Account, err error) {
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) {
return &Account{
Name: name,
+2
View File
@@ -4,6 +4,8 @@ import (
"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 {
return &Account{
Name: name,
+13
View File
@@ -9,6 +9,8 @@ import (
"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) {
resp, err := http.Get("https://sessionserver.mojang.com/blockedservers")
if err != nil {
@@ -36,6 +38,9 @@ type ProfileResp struct {
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) {
url := "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + name + "&serverId=" + server
if ip != "" {
@@ -62,6 +67,8 @@ type MojangPublicKeysResp struct {
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) {
resp, err := http.Get("https://sessionserver.mojang.com/session/minecraft/profile/public_key_by_token")
if err != nil {
@@ -72,6 +79,8 @@ func MojangPublicKeys() (keys MojangPublicKeysResp, err error) {
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) {
url := "https://sessionserver.mojang.com/session/minecraft/profile/" + id
if !unsinged {
@@ -91,6 +100,8 @@ type IdName struct {
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) {
// may also use these:
//https://api.mojang.com/users/profiles/minecraft/<player name>
@@ -105,6 +116,8 @@ func NameToId(name string) (idName IdName, err error) {
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) {
b := bytes.NewBuffer(nil)
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})
}
// Start begins responding to keep-alive packets on the connection.
func Start(c *proto.Conn) {
m := &mod{Conn: c}
m.RegisterUntilLatest(m.onKeepAlive)
+1
View File
@@ -47,6 +47,7 @@ func (c *ConfigIgnorer) OnKeepAlive(packet *v1_21_8.ConfigurationToClientPacketK
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) {
ci := &ConfigIgnorer{c}
ci.RegisterCritical(ci.Default)
+1
View File
@@ -9,6 +9,7 @@ import (
"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) {
return MakeHandshakePacketAddr(c, nextState, c.RemoteAddr().String())
}
+5
View File
@@ -134,6 +134,7 @@ func (s *LoginClient) OnStart() (err error) {
return
}
// LoginRaw performs the login handshake on an already-established connection without DNS resolution or configuration handling.
func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
lc := &LoginClient{
Conn: c,
@@ -166,18 +167,21 @@ func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
return
}
// WithoutDns returns a login setting that disables SRV DNS resolution during login.
func WithoutDns() func(ls *loginSettings) {
return func(ls *loginSettings) {
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) {
return func(ls *loginSettings) {
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) {
return func(ls *loginSettings) {
ls.Version = version
@@ -191,6 +195,7 @@ type loginSettings struct {
IgnoreConfig bool
}
// 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) {
ls := &loginSettings{}
+2
View File
@@ -41,10 +41,12 @@ type CFB8 struct {
mutex sync.Mutex
}
// NewCFB8Decrypt creates a new CFB8 stream cipher in decrypt mode.
func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 {
return newCFB8(c, iv, true)
}
// NewCFB8Encrypt creates a new CFB8 stream cipher in encrypt mode.
func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 {
return newCFB8(c, iv, false)
}
+1
View File
@@ -21,6 +21,7 @@ func uuidV3(space []byte, data []byte) uuid.UUID {
return u
}
// FromOfflinePlayer generates a UUID for an offline-mode (cracked) player from their display name.
func FromOfflinePlayer(displayName string) uuid.UUID {
return uuidV3([]byte("OfflinePlayer:"), []byte(displayName))
}
+3
View File
@@ -28,6 +28,7 @@ type Biome struct {
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) {
var ok bool
if ret, ok = BiomesCache[v]; ok {
@@ -39,6 +40,7 @@ func BiomesForVersion(v string) (ret []*Biome) {
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) {
for _, b := range BiomesForVersion(version) {
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")
// LookupBiomeByName returns the biome with the given name in the given version.
func LookupBiomeByName(version string, name string) (biome *Biome, ok bool) {
for _, b := range BiomesForVersion(version) {
if b.Name == name {
+5
View File
@@ -59,6 +59,7 @@ type BlockState struct {
var BlocksCache = make(map[string][]*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) {
var ok bool
if ret, ok = BlocksCache[v]; ok {
@@ -84,6 +85,7 @@ func BlocksForVersion(v string) (ret []*Block) {
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) {
idmap, found := BlocksCacheIdMap[version]
if !found {
@@ -101,6 +103,7 @@ func LookupBlockByStateId(version string, stateId int32) (block *Block, ok bool)
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) {
for _, b := range BlocksForVersion(version) {
if b.Name == name {
@@ -111,6 +114,7 @@ func LookupBlockByName(version string, name string) (block *Block, ok bool) {
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) {
b, ok := LookupBlockByName(version, name)
if !ok {
@@ -135,6 +139,7 @@ var BadBlockStateTypeErr = errors.New("bad block state type")
var BadEnumErr = errors.New("bad enum")
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) {
b, ok := LookupBlockByStateId(version, stateId)
if !ok {
+2
View File
@@ -12,6 +12,7 @@ import (
//go:embed minecraft-data/pc minecraft-data/dataPaths.json
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) {
b, err := MinecraftData.ReadFile(path)
if err != nil {
@@ -21,6 +22,7 @@ func LoadJson(path string, data any) (err error) {
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) {
b, err := MinecraftData.ReadFile("minecraft-data/" + Paths.Data[version][dataName] + "/" + dataName + ".json")
if err != nil {
+3
View File
@@ -19,6 +19,7 @@ type Item struct {
var ItemsCache = make(map[string][]*Item)
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) {
var ok bool
ItemsCacheMutex.RLock()
@@ -35,6 +36,7 @@ func ItemsForVersion(v string) (ret []*Item) {
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) {
for _, i := range ItemsForVersion(version) {
if i.Id == id {
@@ -46,6 +48,7 @@ func LookupItemById(version string, id int32) (item *Item, ok bool) {
return
}
// LookupItemByName returns the item with the given name in the given version.
func LookupItemByName(version string, name string) (item *Item, ok bool) {
for _, i := range ItemsForVersion(version) {
if i.Name == name {
+3
View File
@@ -20,6 +20,7 @@ type VersionInfo struct {
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) {
for _, v := range ProtocolVersions {
if v.MinecraftVersion == name {
@@ -31,6 +32,7 @@ func LookUpVersionByName(name string) (ret VersionInfo, err error) {
return
}
// LookUpVersionByProtocolVersion returns the VersionInfo for the given protocol version number.
func LookUpVersionByProtocolVersion(version int32) (ret VersionInfo, err error) {
for _, v := range ProtocolVersions {
if v.Version == version {
@@ -42,6 +44,7 @@ func LookUpVersionByProtocolVersion(version int32) (ret VersionInfo, err error)
return
}
// LookUpVersionByDataVersion returns the VersionInfo for the given Minecraft data version number.
func LookUpVersionByDataVersion(version int32) (ret VersionInfo, err error) {
for _, v := range ProtocolVersions {
if v.DataVersion == version {
+1
View File
@@ -11,6 +11,7 @@ import (
//go:embed registry/*
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) {
r, err := Registry.Open("registry/" + name + ".nbt")
if err != nil {
+6 -1
View File
@@ -69,6 +69,8 @@ var WhileClosingErr = errors.New("error while closing")
var ContextDoneErr = errors.New("context done")
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) {
hv = reflect.ValueOf(h)
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.
// Warning: If a bad handler is passed in the program will panic.
func (l *Loop) RegisterCritical(h any) {
func (l *Loop) RegisterCritical(hs ...any) {
for _, h := range hs {
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.
@@ -233,6 +237,7 @@ func (l *Loop) StartLoop() (err error) {
return
}
// NewLoop creates a new Loop with a background context.
func NewLoop() (l *Loop) {
return NewLoopCtx(context.Background())
}
+1
View File
@@ -48,6 +48,7 @@ type BiomeNoise struct {
Temperature, Humidity, ContinentalNess, Erosion, Shift, Weirdness DoublePerlin
}
// BiomeNoiseFromXoroshiro creates a BiomeNoise from the given Xoroshiro source.
func BiomeNoiseFromXoroshiro(x Xoroshiro, large bool) (v BiomeNoise) {
x = x.Split()
+1
View File
@@ -6,6 +6,7 @@ type DoublePerlin struct {
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) {
na := -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)
}
// PerlinNoiseFromXoroshiro initializes a PerlinNoise from the given Xoroshiro source.
func PerlinNoiseFromXoroshiro(x Xoroshiro) (p PerlinNoise) {
p.xoffset = 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,
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) {
if (-omin < 0 || -omin >= len(octaveLacunaInits)) || (lenn < 0 || lenn >= len(octavePersistInits)) {
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
}
// CreateBiomeDepthSpline builds the Minecraft biome depth spline used for terrain generation.
func CreateBiomeDepthSpline() Spline {
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)
+2
View File
@@ -42,6 +42,7 @@ func (x *Xoroshiro) NextFloat64() float64 {
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) {
sum := md5.Sum([]byte(s))
x.L = binary.BigEndian.Uint64(sum[:8])
@@ -49,6 +50,7 @@ func XoroshiroFromString(s string) (x Xoroshiro) {
return
}
// XoroshiroFromSeed creates an Xoroshiro from an int64 seed.
func XoroshiroFromSeed(seed int64) (x Xoroshiro) {
x.L = uint64(seed) ^ 0x6a09e667f3bcc909
x.H = x.L + 0x9e3779b97f4a7c15
+1
View File
@@ -174,6 +174,7 @@ type Attribute struct {
Base float64
}
// ReadLevelData reads and decodes the level.dat NBT file from a reader.
func ReadLevelData(r io.Reader) (l Level, err error) {
var lDataWrapped struct{ Data Level }
err = nbt.ReadFile(r, &lDataWrapped)
+1
View File
@@ -52,6 +52,7 @@ type ChunkHeader struct {
Chunk, Time [ChunksRegionFile]int32
}
// ReadChunkHeader reads the chunk header from an MCA region file.
func ReadChunkHeader(r *os.File) (ch *ChunkHeader, err error) {
var h ChunkHeader
err = binary.Read(r, binary.BigEndian, &h)
+1
View File
@@ -10,6 +10,7 @@ type Chunk struct {
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) {
c = &Chunk{}
c.Version = version
+3
View File
@@ -94,6 +94,7 @@ type Structure struct {
Version string
}
// LoadFromPath loads a litematic file from disk.
func LoadFromPath(path string) (s *Structure, err error) {
f, err := os.Open(path)
if err != nil {
@@ -104,6 +105,7 @@ func LoadFromPath(path string) (s *Structure, err error) {
return
}
// LoadFromFile reads and parses a litematic file from a reader.
func LoadFromFile(r io.Reader) (s *Structure, err error) {
n, err := nbt.ReadUnstructuredFile(r)
if err != nil {
@@ -112,6 +114,7 @@ func LoadFromFile(r io.Reader) (s *Structure, err error) {
return Load(n.Value)
}
// Load decodes raw NBT data into a litematic Structure.
func Load(v any) (s *Structure, err error) {
s = &Structure{}
err = mapstructure.Decode(v, &s.File)
+7
View File
@@ -30,6 +30,7 @@ type Section struct {
Blocks, Biomes *Storage
}
// MakeBiomeFormat returns the StorageFormat for biome data at the given version.
func MakeBiomeFormat(version string) StorageFormat {
directBpe := uint8(math.Ceil(math.Log2(float64(len(data2.BiomesForVersion(version))))))
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 {
directBpe := uint8(math.Ceil(math.Log2(float64(len(data2.BlocksForVersion(version))))))
return StorageFormat{
@@ -51,6 +53,7 @@ func MakeBlockFormat(version string) StorageFormat {
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) {
var bpe uint8
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)
}
// WriteSectionStorage writes a packed section storage to a writer.
func WriteSectionStorage(w io.Writer, s *Storage) (err error) {
err = binary.Write(w, binary.BigEndian, s.bpe)
if err != nil {
@@ -122,6 +126,7 @@ func WriteSectionStorage(w io.Writer, s *Storage) (err error) {
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) {
s = Section{}
err = binary.Read(r, binary.BigEndian, &s.BlockCount)
@@ -136,6 +141,7 @@ func SectionDecodePacket(r io.Reader, version string) (s Section, err error) {
return
}
// SectionEncodePacket encodes a chunk section to the Minecraft packet wire format.
func SectionEncodePacket(w io.Writer, s Section) (err error) {
err = binary.Write(w, binary.BigEndian, s.BlockCount)
if err != nil {
@@ -149,6 +155,7 @@ func SectionEncodePacket(w io.Writer, s Section) (err error) {
return
}
// SectionEquals returns whether two sections are equal.
func SectionEquals(a, b Section) bool {
if a.BlockCount != b.BlockCount {
return false
+1
View File
@@ -250,6 +250,7 @@ func (r *Storage) Equals(other *Storage) bool {
return true
}
// CountStorage counts the number of elements in the storage that match the given value.
func CountStorage(s *Storage, v int32) (a int) {
for i := range s.format.Len {
if w, _ := s.Get(i); w == v {
+1
View File
@@ -5,6 +5,7 @@ import (
"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) {
n, err := nbt.ReadUnstructuredFilePath(fileName)
if err != nil {
+1
View File
@@ -2,6 +2,7 @@ package mapstructure
var DefaultFormat = NewFormat("mapstructure", WithRequireAll(), WithTrySnakeCase(), WithTryLowCase())
// Decode is a stub placeholder. Use Format.Decode instead.
func Decode(data any, v any) error {
return nil
}
+8
View File
@@ -25,6 +25,7 @@ type Format struct {
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 {
f := &Format{
Name: name,
@@ -39,30 +40,35 @@ func NewFormat(name string, opts ...Option) *Format {
type Option func(*Format)
// WithRequireAll returns an Option that requires all struct fields to be present in the map.
func WithRequireAll() Option {
return func(f *Format) {
f.requireAll = true
}
}
// WithErrorOnExtra returns an Option that causes an error when the map contains unmatched keys.
func WithErrorOnExtra() Option {
return func(f *Format) {
f.errorOnExtra = true
}
}
// WithTrySnakeCase returns an Option that tries to match struct fields using snake_case names.
func WithTrySnakeCase() Option {
return func(f *Format) {
f.trySnakeCase = true
}
}
// WithTryLowCase returns an Option that tries to match struct fields using lowercase names.
func WithTryLowCase() Option {
return func(f *Format) {
f.tryLowCase = true
}
}
// WithTypeCodec returns an Option that registers custom decode/encode functions for a type.
func WithTypeCodec(decodeFn, encodeFn any) Option {
dt := reflect.TypeOf(decodeFn)
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 {
v := reflect.ValueOf(target)
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)
}
// 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) {
v := reflect.ValueOf(source)
if v.Kind() == reflect.Pointer {
+5
View File
@@ -10,6 +10,7 @@ import (
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) {
n, err := ReadUnstructuredFile(file)
if err != nil {
@@ -18,6 +19,7 @@ func ReadFile(file io.Reader, data any) (err error) {
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) {
f, err := os.Create(path)
if err != nil {
@@ -27,6 +29,7 @@ func WriteUnstructuredFilePath(path string, n *Tag) (err error) {
return WriteUnstructuredFile(f, n)
}
// WriteUnstructuredFile writes an NBT tag as a gzipped file to w.
func WriteUnstructuredFile(file io.Writer, n *Tag) (err error) {
gzw := gzip.NewWriter(file)
defer gzw.Close()
@@ -34,6 +37,7 @@ func WriteUnstructuredFile(file io.Writer, n *Tag) (err error) {
return
}
// ReadUnstructuredFile reads a gzipped NBT file from r and returns the root Tag.
func ReadUnstructuredFile(file io.Reader) (n *Tag, err error) {
r, err := gzip.NewReader(file)
if err != nil {
@@ -45,6 +49,7 @@ func ReadUnstructuredFile(file io.Reader) (n *Tag, err error) {
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) {
f, err := os.Open(path)
if err != nil {
+13
View File
@@ -73,6 +73,7 @@ type Conn struct {
Log *slog.Logger
}
// StartConn starts the connection event loop, registering the receive routine and login tick handler.
func (c *Conn) StartConn() (err error) {
c.RegisterEventSource(c.ActivateReceiveRoutine())
c.RegisterCritical(c.OnLoginTick)
@@ -81,6 +82,7 @@ func (c *Conn) StartConn() (err error) {
return
}
// OnLoginTick handles a login tick by receiving a packet and firing it as an event.
func (c *Conn) OnLoginTick(_ LoginTick) (err error) {
packet, err := c.Receive()
if err != nil {
@@ -90,30 +92,35 @@ func (c *Conn) OnLoginTick(_ LoginTick) (err error) {
return
}
// SetState sets the connection's protocol state.
func (c *Conn) SetState(state proto_base.State) {
c.stateMutex.Lock()
defer c.stateMutex.Unlock()
c.state = state
}
// State returns the current connection protocol state.
func (c *Conn) State() proto_base.State {
c.stateMutex.RLock()
defer c.stateMutex.RUnlock()
return c.state
}
// SetAsyncPackets enables or disables asynchronous packet handling in the receive routine.
func (c *Conn) SetAsyncPackets(asyncPackets bool) {
c.asyncPacketsMutex.Lock()
defer c.asyncPacketsMutex.Unlock()
c.asyncPackets = asyncPackets
}
// GetAsyncPackets returns whether asynchronous packet handling is enabled.
func (c *Conn) GetAsyncPackets() bool {
c.asyncPacketsMutex.RLock()
defer c.asyncPacketsMutex.RUnlock()
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) {
versionData, err := data.LookUpVersionByName(version)
if err != nil {
@@ -124,6 +131,7 @@ func (c *Conn) SetVersion(version string) (err error) {
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) {
versionData, err := data.LookUpVersionByProtocolVersion(version)
if err != nil {
@@ -202,6 +210,7 @@ func SimplePacketToBytes(packet proto_base.EncodeDecodeAble) (b []byte, err erro
return
}
// SimplePacketToBytesLenPrefix is like SimplePacketToBytes but also prepends the length-prefix framing.
func SimplePacketToBytesLenPrefix(packet proto_base.EncodeDecodeAble) (b []byte, err error) {
b, err = SimplePacketToBytes(packet)
if err != nil {
@@ -282,6 +291,7 @@ func (c *Conn) sendRegisteredPacket(packet proto_base.EncodeDecodeAble) (err err
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) {
switch packet := packet.(type) {
case *UnCodablePacket:
@@ -292,6 +302,7 @@ func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) {
return
}
// Receive reads and decodes a raw packet from the remote peer.
func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) {
if c.R == nil {
return
@@ -335,10 +346,12 @@ func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) {
}
return
}
// NewConn creates a new Conn with default settings and a background context.
func NewConn() (ret *Conn) {
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) {
ret = &Conn{}
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) {
pP, ok := pidMap[PidEtc{direction, state, pid, version}]
if !ok {
@@ -39,6 +40,7 @@ func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.Sta
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) {
i, ok := LookUpTypeByPacketInfo(direction, state, pid, version)
if !ok {
@@ -48,6 +50,7 @@ func LookUpTypeByPacketInfoAndCopyType(direction proto_base.Direction, state pro
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) {
undecodedPacket, isUndecoable := packet.(*UnCodablePacket)
if isUndecoable {
@@ -64,6 +67,7 @@ func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (p proto_base.Pa
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) {
for _, p := range proto_generated.Packets {
if state == p.State && reflect.TypeOf(p.Type) == reflect.TypeOf(packet) {
@@ -73,6 +77,7 @@ func LookupPacketInfoByTypeAndState(packet proto_base.EncodeDecodeAble, state pr
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) {
for _, p := range proto_generated.Packets {
if state == p.State && p.Name == name && p.ProtocolVersion == version {
@@ -82,6 +87,7 @@ func LookupPacketInfoByNameProtocolVersionAndState(name string, version int32, s
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) {
for _, p := range proto_generated.Packets {
if state == p.State && p.Name == name && p.ProtocolVersion == version && p.Direction == direction {
+5
View File
@@ -27,18 +27,21 @@ type RawConn struct {
MaxPacketSize int32
}
// SetCompressionThreshold sets the packet compression threshold. Values below 0 disable compression.
func (c *RawConn) SetCompressionThreshold(threshold int32) {
c.CompressionThresholdMut.Lock()
defer c.CompressionThresholdMut.Unlock()
c.compressionThreshold = threshold
}
// GetCompressionThreshold returns the current compression threshold.
func (c *RawConn) GetCompressionThreshold() int32 {
c.CompressionThresholdMut.RLock()
defer c.CompressionThresholdMut.RUnlock()
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) {
if c.MaxPacketSize > 0 && int32(len(rawPacketBytes)) > c.MaxPacketSize {
err = errors.New("packet too large")
@@ -90,6 +93,7 @@ func (c *RawConn) SendRaw(rawPacketBytes []byte) (err error) {
return
}
// ReceiveRaw reads and decompresses (if needed) a raw packet from the connection.
func (c *RawConn) ReceiveRaw() (packetBytes []byte, err error) {
rawPacketLen, err := proto_base.DecodeVarInt(c.R)
if err != nil {
@@ -142,6 +146,7 @@ func (c *RawConn) ReceiveRaw() (packetBytes []byte, err error) {
return
}
// SetSecret enables AES/CFB8 encryption on the connection using the shared secret.
func (c *RawConn) SetSecret(sharedSecret []byte) (err error) {
var b cipher.Block
b, err = aes.NewCipher(sharedSecret)
+1
View File
@@ -16,6 +16,7 @@ type Reader struct {
Version int32
}
// NewReader creates a new replay Reader from an io.Reader.
func NewReader(r io.Reader) *Reader {
return &Reader{r: r}
}
+1
View File
@@ -13,6 +13,7 @@ type Writer struct {
w io.Writer
}
// NewWriter creates a new replay Writer from an io.Writer.
func NewWriter(w io.Writer) *Writer {
return &Writer{w: w}
}
+2
View File
@@ -11,6 +11,7 @@ import (
"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 {
switch from.Kind() {
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) {
switch from.Kind() {
case reflect.Struct:
+39 -1
View File
@@ -7,10 +7,12 @@ import (
"math"
)
// UnimplementedErr is returned when a code path is not yet implemented.
var (
UnimplementedErr = errors.New("unimplemented")
)
// Direction indicates whether a packet is sent to the server or to the client.
type Direction int
const (
@@ -18,6 +20,7 @@ const (
ToClient
)
// String returns a human-readable representation of the Direction.
func (d Direction) String() string {
switch d {
case ToServer:
@@ -29,10 +32,12 @@ func (d Direction) String() string {
}
}
// Opposite returns the reverse Direction.
func (d Direction) Opposite() Direction {
return d ^ 1
}
// State represents the Minecraft connection state.
type State int32
const (
@@ -43,6 +48,7 @@ const (
Play
)
// String returns a human-readable representation of the State.
func (s State) String() string {
switch s {
case Handshaking:
@@ -60,13 +66,16 @@ func (s State) String() string {
}
}
// Actor represents a participant in a Minecraft connection.
type Actor int
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
)
// String returns a human-readable representation of the Actor.
func (a Actor) String() string {
switch a {
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 {
switch a {
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 {
switch a {
case Servee:
@@ -101,13 +112,19 @@ func (a Actor) ReceiveDirection() Direction {
}
type (
// ToDo is a placeholder type for unimplemented packet fields.
ToDo struct{}
// RestBuffer represents the remaining unread bytes in a packet buffer.
RestBuffer []byte
)
// ToDoError is returned when a ToDo placeholder is used.
var ToDoError = errors.New("to do")
// BadTypeError is returned when an unexpected type is encountered.
var BadTypeError = errors.New("bad type")
// Encode writes the RestBuffer to w.
func (b *RestBuffer) Encode(w io.Writer) (err error) {
n, err := w.Write(*b)
if n != len(*b) {
@@ -116,11 +133,13 @@ func (b *RestBuffer) Encode(w io.Writer) (err error) {
return
}
// Decode reads all remaining bytes from r into the RestBuffer.
func (b *RestBuffer) Decode(r io.Reader) (err error) {
*b, err = io.ReadAll(r)
return
}
// EncodeString encodes a length-prefixed UTF-8 string in Minecraft varint format.
func EncodeString(w io.Writer, s string) (err error) {
err = EncodeVarInt(w, int32(len(s)))
if err != nil {
@@ -130,6 +149,7 @@ func EncodeString(w io.Writer, s string) (err error) {
return
}
// DecodeString decodes a length-prefixed UTF-8 string in Minecraft varint format.
func DecodeString(r io.Reader) (ret string, err error) {
l, err := DecodeVarInt(r)
if err != nil {
@@ -146,16 +166,19 @@ func DecodeString(r io.Reader) (ret string, err error) {
return string(rawString), nil
}
// Encode is a no-op stub that always returns ToDoError.
func (t ToDo) Encode(_ io.Writer) (err error) {
err = ToDoError
return
}
// Decode is a no-op stub that always returns ToDoError.
func (t ToDo) Decode(_ io.Reader) (ret ToDo, err error) {
err = ToDoError
return
}
// EncodeVarInt encodes a signed 32-bit integer in Minecraft varint format.
func EncodeVarInt(w io.Writer, v int32) (err error) {
uv := uint32(v)
for {
@@ -175,6 +198,7 @@ func EncodeVarInt(w io.Writer, v int32) (err error) {
return nil
}
// DecodeVarInt decodes a signed 32-bit integer in Minecraft varint format.
func DecodeVarInt(r io.Reader) (ret int32, err error) {
for i := range 5 { // 32/7
var b uint8 = 0
@@ -191,6 +215,7 @@ func DecodeVarInt(r io.Reader) (ret int32, err error) {
return
}
// EncodeVarLong encodes a signed 64-bit integer in Minecraft varlong format.
func EncodeVarLong(w io.Writer, v int64) (err error) {
uv := uint64(v)
for {
@@ -210,6 +235,7 @@ func EncodeVarLong(w io.Writer, v int64) (err error) {
return nil
}
// DecodeVarLong decodes a signed 64-bit integer in Minecraft varlong format.
func DecodeVarLong(r io.Reader) (ret int64, err error) {
for i := range 10 { // ceil(64/7)
var b uint8
@@ -226,6 +252,7 @@ func DecodeVarLong(r io.Reader) (ret int64, err error) {
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) {
var ok bool
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
}
// EncodeDecodeAble is implemented by types that can be encoded and decoded for
// Minecraft protocol packets.
type EncodeDecodeAble interface {
Encode(w io.Writer) (err error)
Decode(r io.ReadSeeker) (err error)
}
// PacketInfo holds metadata about a Minecraft protocol packet.
type PacketInfo struct {
Type EncodeDecodeAble
Name string
@@ -249,6 +279,7 @@ type PacketInfo struct {
ProtocolVersion int32
}
// Bool2uint8 converts a bool to a uint8 (1 for true, 0 for false).
func Bool2uint8(b bool) uint8 {
if b {
return 1
@@ -256,6 +287,7 @@ func Bool2uint8(b bool) uint8 {
return 0
}
// Bool2uint16 converts a bool to a uint16 (1 for true, 0 for false).
func Bool2uint16(b bool) uint16 {
if b {
return 1
@@ -263,6 +295,7 @@ func Bool2uint16(b bool) uint16 {
return 0
}
// Bool2uint32 converts a bool to a uint32 (1 for true, 0 for false).
func Bool2uint32(b bool) uint32 {
if b {
return 1
@@ -270,6 +303,7 @@ func Bool2uint32(b bool) uint32 {
return 0
}
// Bool2uint64 converts a bool to a uint64 (1 for true, 0 for false).
func Bool2uint64(b bool) uint64 {
if b {
return 1
@@ -277,13 +311,17 @@ func Bool2uint64(b bool) uint64 {
return 0
}
// LpVec3d is a compressed 3D velocity vector used in Minecraft packets.
type LpVec3d struct {
X, Y, Z float64
}
// Encode encodes the LpVec3d as a compressed velocity triple into w.
func (v *LpVec3d) Encode(w io.Writer) error {
return writeVelocity(w, *v)
}
// Decode decodes a compressed velocity triple from r into the LpVec3d.
func (v *LpVec3d) Decode(r io.ReadSeeker) error {
v3, err := readVelocity(r)
if err != nil {
+1
View File
@@ -14,6 +14,7 @@ type ConfigServer struct {
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) {
cs := &ConfigServer{Conn: c}
n, err := data.LoadRegistry(c.Version)
+1
View File
@@ -17,6 +17,7 @@ var (
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) {
if c.Actor != proto_base.Servee {
err = NotClientErr
+8
View File
@@ -204,6 +204,7 @@ func (l *LoginServer) FinishLogin() (err error) {
type LoginServerSetting func(*LoginServer)
// WithOtherAccount returns a setting that overrides the login identity with the given account.
func WithOtherAccount(a *api2.Account) LoginServerSetting {
return func(s *LoginServer) {
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 {
return func(s *LoginServer) {
s.Status = status
}
}
// WithStatus marshals a StatusResponse to JSON and passes it to WithRawStatus.
func WithStatus(response StatusResponse) LoginServerSetting {
var status []byte
status, err := json.Marshal(response)
@@ -229,6 +232,7 @@ func WithStatus(response StatusResponse) LoginServerSetting {
return WithRawStatus(status)
}
// WithCompatibleVersionsRange returns a setting that restricts the server to protocol versions in [start, end].
func WithCompatibleVersionsRange(start, end int32) LoginServerSetting {
var versions []int32
for i := start; i <= end; i++ {
@@ -237,24 +241,28 @@ func WithCompatibleVersionsRange(start, end int32) LoginServerSetting {
return WithCompatibleVersions(versions...)
}
// WithCompatibleVersions returns a setting that restricts the server to the given protocol versions.
func WithCompatibleVersions(versions ...int32) LoginServerSetting {
return func(s *LoginServer) {
s.CompatibleVersions = versions
}
}
// WithoutOnlineMode returns a setting that disables online-mode authentication.
func WithoutOnlineMode() LoginServerSetting {
return func(s *LoginServer) {
s.OnlineMode = false
}
}
// WithoutEncryption returns a setting that disables connection encryption.
func WithoutEncryption() LoginServerSetting {
return func(s *LoginServer) {
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) {
ret = &LoginServer{Conn: c}
ret.OnlineMode = true
+5
View File
@@ -9,6 +9,7 @@ import (
"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) {
ret = proto.NewConn()
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)
// ServeClient accepts a single client connection, creates a handler via the factory, and runs it.
func ServeClient(cNet net.Conn, factory Factory) {
c := Servee(cNet)
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) {
l, err := net.Listen("tcp", listenAddr)
if err != nil {
@@ -57,6 +60,8 @@ func StartServerWithFactory(listenAddr string, factory Factory) (err error) {
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) {
l, err := net.Listen("tcp", listenAddr)
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})
}
// ServeStatus runs the server list ping sequence, responding with the given StatusResponse.
func ServeStatus(c *proto.Conn, s StatusResponse) (err error) {
status, err := json.Marshal(s)
if err != nil {
+1
View File
@@ -526,6 +526,7 @@ func replacePlaceholders(s string, values map[string]any) string {
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 {
s := replacePlaceholders(format, placeholders)
return Pretty(s)
+3
View File
@@ -7,6 +7,7 @@ import (
var boundaries = []rune{' ', '_', '-', '.', ',', ';', ':', '(', ')', '[', ']', '{', '}'}
// CamelCase converts a string with word boundaries to CamelCase.
func CamelCase(s string) string {
ret := ""
atWordBoundary := true
@@ -24,6 +25,7 @@ func CamelCase(s string) string {
return ret
}
// SnakeCase converts a CamelCase string to snake_case.
func SnakeCase(s string) string {
ret := ""
for i, c := range s {
@@ -38,6 +40,7 @@ func SnakeCase(s string) string {
return ret
}
// FirstLetterLower returns the string with the first rune lowercased.
func FirstLetterLower(s string) string {
return string(unicode.ToLower(rune(s[0]))) + s[1:]
}
+2
View File
@@ -2,10 +2,12 @@ package util
import "math"
// FloorDiv returns floor division (rounds toward negative infinity).
func FloorDiv(a, b int32) int32 {
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 {
return uint8(math.Ceil(math.Log2(x)))
}
+5
View File
@@ -2,6 +2,7 @@ package util
import "fmt"
// MustOk returns v if ok is true, otherwise panics.
func MustOk[T any](v T, ok bool) T {
if !ok {
panic("not ok")
@@ -9,6 +10,7 @@ func MustOk[T any](v T, ok bool) T {
return v
}
// MustT returns v if err is nil, otherwise panics.
func MustT[T any](v T, err error) T {
if err != nil {
panic(err)
@@ -16,12 +18,14 @@ func MustT[T any](v T, err error) T {
return v
}
// Must panics if err is non-nil.
func Must(err error) {
if err != nil {
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) {
var zero T
r, ok := v.(T)
@@ -31,6 +35,7 @@ func AssertTypeError[T any](v any) (T, error) {
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) {
v, ok := m[k]
if !ok {
+5
View File
@@ -6,6 +6,7 @@ import (
"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 {
keysIter := maps.Keys(m)
keys := slices.Collect(keysIter)
@@ -13,6 +14,7 @@ func OrderedKeys[Map map[K]V, K cmp.Ordered, V any](m Map) []K {
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 {
ret := map[K]T{}
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
}
// 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 {
ret := map[V]K{}
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
}
// PopFront removes and returns the first element of the slice.
func PopFront[T any](s []T) (T, []T, bool) {
if len(s) == 0 {
var zero T
@@ -37,6 +41,7 @@ func PopFront[T any](s []T) (T, []T, bool) {
return s[0], s[1:], true
}
// CombineNamAndData returns a slice containing name and data.
func CombineNamAndData(name string, data any) []any {
return []any{name, data}
}