mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-04 20:13:33 +00:00
Route room inputs through one toRoom helper, iterate live seats with connected(), and share seating and reconnect-window code. Declare every error code once in errcodes.go, which the web vocabulary test now reads. Fixes: - a kicked player is released before being told, so their next action is answered not_in_a_room - a rejected word is counted before move_rejected is sent - room code draw and registration share one lock hold - two concurrent resumes with one token can no longer both take a seat - multiplayer tests ready guests one at a time instead of racing Behaviour changes: - opening or joining another room while a game is running is refused with already_in_a_game - resigning in the lobby answers game_not_started
98 lines
3.5 KiB
Go
98 lines
3.5 KiB
Go
package wsapi
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/coder/websocket"
|
|
noituv1 "github.com/tiennm99dev/noitu/server/gen/noitu/v1"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// maxFrameBytes caps what a client may send.
|
|
//
|
|
// The largest legitimate client frame is a Hello carrying a 20-rune nickname,
|
|
// well under 200 bytes. 4 KiB leaves room for the protocol to grow while still
|
|
// making a memory-exhaustion attempt fail at the socket rather than in the
|
|
// decoder.
|
|
const maxFrameBytes = 4096
|
|
|
|
// ErrNotBinary rejects text frames. The protocol is protobuf over binary
|
|
// frames only; a text frame means the peer is speaking something else, and
|
|
// guessing at it is how a parser becomes an attack surface.
|
|
var ErrNotBinary = errors.New("wsapi: expected a binary frame")
|
|
|
|
// Encode marshals a server message for the wire.
|
|
func Encode(m *noituv1.ServerMessage) ([]byte, error) {
|
|
raw, err := proto.Marshal(m)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wsapi: encode: %w", err)
|
|
}
|
|
return raw, nil
|
|
}
|
|
|
|
// Decode parses a client frame.
|
|
//
|
|
// typ is checked here rather than at the call site so every entry point gets
|
|
// the same rejection.
|
|
func Decode(typ websocket.MessageType, raw []byte) (*noituv1.ClientMessage, error) {
|
|
if typ != websocket.MessageBinary {
|
|
return nil, ErrNotBinary
|
|
}
|
|
var m noituv1.ClientMessage
|
|
if err := proto.Unmarshal(raw, &m); err != nil {
|
|
return nil, fmt.Errorf("wsapi: decode: %w", err)
|
|
}
|
|
return &m, nil
|
|
}
|
|
|
|
// Message-building helpers. Every server message is constructed through one of
|
|
// these, so the oneof wrapper is written once per variant instead of at each
|
|
// call site.
|
|
|
|
func welcomeMsg(sessionID, resumeToken, nickname string) *noituv1.ServerMessage {
|
|
return &noituv1.ServerMessage{Payload: &noituv1.ServerMessage_Welcome{Welcome: &noituv1.Welcome{
|
|
SessionId: sessionID,
|
|
ResumeToken: resumeToken,
|
|
ProtocolVersion: ProtocolVersion,
|
|
AcceptedNickname: nickname,
|
|
}}}
|
|
}
|
|
|
|
// suggestion is REJECT_REASON_NOT_IN_DICTIONARY only: the one real word the
|
|
// submission differs from by diacritics alone, or empty when none applies.
|
|
func moveRejectedMsg(reason noituv1.RejectReason, word string, turnSeq uint32, suggestion string) *noituv1.ServerMessage {
|
|
return &noituv1.ServerMessage{Payload: &noituv1.ServerMessage_MoveRejected{
|
|
MoveRejected: &noituv1.MoveRejected{Reason: reason, Word: word, TurnSeq: turnSeq, Suggestion: suggestion},
|
|
}}
|
|
}
|
|
|
|
// wordReportedMsg acknowledges a ReportWord, echoing the word as the server
|
|
// recorded it so the player sees that it was heard.
|
|
func wordReportedMsg(word string) *noituv1.ServerMessage {
|
|
return &noituv1.ServerMessage{Payload: &noituv1.ServerMessage_WordReported{
|
|
WordReported: &noituv1.WordReported{Word: word},
|
|
}}
|
|
}
|
|
|
|
// errorMsg carries a UI key, never prose and never an internal error string.
|
|
// The Vietnamese copy lives in the frontend so all wording stays in one place,
|
|
// and a raw error would leak server internals to anyone with a socket.
|
|
func errorMsg(code errCode) *noituv1.ServerMessage {
|
|
return &noituv1.ServerMessage{Payload: &noituv1.ServerMessage_Error{
|
|
Error: &noituv1.ServerError{Code: string(code), Message: string(code)},
|
|
}}
|
|
}
|
|
|
|
func pongMsg(clientTimeMs, serverTimeMs int64) *noituv1.ServerMessage {
|
|
return &noituv1.ServerMessage{Payload: &noituv1.ServerMessage_Pong{
|
|
Pong: &noituv1.Pong{ClientTimeMs: clientTimeMs, ServerTimeMs: serverTimeMs},
|
|
}}
|
|
}
|
|
|
|
func quickMatchStatusMsg(queued bool) *noituv1.ServerMessage {
|
|
return &noituv1.ServerMessage{Payload: &noituv1.ServerMessage_QuickMatchStatus{
|
|
QuickMatchStatus: &noituv1.QuickMatchStatus{Queued: queued},
|
|
}}
|
|
}
|