diff --git a/Dockerfile b/Dockerfile index cc75542..515ac41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,9 +54,6 @@ RUN cd /multi_server/public/play/games/default/ゆめ2っき/ && \ RUN /bin/bash -c 'mv /multi_server/public/play/games/default/ゆめ2っき/* /multi_server/public/play/games/default/' -COPY --from=0 /workdir/ynoclient/index.wasm /multi_server/public -COPY --from=0 /workdir/ynoclient/index.js /multi_server/public - COPY server/public /multi_server/public RUN mkdir -p /multi_server/public/data/default && \ diff --git a/server/internal/client/clientinfo.go b/server/internal/client/clientinfo.go index 1443f96..23c2f53 100644 --- a/server/internal/client/clientinfo.go +++ b/server/internal/client/clientinfo.go @@ -1,6 +1,7 @@ package client import ( + "fmt" "net" log "github.com/sirupsen/logrus" @@ -17,6 +18,11 @@ func (cid *ClientID) GetAddr() net.Addr { return cid.Conn.RemoteAddr() } +// Dumb +func (cid *ClientID) IsClosed() bool { + return cid.Conn.RemoteAddr() == nil || cid.Conn.RemoteAddr().String() == "" +} + func (cid *ClientID) GetUsername() string { return cid.Name } @@ -32,6 +38,7 @@ type Client interface { GetAddr() net.Addr GetUsername() string SetUsername(name string) + IsClosed() bool } type GameClient struct { @@ -54,8 +61,17 @@ func (gc *GameClient) Unignore(ipv4 net.Addr) { } func (gc *GameClient) Send(payload []byte, sender net.Addr) error { + // if sender == nil { + // return errors.New("Sender has disconnected") + // } + + if gc.ClientID.Conn.RemoteAddr() == nil { + fmt.Errorf("client's remote addr is nil, has likely already disconnected. Dropping message.") + return nil + } + // If the sender is the current user, just return - if gc.ClientID.Conn.RemoteAddr().String() == sender.String() { + if sender != nil && gc.ClientID.Conn.RemoteAddr().String() == sender.String() { return nil } diff --git a/server/internal/client/clientmanager.go b/server/internal/client/clientmanager.go index 73e5fa4..d49f1b4 100644 --- a/server/internal/client/clientmanager.go +++ b/server/internal/client/clientmanager.go @@ -131,7 +131,11 @@ func (cm *ClientPubsubManager) Broadcast(payload interface{}, sockinfo *ClientSo log.Debugf("Broadcasting for room %s", sockinfo.RoomName) for _, client := range clients { - err = client.ClientInfo.Send(payloadBytes, sockinfo.ClientInfo.GetAddr()) + c := sockinfo.ClientInfo + if c == nil { + // Skip? Disconnect? TODO + } + err = client.ClientInfo.Send(payloadBytes, c.GetAddr()) if err != nil { log.Errorf("Failed to send to client. Err: %s. Continuing...", err) } diff --git a/server/internal/client/syncobject.go b/server/internal/client/syncobject.go index a5ee90b..f169f66 100644 --- a/server/internal/client/syncobject.go +++ b/server/internal/client/syncobject.go @@ -1,10 +1,15 @@ package client -import guuid "github.com/google/uuid" +import ( + "log" + "strconv" +) + +var serial int = 0 type Position struct { - X uint16 - Y uint16 + X uint16 `json:"x"` + Y uint16 `json:"y"` } // return {id: data.readUInt32LE(2), value: data.readUInt32LE(6)}; @@ -22,7 +27,7 @@ type Sound struct { type Sprite struct { ID uint16 `json:"id"` - Sheet uint32 `json:"sheet"` + Sheet string `json:"sheet"` } // return {frame: data.readUInt16LE(2)}; @@ -43,30 +48,31 @@ type Weather struct { } type SyncObject struct { - UID string `json:"uid"` // Actually a UUID + Type string `json:"type"` + UID string `json:"uid"` // Actually a UUID - Pos Position `json:"pos,omitempty"` + Pos *Position `json:"pos,omitempty"` posChanged bool - Sprite Sprite `json:"sprite,omitempty"` + Sprite *Sprite `json:"sprite,omitempty"` spriteChanged bool - Weather Weather `json:"weather,omitempty"` + Weather *Weather `json:"weather,omitempty"` weatherChanged bool - Variable Variable `json:'variable,omitempty"` + Variable *Variable `json:"variable,omitempty"` variableChanged bool - Sound Sound `json:"sound,omitempty"` + Sound *Sound `json:"sound,omitempty"` soundChanged bool Name string `json:"name,omitempty"` nameChanged bool - AnimFrame AnimFrame `json:"animframe,omitempty"` + AnimFrame uint16 `json:"animframe,omitempty"` animframeChanged bool - Switch Switch `json:"switch,omitempty"` + Switch *Switch `json:"switch,omitempty"` switchChanged bool MovementAnimationSpeed uint16 `json:"movementAnimationSpeed,omitempty"` @@ -80,39 +86,44 @@ type SyncObject struct { } func NewSyncObject() *SyncObject { - uuid := guuid.New() - return &SyncObject{UID: uuid.String()} + serial += 1 + return &SyncObject{UID: strconv.Itoa(serial), Type: "objectSync", MovementAnimationSpeed: 4} } func (so *SyncObject) SetPos(x, y uint16) { so.posChanged = true - so.Pos = Position{X: x, Y: y} + so.Pos = &Position{X: x, Y: y} } -func (so *SyncObject) SetSprite(id uint16, sheet uint32) { +func (so *SyncObject) SetSprite(id uint16, sheet string) { + // "why do I have to do this?" + if id == 0 && sheet == "" { + return + } + so.Sprite = &Sprite{ID: id, Sheet: sheet} so.spriteChanged = true // TODO: sprite validation goes here - so.Sprite = Sprite{ID: id, Sheet: sheet} + log.Printf("SPRITE CHANGED TO ID %s and SHEET %s", id, sheet) } func (so *SyncObject) SetSound(volume uint16, tempo uint16, balance uint16, name string) { so.soundChanged = true - so.Sound = Sound{Volume: volume, Tempo: tempo, Balance: balance, Name: name} + so.Sound = &Sound{Volume: volume, Tempo: tempo, Balance: balance, Name: name} } func (so *SyncObject) SetWeather(t, strength uint16) { so.weatherChanged = true - so.Weather = Weather{Type: t, Strength: strength} + so.Weather = &Weather{Type: t, Strength: strength} } func (so *SyncObject) SetSwitch(id, value uint32) { so.switchChanged = true - so.Switch = Switch{ID: id, Value: value} + so.Switch = &Switch{ID: id, Value: value} } func (so *SyncObject) SetAnimFrame(frame uint16) { so.animframeChanged = true - so.AnimFrame = AnimFrame{Frame: frame} + so.AnimFrame = frame } func (so *SyncObject) SetName(name string) { @@ -137,7 +148,7 @@ func (so *SyncObject) SetTypingStatus(typingStatus uint16) { func (so *SyncObject) SetVariable(id, value uint32) { so.variableChanged = true - so.Variable = Variable{ID: id, Value: value} + so.Variable = &Variable{ID: id, Value: value} } func (so *SyncObject) GetAllChanges() interface{} { @@ -160,7 +171,7 @@ func (so *SyncObject) clearChanges() { // So this is horrific BUT idk what to do about it lol func (so *SyncObject) GetFlushedChanges() interface{} { - s := SyncObject{UID: so.UID} + s := SyncObject{UID: so.UID, Type: so.Type} if so.posChanged { s.Pos = so.Pos @@ -190,19 +201,19 @@ func (so *SyncObject) GetFlushedChanges() interface{} { s.Name = so.Name } - if s.movementAnimationSpeedChanged { + if so.movementAnimationSpeedChanged { s.MovementAnimationSpeed = so.MovementAnimationSpeed } - if s.facingChanged { + if so.facingChanged { s.Facing = so.Facing } - if s.typingStatusChanged { + if so.typingStatusChanged { s.TypingStatus = so.TypingStatus } - if s.animframeChanged { + if so.animframeChanged { s.AnimFrame = so.AnimFrame } diff --git a/server/internal/clientmessages/client.go b/server/internal/clientmessages/client.go index c53afe2..5af80bf 100644 --- a/server/internal/clientmessages/client.go +++ b/server/internal/clientmessages/client.go @@ -72,7 +72,7 @@ type Movement struct { type Sprite struct { MatchPrefix string `ynoproto:"02"` SpriteID uint16 - Spritesheet uint32 + Spritesheet string } //uint16 packet type, uint16 volume, uint16 tempo, uint16 balance, string sound file @@ -99,42 +99,42 @@ type Name struct { //uint16 packet type, uint16 movement speed type MovementAnimationSpeed struct { - MatchPrefix string `ynoproto:"4"` + MatchPrefix string `ynoproto:"06"` MovementSpeed uint16 } //uint16 packet type, uint32 var id, int32 value type Variable struct { - MatchPrefix string `ynoproto:"5"` + MatchPrefix string `ynoproto:"07"` ID uint32 Value uint32 } //uint16 packet type, uint32 switch id, int32 value type SwitchSync struct { - MatchPrefix string `ynoproto:"6"` + MatchPrefix string `ynoproto:"08"` ID uint32 Value uint32 } //uint16 packet type, uint16 type type AnimType struct { - MatchPrefix string `ynoproto:"7"` + MatchPrefix string `ynoproto:"09"` Type uint16 } //uint16 packet type, uint16 frame type AnimFrame struct { - MatchPrefix string `ynoproto:"8"` + MatchPrefix string `ynoproto:"0A"` Frame uint16 } type Facing struct { - MatchPrefix string `ynoproto:"9"` + MatchPrefix string `ynoproto:"0B"` Facing uint16 } type TypingStatus struct { - MatchPrefix string `ynoproto:"10"` + MatchPrefix string `ynoproto:"0C"` TypingStatus uint16 } diff --git a/server/internal/msghandler/chathandler.go b/server/internal/msghandler/chathandler.go index 062c24d..6f2685d 100644 --- a/server/internal/msghandler/chathandler.go +++ b/server/internal/msghandler/chathandler.go @@ -66,7 +66,7 @@ func (ch *ChatHandler) muxMessage(payload []byte, c gnet.Conn, s *client.ClientS func (ch *ChatHandler) pardonChat(payload []byte, client *client.ClientSockInfo) error { t := clientmessages.UnignoreChatEvents{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, false) switch { case !matched: return errors.New("Failed to match") @@ -98,7 +98,7 @@ func (ch *ChatHandler) pardonChat(payload []byte, client *client.ClientSockInfo) func (ch *ChatHandler) ignoreChat(payload []byte, client *client.ClientSockInfo) error { t := clientmessages.IgnoreChatEvents{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, false) switch { case !matched: return errors.New("Failed to match") @@ -136,7 +136,7 @@ func (ch *ChatHandler) ignoreChat(payload []byte, client *client.ClientSockInfo) func (ch *ChatHandler) setUsername(payload []byte, client *client.ClientSockInfo) error { t := clientmessages.SetUsername{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, false) switch { case err != nil: return err @@ -160,7 +160,7 @@ func (ch *ChatHandler) setUsername(payload []byte, client *client.ClientSockInfo func (ch *ChatHandler) sendUserMessage(payload []byte, client *client.ClientSockInfo) error { t := clientmessages.SendMessage{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, false) switch { case err != nil: return err diff --git a/server/internal/msghandler/gamehandler.go b/server/internal/msghandler/gamehandler.go index f506e6a..7398f45 100644 --- a/server/internal/msghandler/gamehandler.go +++ b/server/internal/msghandler/gamehandler.go @@ -3,11 +3,13 @@ package msghandler import ( "errors" "fmt" + "sync" "time" "github.com/horahoradev/YNO10k/internal/client" "github.com/horahoradev/YNO10k/internal/clientmessages" "github.com/horahoradev/YNO10k/internal/protocol" + "github.com/horahoradev/YNO10k/internal/servermessages" "github.com/panjf2000/gnet" log "github.com/sirupsen/logrus" ) @@ -31,6 +33,7 @@ const ( type GameHandler struct { pubsubManager client.PubSubManager sockinfoFlushMap map[string]*client.ClientSockInfo + mapLock *sync.Mutex } func NewGameHandler(ps client.PubSubManager) *GameHandler { @@ -38,7 +41,9 @@ func NewGameHandler(ps client.PubSubManager) *GameHandler { g := GameHandler{ pubsubManager: ps, sockinfoFlushMap: make(map[string]*client.ClientSockInfo), + mapLock: &sync.Mutex{}, } + g.flushWorker() return &g } @@ -69,8 +74,7 @@ func (ch *GameHandler) muxMessage(payload []byte, c gnet.Conn, s *client.ClientS case variable: return ch.handleVariable(payload, s) case animFrame: - // Unimplemented - return errors.New("Received unimplemented message type animFrame") + return ch.handleAnimFrame(payload, s) case switchsync: return ch.handleSwitchSync(payload, s) case animtype: @@ -92,19 +96,38 @@ func (ch *GameHandler) muxMessage(payload []byte, c gnet.Conn, s *client.ClientS func (ch *GameHandler) flushWorker() { go func() { - timer := time.NewTimer(time.Second) + timer := time.NewTicker(time.Second / 60) defer timer.Stop() for true { <-timer.C for key, si := range ch.sockinfoFlushMap { + // FIXME + if si.ClientInfo.IsClosed() { + ch.mapLock.Lock() + delete(ch.sockinfoFlushMap, key) + ch.mapLock.Unlock() + // return {type: "disconnect", uuid: socket.syncObject.uid}; + + err := ch.pubsubManager.Broadcast(&servermessages.DisconnectMessage{ + Type: "disconnect", + UUID: si.SyncObject.UID, + }, si) + if err != nil { + log.Errorf("Failed to broadcast disconnect message, continuing...") + } + continue + } + flushedSO := si.SyncObject.GetFlushedChanges() err := ch.pubsubManager.Broadcast(flushedSO, si) if err != nil { log.Errorf("Received error when broadcasting SO: %s", err) - } else { - delete(ch.sockinfoFlushMap, key) } + // Can lead to state problems if send fails, TODO + ch.mapLock.Lock() + delete(ch.sockinfoFlushMap, key) + ch.mapLock.Unlock() } } }() @@ -113,37 +136,41 @@ func (ch *GameHandler) flushWorker() { func (ch *GameHandler) handleMovement(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.Movement{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { - case !matched: - return errors.New("Failed to match in handleMovement") case err != nil: return err + case !matched: + return errors.New("Failed to match in handleMovement") } c.SyncObject.SetPos(t.X, t.Y) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleSprite(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.Sprite{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { + case err != nil: + return fmt.Errorf("Failed to handleSprite. Err: %s", err) case !matched: return errors.New("Failed to match in handleSprite") - case err != nil: - return err } c.SyncObject.SetSprite(t.SpriteID, t.Spritesheet) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleSound(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.Sound{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { case !matched: return errors.New("Failed to match in handleSound") @@ -152,13 +179,15 @@ func (ch *GameHandler) handleSound(payload []byte, c *client.ClientSockInfo) err } c.SyncObject.SetSound(t.Volume, t.Tempo, t.Balance, t.SoundFile) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleWeather(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.Weather{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { case !matched: return errors.New("Failed to match in handleWeather") @@ -167,13 +196,15 @@ func (ch *GameHandler) handleWeather(payload []byte, c *client.ClientSockInfo) e } c.SyncObject.SetWeather(t.WeatherType, t.WeatherStrength) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleName(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.Name{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { case !matched: return errors.New("Failed to match in handleWeather") @@ -182,13 +213,15 @@ func (ch *GameHandler) handleName(payload []byte, c *client.ClientSockInfo) erro } c.SyncObject.SetName(t.Name) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleVariable(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.Variable{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { case !matched: return errors.New("Failed to match in handleVariable") @@ -197,13 +230,15 @@ func (ch *GameHandler) handleVariable(payload []byte, c *client.ClientSockInfo) } c.SyncObject.SetVariable(t.ID, t.Value) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleSwitchSync(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.SwitchSync{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { case !matched: return errors.New("Failed to match in handleSwitchSync") @@ -212,28 +247,32 @@ func (ch *GameHandler) handleSwitchSync(payload []byte, c *client.ClientSockInfo } c.SyncObject.SetSwitch(t.ID, t.Value) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleAnimFrame(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.AnimFrame{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { - case !matched: - return errors.New("Failed to match in handleWeather") case err != nil: return err + case !matched: + return errors.New("failed to match in handleAnimFrame") } c.SyncObject.SetAnimFrame(t.Frame) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleFacing(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.Facing{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { case !matched: return errors.New("Failed to match in handleFacing") @@ -242,13 +281,15 @@ func (ch *GameHandler) handleFacing(payload []byte, c *client.ClientSockInfo) er } c.SyncObject.SetFacing(t.Facing) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleTypingStatus(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.TypingStatus{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { case !matched: return errors.New("Failed to match in handleTypingStatus") @@ -257,21 +298,25 @@ func (ch *GameHandler) handleTypingStatus(payload []byte, c *client.ClientSockIn } c.SyncObject.SetTypingStatus(t.TypingStatus) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } func (ch *GameHandler) handleMovementAnimSpeed(payload []byte, c *client.ClientSockInfo) error { t := clientmessages.MovementAnimationSpeed{} - matched, err := protocol.Marshal(payload, &t) + matched, err := protocol.Marshal(payload, &t, true) switch { - case !matched: - return errors.New("Failed to match in handleMovementAnimSpeed") case err != nil: return err + case !matched: + return errors.New("Failed to match in handleMovementAnimSpeed") } c.SyncObject.SetMovementAnimationSpeed(t.MovementSpeed) + ch.mapLock.Lock() ch.sockinfoFlushMap[c.SyncObject.UID] = c + ch.mapLock.Unlock() return nil } diff --git a/server/internal/msghandler/servicemux.go b/server/internal/msghandler/servicemux.go index 15cae7b..8452fc0 100644 --- a/server/internal/msghandler/servicemux.go +++ b/server/internal/msghandler/servicemux.go @@ -35,8 +35,8 @@ func NewServiceMux(gh, ch, lh Handler, cm client.PubSubManager) ServiceMux { func (sm ServiceMux) HandleMessage(clientPayload []byte, c gnet.Conn, cinfo *client.ClientSockInfo) error { log.Debug("Handling service message") - syncChan, ok := sm.SyncChanMap[c.RemoteAddr()] sm.m.Lock() + syncChan, ok := sm.SyncChanMap[c.RemoteAddr()] if !ok { syncChan = make(chan struct{}) sm.SyncChanMap[c.RemoteAddr()] = syncChan diff --git a/server/internal/protocol/protocol.go b/server/internal/protocol/protocol.go index 43a7ef9..5739709 100644 --- a/server/internal/protocol/protocol.go +++ b/server/internal/protocol/protocol.go @@ -17,10 +17,11 @@ import ( } */ -func Marshal(msgbuf []byte, target interface{}) (matched bool, err error) { - if msgbuf[0] == 0 { - // LMAO FIXME - msgbuf = msgbuf[1:] +func Marshal(msgbuf []byte, target interface{}, twoBytePrefix bool) (matched bool, err error) { + // TODO: detect client socket endianness and correct somewher else lmao + if twoBytePrefix { + // little endian LOL + msgbuf = append([]byte{msgbuf[0]}, msgbuf[2:]...) } e := reflect.ValueOf(target).Elem() @@ -79,7 +80,20 @@ func Marshal(msgbuf []byte, target interface{}) (matched bool, err error) { if len(msgbuf[i:]) < 2 { return false, fmt.Errorf("invalid length for uint16") } - n := binary.BigEndian.Uint16(msgbuf[i : i+2]) + n := binary.LittleEndian.Uint16(msgbuf[i : i+2]) + + // Not sure if this is really valid for uint16 + if f.OverflowUint(uint64(n)) { + return false, fmt.Errorf("provided value, %d, would overflow if assigned to struct type", n) + } + + f.SetUint(uint64(n)) + + case reflect.Uint32: + if len(msgbuf[i:]) < 4 { + return false, fmt.Errorf("invalid length for uint32") + } + n := binary.LittleEndian.Uint32(msgbuf[i : i+4]) // Not sure if this is really valid for uint16 if f.OverflowUint(uint64(n)) { diff --git a/server/internal/servermessages/servermessages.go b/server/internal/servermessages/servermessages.go index 4b51e92..bbb64f8 100644 --- a/server/internal/servermessages/servermessages.go +++ b/server/internal/servermessages/servermessages.go @@ -15,3 +15,8 @@ type UserMessage struct { Name string `json:"name"` Trip string `json:"trip"` } + +type DisconnectMessage struct { + Type string `json:"type"` + UUID string `json:"uuid"` +} diff --git a/ynoclient b/ynoclient index 75359c9..6fc2f1b 160000 --- a/ynoclient +++ b/ynoclient @@ -1 +1 @@ -Subproject commit 75359c96918609067315b87e74e9efca21f3fe12 +Subproject commit 6fc2f1bfe6d7ea2e6c9339186464ce7d8c7160e7