14 Commits
Author SHA1 Message Date
adman 8f771166a6 better unknown command text 2026-03-27 13:16:16 +01:00
adman 5d6a1a25ea fix packet spy 2026-03-27 13:09:07 +01:00
adman ca33185f20 fix LookupPacketInfoByType for undecoable 2026-03-27 12:59:10 +01:00
adman e921e9e9d2 fix routine thing 2026-03-27 00:03:01 +01:00
adman 7337068d86 more performance 2026-03-26 23:34:30 +01:00
adman 9983062522 fix timer 2026-03-26 22:39:48 +01:00
adman 4fd5324064 new event system (again) 2026-03-26 22:19:03 +01:00
adman df0e5e354b more srv stuff and packet inspetor 2026-03-25 19:00:18 +01:00
adman 23b5a5d9b7 move cmd to more sensible location 2026-03-25 14:53:38 +01:00
adman 46770ed980 Account changes
- now supports custom enviorments/servers
- cleaner code imo
2026-03-25 14:48:42 +01:00
adman 6179c5f6b6 fix proxy 2026-03-24 22:36:14 +01:00
adman f758a10575 performance fix 2026-03-24 21:50:52 +01:00
adman 41d1ab4c45 performance 2026-03-24 17:46:23 +01:00
adman 032fa26927 fix some race conditions 2026-03-22 22:51:29 +01:00
24 changed files with 603 additions and 278 deletions
+64 -37
View File
@@ -9,32 +9,40 @@ import (
"strings"
"time"
"github.com/admin-else/strom/crypto"
"github.com/google/uuid"
)
type Servers struct {
ServicesHost string
SessionHost string
ProfilesHost string
}
var DefaultServers = &Servers{
ServicesHost: "https://api.minecraftservices.com",
SessionHost: "https://sessionserver.mojang.com",
ProfilesHost: "https://api.mojang.com",
}
type Account struct {
*Servers
Name string
Uuid uuid.UUID
Ygg string
}
const (
BaseUrlApi = "https://api.minecraftservices.com"
)
func (a *Account) doMcApiRequest(method string, url string, from any, to any) (err error) {
var b []byte
if method != "GET" {
b, err = json.Marshal(from)
if err != nil {
return err
return
}
}
r, err := http.NewRequest(method, url, bytes.NewReader(b))
if err != nil {
return err
return
}
r.Header.Set("Content-Type", "application/json")
@@ -44,21 +52,22 @@ func (a *Account) doMcApiRequest(method string, url string, from any, to any) (e
resp, err := http.DefaultClient.Do(r)
if err != nil {
return err
return
}
defer resp.Body.Close()
if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
return fmt.Errorf("bad status code %v", resp.StatusCode)
}
b, err = io.ReadAll(resp.Body)
if err != nil {
return err
return
}
err = json.Unmarshal(b, to)
if err != nil {
return err
return
}
return nil
return
}
type PlayerKeys struct {
@@ -73,7 +82,7 @@ type PlayerKeys struct {
}
func (a *Account) FetchKeys() (keys PlayerKeys, err error) {
err = a.doMcApiRequest("POST", BaseUrlApi+"/player/certificates", nil, &keys)
err = a.doMcApiRequest("POST", a.ServicesHost+"/player/certificates", nil, &keys)
return
}
@@ -90,10 +99,12 @@ func (a *Account) JoinServer(serverId string) (err error) {
if err != nil {
return
}
resp, err := http.Post("https://sessionserver.mojang.com/session/minecraft/join", "application/json", bytes.NewReader(body))
resp, err := http.Post(a.SessionHost+"/session/minecraft/join", "application/json", bytes.NewReader(body))
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
body, err = io.ReadAll(resp.Body)
if err != nil {
@@ -112,17 +123,10 @@ type NameChangResp struct {
}
func (a *Account) NameChangeInfo() (r NameChangResp, err error) {
err = a.doMcApiRequest("GET", "https://api.minecraftservices.com/minecraft/profile/namechange", nil, &r)
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile/namechange", nil, &r)
return
}
func NewOfflineAccount(name string) *Account {
return &Account{
Name: name,
Uuid: crypto.FromOfflinePlayer(name),
}
}
// PlayerProfile represents the player's profile information
type PlayerProfile struct {
ID string `json:"id"`
@@ -143,7 +147,7 @@ type PlayerProfile struct {
// QueryProfile queries the player's profile
func (a *Account) QueryProfile() (profile PlayerProfile, err error) {
err = a.doMcApiRequest("GET", BaseUrlApi+"/minecraft/profile", nil, &profile)
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile", nil, &profile)
return
}
@@ -181,7 +185,7 @@ type PlayerAttributes struct {
// QueryAttributes queries the player's attributes
func (a *Account) QueryAttributes() (attrs PlayerAttributes, err error) {
err = a.doMcApiRequest("GET", BaseUrlApi+"/player/attributes", nil, &attrs)
err = a.doMcApiRequest("GET", a.ServicesHost+"/player/attributes", nil, &attrs)
return
}
@@ -194,7 +198,7 @@ type ModifyAttributesRequest struct {
// ModifyAttributes modifies the player's attributes
func (a *Account) ModifyAttributes(req ModifyAttributesRequest) (attrs PlayerAttributes, err error) {
err = a.doMcApiRequest("POST", BaseUrlApi+"/player/attributes", req, &attrs)
err = a.doMcApiRequest("POST", a.ServicesHost+"/player/attributes", req, &attrs)
return
}
@@ -205,24 +209,23 @@ type BlockedUsers struct {
// GetBlocklist gets the list of blocked users
func (a *Account) GetBlocklist() (blocklist BlockedUsers, err error) {
err = a.doMcApiRequest("GET", BaseUrlApi+"/privacy/blocklist", nil, &blocklist)
err = a.doMcApiRequest("GET", a.ServicesHost+"/privacy/blocklist", nil, &blocklist)
return
}
// CheckNameAvailability checks if a name is available
type NameAvailability struct {
Status string `json:"status"` // DUPLICATE, AVAILABLE, NOT_ALLOWED
}
// CheckNameAvailability checks if a name is available
func (a *Account) CheckNameAvailability(name string) (avail NameAvailability, err error) {
err = a.doMcApiRequest("GET", BaseUrlApi+"/minecraft/profile/name/"+name+"/available", nil, &avail)
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile/name/"+name+"/available", nil, &avail)
return
}
// ChangeName changes the player's name
func (a *Account) ChangeName(newName string) (profile PlayerProfile, err error) {
err = a.doMcApiRequest("PUT", BaseUrlApi+"/minecraft/profile/name/"+newName, nil, &profile)
err = a.doMcApiRequest("PUT", a.ServicesHost+"/minecraft/profile/name/"+newName, nil, &profile)
return
}
@@ -234,19 +237,19 @@ type ChangeSkinRequest struct {
// ChangeSkin changes the player's skin
func (a *Account) ChangeSkin(req ChangeSkinRequest) (profile PlayerProfile, err error) {
err = a.doMcApiRequest("POST", BaseUrlApi+"/minecraft/profile/skins", req, &profile)
err = a.doMcApiRequest("POST", a.ServicesHost+"/minecraft/profile/skins", req, &profile)
return
}
// ResetSkin resets the player's skin to default
func (a *Account) ResetSkin() (profile PlayerProfile, err error) {
err = a.doMcApiRequest("DELETE", BaseUrlApi+"/minecraft/profile/skins/active", nil, &profile)
err = a.doMcApiRequest("DELETE", a.ServicesHost+"/minecraft/profile/skins/active", nil, &profile)
return
}
// HideCape hides the player's active cape
func (a *Account) HideCape() (profile PlayerProfile, err error) {
err = a.doMcApiRequest("DELETE", BaseUrlApi+"/minecraft/profile/capes/active", nil, &profile)
err = a.doMcApiRequest("DELETE", a.ServicesHost+"/minecraft/profile/capes/active", nil, &profile)
return
}
@@ -258,29 +261,53 @@ type ShowCapeRequest struct {
// ShowCape shows a specific cape
func (a *Account) ShowCape(capeID string) (profile PlayerProfile, err error) {
req := ShowCapeRequest{CapeID: capeID}
err = a.doMcApiRequest("PUT", BaseUrlApi+"/minecraft/profile/capes/active", req, &profile)
err = a.doMcApiRequest("PUT", a.ServicesHost+"/minecraft/profile/capes/active", req, &profile)
return
}
// CheckGiftCodeValidity checks if a gift code is valid
func (a *Account) CheckGiftCodeValidity(giftCode string) (valid bool, err error) {
url := BaseUrlApi + "/productvoucher/giftcode"
url := a.ServicesHost + "/productvoucher/" + giftCode
r, err := http.NewRequest("GET", url, nil)
if err != nil {
return false, err
return
}
r.Header.Set("Authorization", "Bearer "+a.Ygg)
resp, err := http.DefaultClient.Do(r)
if err != nil {
return false, err
return
}
defer resp.Body.Close()
// Returns HTTP 200 or 204 if valid, 404 if invalid
if resp.StatusCode == 200 || resp.StatusCode == 204 {
return true, nil
valid = true
return
}
return
}
func NewAccountFromYGG(ygg string) (a *Account, err error) {
a = &Account{
Ygg: ygg,
Servers: DefaultServers,
}
p, err := a.QueryProfile()
if err != nil {
return
}
a.Name = p.Name
a.Uuid, err = uuid.Parse(strings.ReplaceAll(p.ID, "-", ""))
return
}
func NewAccount(name string, uuid uuid.UUID, ygg string) (a *Account) {
return &Account{
Name: name,
Uuid: uuid,
Ygg: ygg,
Servers: DefaultServers,
}
return false, nil
}
+10
View File
@@ -0,0 +1,10 @@
package api
import "github.com/admin-else/strom/crypto"
func NewOfflineAccount(name string) *Account {
return &Account{
Name: name,
Uuid: crypto.FromOfflinePlayer(name),
}
}
-3
View File
@@ -20,9 +20,6 @@ func (c *ConfigIgnorer) OnStart() (err error) {
}
func (c *ConfigIgnorer) Default(e event.Anything) (err error) {
if e.Val == (event.Tick{}) {
return
}
slog.Debug("Config ignorer", "packet", fmt.Sprintf("%#v", e))
return
}
+1
View File
@@ -13,6 +13,7 @@ func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (p *v1_21_8.
return MakeHandshakePacketAddr(c, nextState, c.RemoteAddr().String())
}
// MakeHandshakePacketAddr gets a custom addr
func MakeHandshakePacketAddr(s *proto.Conn, nextState proto_base.State, addr string) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) {
p = &v1_21_8.HandshakingToServerPacketSetProtocol{NextState: int32(nextState), ProtocolVersion: s.ProtocolVersion}
var portStr string
+16 -8
View File
@@ -46,10 +46,6 @@ func (s *LoginClient) OnDefault(event event.Unhandled) (err error) {
return
}
func (s *LoginClient) OnCycle(_ event.Tick) (err error) {
return
}
func (s *LoginClient) OnClose(_ event.Close) (err error) {
return
}
@@ -127,13 +123,12 @@ func (s *LoginClient) OnStart() (err error) {
return
}
func Login(c *proto.Conn, account *api.Account) (err error) {
func LoginRaw(c *proto.Conn, account *api.Account, addr string) (err error) {
lc := &LoginClient{
Conn: c,
Account: account,
}
lc.RegisterCritical(lc.OnDefault)
lc.RegisterCritical(lc.OnCycle)
lc.RegisterCritical(lc.OnClose)
lc.RegisterUntilLatest(lc.OnCompress)
lc.RegisterUntilLatest(lc.OnEncrypt)
@@ -159,11 +154,24 @@ func Login(c *proto.Conn, account *api.Account) (err error) {
return
}
func ConnectAndLogin(connectTo string, account *api.Account) (c *proto.Conn, err error) {
func LoginNoSrv(connectTo string, account *api.Account) (c *proto.Conn, err error) {
c, err = Connect(connectTo)
if err != nil {
return
}
err = Login(c, account)
err = LoginRaw(c, account, connectTo)
return
}
func Login(connectTo string, account *api.Account) (c *proto.Conn, err error) {
connectTo, err = DoDnsSimple(connectTo)
if err != nil {
return
}
c, err = Connect(connectTo)
if err != nil {
return
}
err = LoginRaw(c, account, connectTo)
return
}
+13 -2
View File
@@ -37,7 +37,8 @@ func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
}
// 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.
func StatusRaw(addr string) (s *StatusClient, err error) {
c, err := ConnectVersionLess(addr)
if err != nil {
@@ -69,7 +70,8 @@ func StatusRaw(addr string) (s *StatusClient, err error) {
return
}
func Status(addr string) (status server.StatusResponse, err error) {
// StatusNoDns is like Status but does not resolve SRV records.
func StatusNoDns(addr string) (status server.StatusResponse, err error) {
s, err := StatusRaw(addr)
if err != nil {
return
@@ -77,3 +79,12 @@ func Status(addr string) (status server.StatusResponse, err error) {
err = json.Unmarshal([]byte(s.Status), &status)
return
}
// Status resolves the status of the minecraft server at the given address.
func Status(addr string) (status server.StatusResponse, err error) {
addr, err = DoDnsSimple(addr)
if err != nil {
return
}
return StatusNoDns(addr)
}
+6 -4
View File
@@ -8,18 +8,20 @@ import (
"os"
"slices"
"github.com/admin-else/strom/tools/cli/offline_uuid"
"github.com/admin-else/strom/tools/cli/print_nbt"
"github.com/admin-else/strom/tools/cli/status"
"github.com/admin-else/strom/cmd/strom/offline_uuid"
"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/status"
)
var ExpectedASubcommandErr = errors.New("expected a subcommand")
var UnknownSubcommandErr = errors.New("unknown subcommand")
var UnknownSubcommandErr = errors.New("unknown subcommand try 'strom help' for a list of subcommands")
var subcommands = map[string]func(args []string) error{
"offline-uuid": offline_uuid.Run,
"print-nbt": print_nbt.Run,
"status": status.Run,
"packet-spy": packet_inspector.Run,
}
func Help(args []string) (err error) {
+129
View File
@@ -0,0 +1,129 @@
package packet_inspector
import (
"flag"
"log/slog"
"github.com/admin-else/strom/api"
"github.com/admin-else/strom/client"
"github.com/admin-else/strom/event"
"github.com/admin-else/strom/nbt"
"github.com/admin-else/strom/proto"
"github.com/admin-else/strom/proto_base"
"github.com/admin-else/strom/proto_generated/v1_21_11"
"github.com/admin-else/strom/server"
"github.com/admin-else/strom/text"
)
var Cmd = flag.NewFlagSet("packet-inspector", flag.ContinueOnError)
var OfflineNameFlag = Cmd.String("name", "packet_inspector", "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")
type Proxy struct {
Client, Servee *proto.Conn
Data nbt.Tag
}
func (p *Proxy) Start() (err error) {
p.Client.RegisterCritical(p.OnAnything)
p.Servee.RegisterCritical(p.OnAnything)
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) OnAnything(e event.Anything) (err error) {
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
if !ok {
return
}
packetInfo, ok := proto.LookupPacketInfoByType(packet)
if !ok {
return
}
switch packetInfo.Direction {
case proto_base.ToServer:
err = p.Client.Send(packet)
case proto_base.ToClient:
err = p.Servee.Send(packet)
}
return
}
func (p *Proxy) OnFinishConfiguration(packet *v1_21_11.ConfigurationToServerPacketFinishConfiguration) (err error) {
err = p.Client.Send(packet)
if err != nil {
return
}
p.Servee.SetState(proto_base.Play)
p.Client.SetState(proto_base.Play)
err = event.DontForwardErr
return
}
var StatusResponse = server.StatusResponse{
Version: server.StatusResponseVersion{
Name: text.Pretty("STROM"),
Protocol: 772,
},
Players: server.StatusResponsePlayers{},
Description: text.Pretty("Packet spy forwards to " + *TargetAddr),
Favicon: "",
EnforcesSecureChat: false,
}
func Run(args []string) (err error) {
err = Cmd.Parse(args)
if err != nil {
return
}
slog.SetLogLoggerLevel(slog.LevelDebug)
slog.Info("Starting packet inspector", "listen", *ListenAddr)
err = server.StartServerWithOnConn(*ListenAddr, func(serveeConn *proto.Conn) (err error) {
serveeConn.DecodeEvenIfUnused = true
p := &Proxy{Servee: serveeConn, Client: nil}
var 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))
if err != nil {
return
}
*TargetAddr, err = client.DoDnsSimple(*TargetAddr)
if err != nil {
return
}
c, err := client.Connect(*TargetAddr)
if err != nil {
return
}
c.DecodeEvenIfUnused = true
err = client.LoginRaw(c, acc, *TargetAddr)
if err != nil {
return
}
defer c.Close()
p.Client = c
err = p.Start()
return
})
return
}
+10 -5
View File
@@ -53,6 +53,7 @@ type BlockState struct {
}
var BlocksCache = make(map[string][]*Block)
var BlocksCacheIdMap = make(map[string]map[int32]*Block)
func BlocksForVersion(v string) (ret []*Block) {
var ok bool
@@ -66,13 +67,17 @@ func BlocksForVersion(v string) (ret []*Block) {
}
func LookupBlockByStateId(version string, stateId int32) (block *Block, ok bool) {
for _, b := range BlocksForVersion(version) {
if b.MinStateId <= stateId && stateId <= b.MaxStateId {
block = b
ok = true
return
idmap, found := BlocksCacheIdMap[version]
if !found {
idmap = make(map[int32]*Block)
for _, b := range BlocksForVersion(version) {
for i := range b.MaxStateId - b.MinStateId + 1 {
idmap[b.MinStateId+i] = b
}
}
BlocksCacheIdMap[version] = idmap
}
block, ok = idmap[stateId]
return
}
+8
View File
@@ -1,5 +1,7 @@
package data
import "sync"
// {
// "id": 2,
// "name": "granite",
@@ -15,15 +17,21 @@ type Item struct {
}
var ItemsCache = make(map[string][]*Item)
var ItemsCacheMutex = &sync.RWMutex{}
func ItemsForVersion(v string) (ret []*Item) {
var ok bool
ItemsCacheMutex.RLock()
if ret, ok = ItemsCache[v]; ok {
ItemsCacheMutex.RUnlock()
return
}
ItemsCacheMutex.RUnlock()
var b []*Item
must(LoadVersionedJson(v, "items", &b))
ItemsCacheMutex.Lock()
ItemsCache[v] = b
ItemsCacheMutex.Unlock()
return b
}
+95 -30
View File
@@ -7,20 +7,14 @@ import (
"fmt"
"log/slog"
"reflect"
"time"
)
const (
LoopsPerSecond = 100 // i dunno seems reasonable
TimePerLoop = time.Second / LoopsPerSecond
)
type Loop struct {
HandlerFunctions map[reflect.Type][]reflect.Value
Ctx context.Context
ErrChan chan error
cancel context.CancelFunc
Ctx context.Context
cancel context.CancelFunc
eventSources []reflect.SelectCase
}
type Unhandled struct {
@@ -31,18 +25,22 @@ type Anything struct {
Val any
}
type (
Tick struct{}
)
// Close is fired when the loop shuts down.
type Close struct {
Reason error
}
// HandlerDoneErr will stop the event loop and return the error if provided.
type HandlerDoneErr struct {
Return error
}
// Close is fired when the loop shuts down.
type Close struct {
Reason error
// CallFunc is a special value that when fired with Loop.Fire it calls a function on the main loop
// Its usefull for event event sources to call functions on the main loop
// because event sources dont run on the main loop
type CallFunc struct {
F reflect.Value
Args []reflect.Value
}
func (e HandlerDoneErr) Error() string {
@@ -53,12 +51,27 @@ func (e HandlerDoneErr) Unwrap() error {
return e.Return
}
// MainHandlerErr is a special error used to tell the event loop that the main loop has exited
type MainHandlerErr struct {
Return error
}
func (e MainHandlerErr) Error() string {
return "MainHandlerErr"
}
func (e MainHandlerErr) Unwrap() error {
return e.Return
}
// DontForwardErr will stop the event from being forwarded to other handlers, including anything handlers.
var DontForwardErr = errors.New("dont forward")
var WhileClosingErr = errors.New("error while closing")
// CloseNonCriticalErr will close even when the event is non-critical
var CloseNonCriticalErr = errors.New("close non-critical")
var ContextDoneErr = errors.New("context done")
var RecvNotOkErr = errors.New("recv not ok")
func ValidateHandler(h any) (eventType reflect.Type, hv reflect.Value) {
hv = reflect.ValueOf(h)
@@ -75,6 +88,29 @@ func ValidateHandler(h any) (eventType reflect.Type, hv reflect.Value) {
return
}
// RegisterEventSource registers an event source an event source is a channel this channel along with all other event sources will be reflect.Select 'ed until the loop closes
// here is an example of an event source function
//
// func() (chSending <-chan any) {
// ch := make(chan any)
// go func() {
// ctx := conn.Ctx
// for {
// select {
// case <-ctx.Done():
// return
// case eventt := <-otherSource:
// err = c.DoCommand(eventt)
// }
// }
// }()
// chSending = ch
// return
// }
func (l *Loop) RegisterEventSource(ch <-chan any) {
l.eventSources = append(l.eventSources, reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)})
}
// Register registers a handler that will not close the loop if it returns an error.
// Instead, it will log it to slog.Error
// Warning: If a bad handler is passed in the program will panic.
@@ -111,6 +147,10 @@ func (l *Loop) RegisterDirect(k reflect.Type, h reflect.Value) {
l.HandlerFunctions[k] = append(l.HandlerFunctions[k], h)
}
func (l *Loop) RegisterIgnore(t any) {
l.RegisterDirect(reflect.TypeOf(t), reflect.ValueOf(func(any) error { return nil }))
}
// FireFound triggers all handlers registered for the specified event type and returns if handlers were found or an error occurred.
func (l *Loop) FireFound(event any) (found bool, err error) {
var handlers []reflect.Value
@@ -119,7 +159,7 @@ func (l *Loop) FireFound(event any) (found bool, err error) {
errV := handler.Call([]reflect.Value{reflect.ValueOf(event)})[0]
if !errV.IsNil() {
err = errV.Interface().(error)
err = errors.Join(fmt.Errorf("event handler failed at: %s(%T)", handler.Type().In(0), event), err)
err = errors.Join(fmt.Errorf("event handler failed at: %T", event), err)
return
}
}
@@ -129,6 +169,21 @@ func (l *Loop) FireFound(event any) (found bool, err error) {
// Fire dispatches the provided event to all appropriate handlers and invokes fallback handlers if the event is unhandled.
func (l *Loop) Fire(event any) (err error) {
err, isErr := event.(error)
if isErr {
return
}
err = nil
f, isCallFunc := event.(CallFunc)
if isCallFunc {
errV := f.F.Call(f.Args)[0].Interface()
if errV != nil {
err = errV.(error)
}
return
}
found, err := l.FireFound(event)
if err != nil {
if errors.Is(err, DontForwardErr) {
@@ -146,15 +201,35 @@ func (l *Loop) Fire(event any) (err error) {
return
}
func (l *Loop) startLoop() (err error) {
func (l *Loop) ContextDoneListener() (chSending <-chan any) {
ch := make(chan any)
go func() {
select {
case <-l.Ctx.Done():
ch <- ContextDoneErr
}
}()
return ch
}
func (l *Loop) IsTypeRegistered(t reflect.Type) bool {
_, ok := l.HandlerFunctions[t]
return ok
}
func (l *Loop) StartLoop() (err error) {
_ = *l // exit early on nil loop
l.RegisterEventSource(l.ContextDoneListener())
for {
tickStartTime := time.Now()
err = l.Fire(Tick{})
_, v, ok := reflect.Select(l.eventSources)
if !ok {
err = RecvNotOkErr
break
}
err = l.Fire(v.Interface())
if err != nil {
break
}
time.Sleep(TimePerLoop - time.Since(tickStartTime))
}
if errors.Is(err, HandlerDoneErr{}) {
err = errors.Unwrap(err)
@@ -166,18 +241,8 @@ func (l *Loop) startLoop() (err error) {
return
}
func (l *Loop) StartLoop() (err error) {
go func() {
l.ErrChan <- l.startLoop()
}()
err = <-l.ErrChan
l.cancel()
return
}
func NewLoop() (l *Loop) {
l = &Loop{}
l.ErrChan = make(chan error)
l.Ctx, l.cancel = context.WithCancel(context.Background())
l.HandlerFunctions = make(map[reflect.Type][]reflect.Value)
return
+21 -32
View File
@@ -2,7 +2,6 @@ package event
import (
"bufio"
"context"
"os"
)
@@ -10,36 +9,26 @@ type Stdin struct {
Val string
}
type StdinHandler struct {
*Loop
stdInChan chan Stdin
Ctx context.Context
}
func (s *StdinHandler) OnTick(_ Tick) (err error) {
select {
case stdin := <-s.stdInChan:
err = s.Fire(Stdin{stdin.Val})
default:
}
return
}
func (s *StdinHandler) ScanJob() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
select {
case <-s.Ctx.Done():
return
default:
s.stdInChan <- Stdin{scanner.Text()}
}
}
}
func (s *StdinHandler) OnStart() (err error) {
s.RegisterCritical(s.OnTick)
s.stdInChan = make(chan Stdin)
go s.ScanJob()
func StartListingStdin(l *Loop) {
l.RegisterEventSource(func() (chSending <-chan any) {
ch := make(chan any)
go func() {
scanner := bufio.NewScanner(os.Stdin)
ctx := l.Ctx
for scanner.Scan() {
select {
case <-ctx.Done():
return
default:
ch <- Stdin{scanner.Text()}
}
}
if err := scanner.Err(); err != nil {
ch <- err
}
}()
chSending = ch
return
}())
return
}
+61
View File
@@ -1,6 +1,8 @@
package event
import (
"context"
"reflect"
"slices"
"time"
)
@@ -48,6 +50,45 @@ func (t *Timer) Tick() (err error) {
return
}
func (t *Timer) Trigger(ch chan any) {
triggerAt := time.Time{}
found := false
iOnce, iInterval := -1, -1
for i, oTask := range t.Once {
if oTask.Trigger.Before(triggerAt) || triggerAt.IsZero() {
found = true
triggerAt = oTask.Trigger
iOnce = i
}
}
for i, iTask := range t.Interval {
if iTask.LastTrigger.Add(iTask.Interval).Before(triggerAt) || triggerAt.IsZero() {
found = true
triggerAt = iTask.LastTrigger.Add(iTask.Interval)
iOnce = -1
iInterval = i
}
}
if !found {
return
}
now := time.Now()
time.Sleep(triggerAt.Sub(now))
if iOnce >= 0 {
ch <- CallFunc{F: reflect.ValueOf(t.Once[iOnce].F), Args: []reflect.Value{}}
t.Once = slices.Delete(t.Once, iOnce, iOnce+1)
return
}
if iInterval >= 0 {
ch <- CallFunc{F: reflect.ValueOf(t.Interval[iInterval].F), Args: []reflect.Value{}}
t.Interval[iInterval].LastTrigger = now
return
}
panic("unreachable")
}
func (t *Timer) Every(interval time.Duration, f func() error) {
t.Interval = append(t.Interval, IntervalTask{
F: f,
@@ -61,3 +102,23 @@ func (t *Timer) In(interval time.Duration, f func() error) {
Trigger: time.Now().Add(interval),
})
}
func (t *Timer) MakeEventSource(ctx context.Context) (chSending <-chan any) {
ch := make(chan any)
go func() {
for {
select {
case <-ctx.Done():
return
default:
t.Trigger(ch)
}
}
}()
chSending = ch
return
}
func (t *Timer) Start(c *Loop) {
c.RegisterEventSource(t.MakeEventSource(c.Ctx))
}
+5 -5
View File
@@ -6,6 +6,7 @@ import (
"math"
"os"
"strings"
"unsafe"
"github.com/admin-else/strom/data"
"github.com/admin-else/strom/level"
@@ -42,10 +43,9 @@ type Region struct {
}
func (r *Region) GetBlockAt(x, y, z int32) (name string, properties map[string]string, err error) {
uintSlice := make([]uint64, len(r.BlockStates))
for i, v := range r.BlockStates {
uintSlice[i] = uint64(v)
}
// this is probably fine
uintSlice := unsafe.Slice((*uint64)(unsafe.Pointer(unsafe.SliceData(r.BlockStates))), len(r.BlockStates))
if r.Size.X < 0 {
r.Size.X *= -1
}
@@ -56,7 +56,7 @@ func (r *Region) GetBlockAt(x, y, z int32) (name string, properties map[string]s
r.Size.Z *= -1
}
if x >= r.Size.X || y >= r.Size.Y || z >= r.Size.Z {
if x >= r.Size.X || y >= r.Size.Y || z >= r.Size.Z || x < 0 || y < 0 || z < 0 {
err = OutOfBoundsErr
return
}
+55 -56
View File
@@ -15,17 +15,24 @@ import (
"github.com/admin-else/strom/proto_generated"
)
const (
BufferNPackets = 100
)
var (
BadPacketTypeErr = errors.New("bad packet type")
BadPacketIdErr = errors.New("bad packet id")
PacketNotFullyDecodedErr = errors.New("packet not fully decoded")
ContextDoneErr = errors.New("context done")
WrongStateErr = errors.New("wrong state")
WrongDirectionErr = errors.New("wrong direction")
CantTranslateErr = errors.New("cant translate packet")
PacketDoesntExistInFutureVersionErr = errors.New("packet doesnt exist in future version")
NoHandlerRegisteredErr = errors.New("no handler registered")
)
// LoginTick is used as a subsite for the old event system because it is very useful in login contexts
type LoginTick struct{}
var LatestVersion = proto_generated.SupportedVersions[len(proto_generated.SupportedVersions)-1]
// UnCodablePacket represents a packet that could not be decoded.
@@ -48,41 +55,34 @@ func (u *UnCodablePacket) Decode(r io.Reader) (err error) {
type Conn struct {
RawConn
*event.Loop
state proto_base.State
stateMutex sync.RWMutex
Actor proto_base.Actor
Version string
ProtocolVersion int32
state proto_base.State
stateMutex sync.RWMutex
Actor proto_base.Actor
Version string
ProtocolVersion int32
DecodeEvenIfUnused bool
receiveRoutine bool
packetCh chan proto_base.EncodeDecodeAble
sendTickAlways bool
}
func (c *Conn) StartConn() (err error) {
c.RegisterCritical(c.OnTick)
if c.state == proto_base.Play {
c.ActivateReceiveRoutine()
}
c.sendTickAlways = c.State() != proto_base.Play
c.RegisterEventSource(c.ActivateReceiveRoutine())
c.RegisterCritical(c.OnLoginTick)
err = c.Loop.StartLoop()
c.Loop = event.NewLoop()
return
}
func (c *Conn) ReceiveJob() {
ctx := c.Ctx
for {
select {
case <-ctx.Done():
return
default:
packet, err := c.Receive()
if err != nil {
c.ErrChan <- err
return
}
c.packetCh <- packet
}
func (c *Conn) OnLoginTick(_ LoginTick) (err error) {
packet, err := c.Receive()
if err != nil {
return
}
slog.Debug("logintick recv", "actor", c.Actor, "packet", fmt.Sprintf("%#v", packet))
err = c.Fire(packet)
return
}
func (c *Conn) SetState(state proto_base.State) {
@@ -119,13 +119,31 @@ func (c *Conn) SetProtocolVersion(version int32) (err error) {
// ActivateReceiveRoutine activates the receive-routine.
// It ensures that the receive-routine is only started once and creates a buffered channel for packet handling.
func (c *Conn) ActivateReceiveRoutine() {
if c.receiveRoutine {
return
}
c.packetCh = make(chan proto_base.EncodeDecodeAble, 100) // i dunno how many to buffer 100 seems reasonable
go c.ReceiveJob()
c.receiveRoutine = true
func (c *Conn) ActivateReceiveRoutine() (chSending <-chan any) {
ch := make(chan any, BufferNPackets)
go func() {
ctx := c.Ctx // this prevents c.Ctx being reassigned
for {
select {
case <-ctx.Done():
return
default:
if c.sendTickAlways {
ch <- LoginTick{}
} else {
packet, err := c.Receive()
if err != nil {
ch <- err
return
}
slog.Debug("recv", "actor", c.Actor, "packet", fmt.Sprintf("%#v", packet))
ch <- packet
}
}
}
}()
chSending = ch
return
}
func (c *Conn) translatePacketVersion(packet proto_base.EncodeDecodeAble, packetInfo proto_base.PacketInfo) (convertedPacket proto_base.EncodeDecodeAble, err error) {
@@ -203,6 +221,10 @@ func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) {
packet = &UnCodablePacket{Err: BadPacketIdErr, Data: packetBytes, Direction: c.Actor.ReceiveDirection()}
return
}
if !c.DecodeEvenIfUnused && !c.IsTypeRegistered(reflect.TypeOf(packet)) {
packet = &UnCodablePacket{Err: NoHandlerRegisteredErr, Data: packetBytes, Direction: c.Actor.ReceiveDirection()}
return
}
err = packet.Decode(b)
if err != nil {
packet = &UnCodablePacket{Err: err, Data: packetBytes, Direction: c.Actor.ReceiveDirection()}
@@ -216,26 +238,3 @@ func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) {
}
return
}
func (c *Conn) OnTick(_ event.Tick) (err error) {
var packet proto_base.EncodeDecodeAble
if c.receiveRoutine {
select {
case packet = <-c.packetCh:
slog.Debug("receive", "actor", c.Actor, "packet", fmt.Sprintf("%#v", packet))
err = c.Loop.Fire(packet)
case <-c.Ctx.Done():
err = ContextDoneErr
return
default:
}
} else {
packet, err = c.Receive()
if err != nil {
return
}
slog.Debug("receive", "actor", c.Actor, "packet", fmt.Sprintf("%#v", packet))
err = c.Loop.Fire(packet)
}
return
}
+25 -6
View File
@@ -7,15 +7,27 @@ import (
"github.com/admin-else/strom/proto_generated"
)
// I know this is in-performant and i dont care until it becomes a problem for somebody it will stay like this and it is pretty easy to make caches or similar to speed this up so yeah
type PidEtc struct {
Direction proto_base.Direction
State proto_base.State
PacketId int32
ProtocolVersion int32
}
var pidMap = make(map[PidEtc]*proto_base.PacketInfo)
func init() {
for _, pin := range proto_generated.Packets {
pidMap[PidEtc{pin.Direction, pin.State, pin.PacketId, pin.ProtocolVersion}] = &pin
}
}
func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.State, pid, version int32) (p proto_base.PacketInfo, ok bool) {
for _, pin := range proto_generated.Packets {
if pin.Direction != direction || pin.State != state || pin.ProtocolVersion != version || pin.PacketId != pid {
continue
}
return pin, true
pP, ok := pidMap[PidEtc{direction, state, pid, version}]
if !ok {
return
}
p = *pP
return
}
@@ -29,6 +41,13 @@ func LookUpTypeByPacketInfoAndCopyType(direction proto_base.Direction, state pro
}
func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (p proto_base.PacketInfo, ok bool) {
undecodedPacket, isUndecoable := packet.(*UnCodablePacket)
if isUndecoable {
p = proto_base.PacketInfo{Type: &UnCodablePacket{}, Name: "un_codable", Direction: undecodedPacket.Direction, State: proto_base.Handshaking, PacketId: -1, ProtocolVersion: -1}
ok = true
return
}
for _, p := range proto_generated.Packets {
if reflect.TypeOf(p.Type) == reflect.TypeOf(packet) {
return p, true
+3 -1
View File
@@ -63,6 +63,7 @@ func (l *LoginServer) OnHandshake(packet *v1_21_8.HandshakingToServerPacketSetPr
return
}
err = l.SetProtocolVersion(packet.ProtocolVersion)
return
}
@@ -159,6 +160,7 @@ func WithCompatibleVersions(versions ...int32) LoginServerSetting {
func ServeLogin(c *proto.Conn, settings ...LoginServerSetting) (ret *LoginServer, err error) {
ret = &LoginServer{Conn: c}
ret.RegisterCritical(ret.OnDefault)
ret.RegisterIgnore(event.Close{})
ret.RegisterUntilLatest(ret.OnHandshake)
ret.RegisterUntilLatest(ret.OnLoginStart)
ret.RegisterUntilLatest(ret.OnLoginAcknowledged)
@@ -168,6 +170,6 @@ func ServeLogin(c *proto.Conn, settings ...LoginServerSetting) (ret *LoginServer
s(ret)
}
ret.CompressionThreshold = 256
err = ret.StartLoop()
err = ret.StartConn()
return
}
+2
View File
@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"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"
@@ -19,6 +20,7 @@ func Servee(c net.Conn) (ret *proto.Conn) {
ret.Conn = c
ret.R = c
ret.W = c
ret.Loop = event.NewLoop()
return
}
@@ -0,0 +1,18 @@
package failed_packets_test
import (
"bytes"
"testing"
"github.com/admin-else/strom/proto_generated/v1_21_8"
)
// Error: %v
func TestFailedPacket%10X(t *testing.T) {
p := v1_21_8.Play%vPacket{}
b := bytes.NewBuffer(%#v)
err := p.Decode(b)
if err != nil {
t.Fatal(err)
}
}
+53 -89
View File
@@ -2,6 +2,7 @@ package main
import (
"crypto/sha256"
_ "embed"
"fmt"
"log/slog"
"os"
@@ -9,105 +10,76 @@ import (
"github.com/admin-else/strom/api"
"github.com/admin-else/strom/client"
"github.com/admin-else/strom/event"
"github.com/admin-else/strom/nbt"
"github.com/admin-else/strom/proto"
"github.com/admin-else/strom/proto_base"
"github.com/admin-else/strom/proto_generated/v1_21_8"
"github.com/admin-else/strom/proto_generated/v1_21_11"
"github.com/admin-else/strom/server"
"github.com/admin-else/strom/text"
)
type ProxyClient struct {
*proto.Conn
Server *Proxy
}
func (p *ProxyClient) OnDefault(event event.Unhandled) (err error) {
packet, isPacket := event.Val.(proto_base.EncodeDecodeAble)
if !isPacket {
return
}
err = p.Server.Send(packet)
return
}
func (p *ProxyClient) OnCycle(_ event.Tick) (err error) {
return
}
func (p *ProxyClient) OnStart() (err error) {
return
}
func (p *ProxyClient) OnUnCodeAble(packet *proto.UnCodablePacket) (err error) {
err = p.OnDefault(event.Unhandled{Val: packet})
if err != nil {
return
}
SaveUnCodeAbleAsTest(proto_base.ToServer, packet)
return
}
//go:embed failed_packet.go.tmpl
var TestSrcF string
type Proxy struct {
*proto.Conn
Client *ProxyClient
Client, Servee *proto.Conn
Data nbt.Tag
}
func (p *Proxy) OnDefault(event event.Unhandled) (err error) {
packet, isPacket := event.Val.(proto_base.EncodeDecodeAble)
if !isPacket {
func (p *Proxy) Start() (err error) {
p.Client.RegisterCritical(p.OnAnything)
p.Servee.RegisterCritical(p.OnAnything)
p.Servee.Register(p.OnUnCodeAble)
p.Client.Register(p.OnUnCodeAble)
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) OnAnything(e event.Anything) (err error) {
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
if !ok {
return
}
packetInfo, ok := proto.LookupPacketInfoByType(packet)
if !ok {
return
}
switch packetInfo.Direction {
case proto_base.ToServer:
err = p.Client.Send(packet)
case proto_base.ToClient:
err = p.Servee.Send(packet)
}
return
}
func (p *Proxy) OnFinishConfiguration(packet *v1_21_11.ConfigurationToServerPacketFinishConfiguration) (err error) {
err = p.Client.Send(packet)
return
}
func (p *Proxy) OnCycle(_ event.Tick) (err error) {
return
}
func (p *Proxy) OnStart() (err error) {
return
}
func (p *Proxy) OnFinishConfiguration(packet *v1_21_8.ConfigurationToServerPacketFinishConfiguration) (err error) {
err = p.OnDefault(event.Unhandled{Val: packet})
if err != nil {
return
}
p.SetState(proto_base.Play)
p.Servee.SetState(proto_base.Play)
p.Client.SetState(proto_base.Play)
err = event.DontForwardErr
return
}
func (p *Proxy) OnUnCodeAble(packet *proto.UnCodablePacket) (err error) {
err = p.OnDefault(event.Unhandled{Val: packet})
if err != nil {
return
}
SaveUnCodeAbleAsTest(proto_base.ToClient, packet)
SaveUnCodeAbleAsTest(packet.Direction, packet)
return
}
const TestSrcF = `package failed_packets_test
import (
"bytes"
"testing"
"github.com/admin-else/strom/proto_generated/v1_21_8"
)
// Error: %v
func TestFailedPacket%10X(t *testing.T) {
p := v1_21_8.Play%vPacket{}
b := bytes.NewBuffer(%#v)
err := p.Decode(b)
if err != nil {
t.Fatal(err)
}
}
`
func SaveUnCodeAbleAsTest(d proto_base.Direction, packet *proto.UnCodablePacket) {
hUntrimmed := sha256.Sum256(packet.Data)
h := hUntrimmed[:8]
@@ -138,28 +110,20 @@ var StatusResponse = server.StatusResponse{
func main() {
slog.SetLogLoggerLevel(slog.LevelDebug)
err := server.StartServerWithOnConn(":25565", func(serveeConn *proto.Conn) (err error) {
p := &Proxy{Conn: serveeConn, Client: nil}
err := server.StartServerWithOnConn(":25566", func(serveeConn *proto.Conn) (err error) {
p := &Proxy{Servee: serveeConn, Client: nil}
acc := api.NewOfflineAccount("sigma")
_, err = server.ServeLogin(serveeConn, server.WithOtherAccount(acc), server.WithStatus(StatusResponse))
if err != nil {
return
}
c, err := client.ConnectAndLogin("127.0.0.1:25566", acc)
c, err := client.LoginNoSrv("127.0.0.1:25565", acc)
if err != nil {
return
}
pc := &ProxyClient{Conn: c, Server: p}
p.Client = pc
errChan := make(chan error)
defer pc.Close()
go func() {
errChan <- pc.StartConn()
}()
go func() {
errChan <- p.StartConn()
}()
err = <-errChan
defer c.Close()
p.Client = c
err = p.Start()
return
})
if err != nil {
+8
View File
@@ -0,0 +1,8 @@
package util
import "math/rand/v2"
func ShuffleSlice[T any](s []T) (ret []T) {
rand.Shuffle(len(s), func(i, j int) { s[i], s[j] = s[j], s[i] })
return s
}