diff --git a/cmd/gameserver/main.go b/cmd/gameserver/main.go index 8d82365..b57287f 100644 --- a/cmd/gameserver/main.go +++ b/cmd/gameserver/main.go @@ -25,7 +25,7 @@ import ( "github.com/pangbox/server/common/topology" "github.com/pangbox/server/database" "github.com/pangbox/server/database/accounts" - "github.com/pangbox/server/game" + gameserver "github.com/pangbox/server/game/server" log "github.com/sirupsen/logrus" "github.com/xo/dburl" ) @@ -66,7 +66,7 @@ func main() { } log.Println("Listening for game server on", listenAddr) - gameServer := game.New(game.Options{ + gameServer := gameserver.New(gameserver.Options{ TopologyClient: topologyClient, AccountsService: accounts.NewService(accounts.Options{ Database: db, diff --git a/cmd/packetparse/main.go b/cmd/packetparse/main.go index d0c046f..ec32744 100644 --- a/cmd/packetparse/main.go +++ b/cmd/packetparse/main.go @@ -30,7 +30,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/go-restruct/restruct" "github.com/pangbox/server/common" - "github.com/pangbox/server/game" + gamepacket "github.com/pangbox/server/game/packet" "github.com/pangbox/server/login" "github.com/pangbox/server/message" ) @@ -49,9 +49,9 @@ func GetMessageTable(server string, origin string) (common.AnyMessageTable, erro case "game": switch origin { case "server": - return game.ServerMessageTable.Any(), nil + return gamepacket.ServerMessageTable.Any(), nil case "client": - return game.ClientMessageTable.Any(), nil + return gamepacket.ClientMessageTable.Any(), nil default: return nil, fmt.Errorf("unexpected origin %q; valid values are server, client", origin) } diff --git a/common/actor/actor.go b/common/actor/actor.go new file mode 100644 index 0000000..58b1913 --- /dev/null +++ b/common/actor/actor.go @@ -0,0 +1,223 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package actor + +import ( + "context" + "errors" + "sync" +) + +var ErrActorDead = errors.New("actor dead") + +type Message[T any] struct { + Context context.Context + Value T + Promise *Promise[any] +} + +type Base[T any] struct { + mutex sync.RWMutex + task *Task[T] + err error +} + +type Task[T any] struct { + wg sync.WaitGroup + msgCh chan Message[T] + doneCh chan struct{} + cancel context.CancelFunc + ctx context.Context +} + +// TryStart returns true if started, or false if there's already a running task. +func (b *Base[T]) TryStart(ctx context.Context, callback func(context.Context, *Task[T]) error) bool { + // Fast/low contention path + b.mutex.RLock() + task := b.task + b.mutex.RUnlock() + + if task != nil { + return false + } + + b.mutex.Lock() + defer b.mutex.Unlock() + + // Need to re-check now. + if b.task != nil { + return false + } + + ctx, cancel := context.WithCancel(ctx) + task = &Task[T]{ + msgCh: make(chan Message[T]), + doneCh: make(chan struct{}), + ctx: ctx, + cancel: cancel, + } + + // These signal the status to elsewhere. + b.task = task + b.err = nil + + go func() { + defer func() { + b.mutex.Lock() + b.task = nil + b.mutex.Unlock() + + task.wg.Wait() + + // At this point, nothing can see this task anymore. + // It should be safe to close the message channel. + close(task.msgCh) + close(task.doneCh) + }() + defer cancel() + if err := callback(ctx, task); err != nil { + b.mutex.Lock() + b.err = err + b.mutex.Unlock() + } + }() + + return true +} + +// Shutdown shuts down the task, if it's running. Shutdown will wait for the +// current instance of the task to fully shut down. +func (b *Base[T]) Shutdown(ctx context.Context) error { + b.mutex.RLock() + task := b.task + b.mutex.RUnlock() + + if task != nil { + task.cancel() + select { + case <-task.doneCh: + return nil + case <-ctx.Done(): + return ctx.Err() + } + } + return nil +} + +// Active returns true if the task is currently running. Note that by the time +// this function returns, the value it returns may already be stale. +func (b *Base[T]) Active() bool { + b.mutex.RLock() + task := b.task + b.mutex.RUnlock() + + return task != nil +} + +// Err returns the last error, if a task returns one. +func (b *Base[T]) Err() error { + b.mutex.RLock() + err := b.err + b.mutex.RUnlock() + + return err +} + +func (b *Base[T]) acquireTask() *Task[T] { + b.mutex.RLock() + defer b.mutex.RUnlock() + + task := b.task + if task != nil { + task.wg.Add(1) + } + + return task +} + +func (t *Task[T]) release() { + if t != nil { + t.wg.Done() + } +} + +// TrySend tries to send if it wouldn't block. The boolean result is set to true +// when the message is successfully sent, false otherwise. +func (b *Base[T]) TrySend(ctx context.Context, value T) (*Promise[any], bool) { + msg := Message[T]{ + Context: ctx, + Value: value, + Promise: NewPromise[any](), + } + + task := b.acquireTask() + defer task.release() + + if task == nil { + return nil, false + } + + defer task.wg.Done() + + select { + case task.msgCh <- msg: + return msg.Promise, true + default: + msg.Promise.Close() + return nil, false + } +} + +// Send will block until the message is sent or the context is cancelled. +func (b *Base[T]) Send(ctx context.Context, value T) (*Promise[any], error) { + msg := Message[T]{ + Context: ctx, + Value: value, + Promise: NewPromise[any](), + } + + task := b.acquireTask() + defer task.release() + + if task == nil { + return nil, ErrActorDead + } + + select { + case task.msgCh <- msg: + return msg.Promise, nil + case <-ctx.Done(): + msg.Promise.Close() + return nil, ctx.Err() + } +} + +// Receive receives the next message in the mailbox. Note that the promise needs +// to be resolved or rejected for every message. +func (t *Task[T]) Receive() (Message[T], error) { + var msg Message[T] + + msgCh := t.msgCh + + select { + case msg = <-msgCh: + return msg, nil + case <-t.ctx.Done(): + return msg, t.ctx.Err() + } +} diff --git a/common/actor/promise.go b/common/actor/promise.go new file mode 100644 index 0000000..51c660b --- /dev/null +++ b/common/actor/promise.go @@ -0,0 +1,71 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package actor + +import ( + "context" + "errors" + "sync" +) + +var ErrClosed = errors.New("promise closed") + +// Promise is a simple promise-like object. +type Promise[T any] struct { + once sync.Once + doneCh chan struct{} + value T + err error +} + +func NewPromise[T any]() *Promise[T] { + return &Promise[T]{ + doneCh: make(chan struct{}), + } +} + +func (p *Promise[T]) Resolve(value T) { + p.once.Do(func() { + p.value = value + close(p.doneCh) + }) +} + +func (p *Promise[T]) Reject(err error) { + p.once.Do(func() { + p.err = err + close(p.doneCh) + }) +} + +func (p *Promise[T]) Close() { + p.Reject(ErrClosed) +} + +func (p *Promise[T]) Wait(ctx context.Context) (T, error) { + var t T + select { + case <-p.doneCh: + if p.err != nil { + return t, p.err + } + return p.value, nil + case <-ctx.Done(): + return t, ctx.Err() + } +} diff --git a/common/bufconn/bufconn.go b/common/bufconn/bufconn.go index 0a77d63..f65db1a 100644 --- a/common/bufconn/bufconn.go +++ b/common/bufconn/bufconn.go @@ -46,7 +46,8 @@ func (e netErrorTimeout) Timeout() bool { return true } func (e netErrorTimeout) Temporary() bool { return false } var errClosed = fmt.Errorf("closed") -var errTimeout net.Error = netErrorTimeout{error: fmt.Errorf("i/o timeout")} + +var errTimeout net.Error = netErrorTimeout{error: fmt.Errorf("i/o timeout")} // +checklocksignore // Listen returns a Listener that can only be contacted by its own Dialers and // creates buffered connections between the two. @@ -117,20 +118,33 @@ type pipe struct { // // w and r are always in the range [0, cap(buf)) and [0, len(buf)]. buf []byte - w, r int + w, r int // +checklocksignore + // +checklocks:mu wwait sync.Cond + + // +checklocks:mu rwait sync.Cond // Indicate that a write/read timeout has occurred + + // +checklocks:mu wtimedout bool + + // +checklocks:mu rtimedout bool + // +checklocks:mu wtimer *time.Timer + + // +checklocks:mu rtimer *time.Timer - closed bool - writeClosed bool + // +checklocks:mu + closed bool + + // +checklocks:mu + writeClosed bool // +checklocksignore } func newPipe(sz int) *pipe { @@ -144,11 +158,11 @@ func newPipe(sz int) *pipe { } func (p *pipe) empty() bool { - return p.r == len(p.buf) + return p.r == len(p.buf) // +checklocksignore } func (p *pipe) full() bool { - return p.r < len(p.buf) && p.r == p.w + return p.r < len(p.buf) && p.r == p.w // +checklocksignore } func (p *pipe) Read(b []byte) (n int, err error) { diff --git a/common/pubsub/postgres.go b/common/pubsub/postgres.go deleted file mode 100755 index 84c20b9..0000000 --- a/common/pubsub/postgres.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (C) 2018-2023, John Chadwick -// -// Permission to use, copy, modify, and/or distribute this software for any purpose -// with or without fee is hereby granted, provided that the above copyright notice -// and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -// THIS SOFTWARE. -// -// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick -// SPDX-License-Identifier: ISC - -package pubsub - -import ( - "encoding/json" - "time" - - "github.com/lib/pq" - log "github.com/sirupsen/logrus" -) - -var ( - _ = PubSub(&PostgresPubSub{}) -) - -// GetPostgresListenerFunc is a type of function that returns a new -// pq.Listener ready to start listening to pubsub channels. You must provide -// such a function to PostgresPubSub. -type GetPostgresListenerFunc func() (*pq.Listener, error) - -// PostgresStream is a handler for a single PostgreSQL subscription. -type PostgresStream struct { - listener *pq.Listener - logger *log.Entry - stream chan Message -} - -func newPostgresStream(listener *pq.Listener, channel string, log *log.Entry) (*PostgresStream, error) { - stream := &PostgresStream{ - listener: listener, - logger: log.WithField("channel", channel), - stream: make(chan Message), - } - - err := stream.listen(channel) - if err != nil { - return nil, err - } - - return stream, nil -} - -// listen begins listening on a given channel. -func (s *PostgresStream) listen(channel string) error { - err := s.listener.Listen(channel) - if err != nil { - return err - } - - go func() { - defer close(s.stream) - - for { - select { - case data, ok := <-s.listener.Notify: - if !ok { - return - } - message := Message{} - err := json.Unmarshal([]byte(data.Extra), &message) - if err != nil { - s.logger.Error(err) - break - } - s.stream <- message - case <-time.After(30 * time.Second): - go s.listener.Ping() - } - - } - }() - - return nil -} - -// Channel returns a channel of streaming messages. -func (s *PostgresStream) Channel() <-chan Message { - return s.stream -} - -// Close ends the pubsub stream. -func (s *PostgresStream) Close() error { - return s.listener.Close() -} - -// PostgresPubSub is an implementation of pubsub using PostgreSQL -// notifications. -type PostgresPubSub struct { - getListener GetPostgresListenerFunc - chanPrefix string - logger *log.Entry -} - -// NewPostgresPubSub creates a new PostgreSQL publish/subscribe engine. -func NewPostgresPubSub(getListener GetPostgresListenerFunc, chanPrefix string, log *log.Entry) (*PostgresPubSub, error) { - return &PostgresPubSub{ - getListener: getListener, - chanPrefix: chanPrefix, - logger: log.WithField("pubsub", "postgres"), - }, nil -} - -// Publish publishes a message on a given channel. -func (p *PostgresPubSub) Publish(channel string, message Message) error { - return nil -} - -// Subscribe listens for messages on a given channel. -func (p *PostgresPubSub) Subscribe(channel string) (Stream, error) { - listener, err := p.getListener() - if err != nil { - return nil, err - } - - stream, err := newPostgresStream(listener, p.chanPrefix+channel, p.logger) - if err != nil { - listener.Close() - return nil, err - } - - return stream, nil -} diff --git a/common/server.go b/common/server.go index 2569280..6c66d97 100755 --- a/common/server.go +++ b/common/server.go @@ -27,7 +27,9 @@ import ( type BaseHandlerFunc func(*log.Entry, net.Conn) error type BaseServer struct { - mu sync.RWMutex + mu sync.RWMutex + + // +checklocks:mu listener net.Listener } diff --git a/common/serverconn.go b/common/serverconn.go index efa2561..f19dc19 100755 --- a/common/serverconn.go +++ b/common/serverconn.go @@ -18,10 +18,13 @@ package common import ( + "context" "encoding/binary" "encoding/hex" "io" + "math/rand" "net" + "sync" "github.com/davecgh/go-spew/spew" "github.com/go-restruct/restruct" @@ -29,21 +32,74 @@ import ( log "github.com/sirupsen/logrus" ) +type HelloMessage interface { + SetKey(key uint8) +} + // ServerConn provides base functionality for PangYa-compatible servers. type ServerConn[ClientMsg Message, ServerMsg Message] struct { - Socket net.Conn - Key uint8 - Log *log.Entry + sendMu sync.RWMutex + + socket net.Conn + key uint8 + log *log.Entry ClientMsg MessageTable[ClientMsg] ServerMsg MessageTable[ServerMsg] } +func NewServerConn[C Message, S Message]( + socket net.Conn, + log *log.Entry, + clientMsg MessageTable[C], + serverMsg MessageTable[S], +) *ServerConn[C, S] { + key := uint8(rand.Intn(16)) + return &ServerConn[C, S]{ + socket: socket, + key: key, + log: log, + ClientMsg: clientMsg, + ServerMsg: serverMsg, + } +} + +// RemoteAddr returns the address of the remotely connected endpoint. +func (c *ServerConn[_, _]) RemoteAddr() net.Addr { + return c.socket.RemoteAddr() +} + +// Log returns a log.Entry for logging. +func (c *ServerConn[_, _]) Log() *log.Entry { + return c.log +} + +// SendHello sends the initial handshake bytes to the client. +func (c *ServerConn[_, _]) SendHello(hello HelloMessage) error { + hello.SetKey(c.key) + + data, err := restruct.Pack(binary.LittleEndian, hello) + if err != nil { + return err + } + + c.sendMu.Lock() + defer c.sendMu.Unlock() + + _, err = c.socket.Write(data) + if err != nil { + return err + } + + return nil +} + // ReadPacket attempts to read a single packet from the socket. +// It is not safe to call ReadPacket from multiple goroutines. func (c *ServerConn[_, _]) ReadPacket() ([]byte, error) { packetHeaderBytes := [4]byte{} - read, err := c.Socket.Read(packetHeaderBytes[:]) + read, err := c.socket.Read(packetHeaderBytes[:]) if err != nil { return nil, err } else if read != len(packetHeaderBytes) { @@ -53,21 +109,21 @@ func (c *ServerConn[_, _]) ReadPacket() ([]byte, error) { remaining := binary.LittleEndian.Uint16(packetHeaderBytes[1:3]) packet := make([]byte, len(packetHeaderBytes)+int(remaining)) copy(packet[:4], packetHeaderBytes[:]) - read, err = c.Socket.Read(packet[4:]) + read, err = c.socket.Read(packet[4:]) if err != nil { return nil, err } else if read != len(packet[4:]) { return nil, io.EOF } - return pangcrypt.ClientDecrypt(packet, c.Key) + return pangcrypt.ClientDecrypt(packet, c.key) } // ParsePacket attempts to construct a packet from packet data. func (c *ServerConn[ClientMsg, _]) ParsePacket(packet []byte) (ClientMsg, error) { msgid := binary.LittleEndian.Uint16(packet[:2]) - c.Log.Debug(hex.Dump(packet)) + c.log.Debug(hex.Dump(packet)) message, err := c.ClientMsg.Build(msgid) if err != nil { @@ -94,8 +150,11 @@ func (c *ServerConn[ClientMsg, _]) ReadMessage() (ClientMsg, error) { return c.ParsePacket(data) } -// SendMessage sends a message to the client. -func (c *ServerConn[_, ServerMsg]) SendMessage(msg ServerMsg) error { +// SendMessage sends a message to the client. It is safe to call SendMessage +// from multiple goroutines. +func (c *ServerConn[_, ServerMsg]) SendMessage(_ context.Context, msg ServerMsg) error { + // TODO: need to handle context cancellation + data, err := restruct.Pack(binary.LittleEndian, msg) if err != nil { return err @@ -110,16 +169,21 @@ func (c *ServerConn[_, ServerMsg]) SendMessage(msg ServerMsg) error { binary.LittleEndian.PutUint16(msgid[:], id) data = append(msgid[:], data...) - data, err = pangcrypt.ServerEncrypt(data, c.Key, 0) + data, err = pangcrypt.ServerEncrypt(data, c.key, 0) if err != nil { return err } - written, err := c.Socket.Write(data) + + c.sendMu.Lock() + defer c.sendMu.Unlock() + + written, err := c.socket.Write(data) if err != nil { return err } else if written != len(data) { return io.EOF } + return nil } @@ -145,11 +209,11 @@ func (c *ServerConn[_, ServerMsg]) DebugMsg(msg ServerMsg) error { // SendRaw sends raw bytes into a PangYa packet. func (c *ServerConn[_, ServerMsg]) SendRaw(data []byte) error { - data, err := pangcrypt.ServerEncrypt(data, c.Key, 0) + data, err := pangcrypt.ServerEncrypt(data, c.key, 0) if err != nil { return err } - written, err := c.Socket.Write(data) + written, err := c.socket.Write(data) if err != nil { return err } else if written != len(data) { diff --git a/game/conn.go b/game/conn.go deleted file mode 100755 index d3fc77e..0000000 --- a/game/conn.go +++ /dev/null @@ -1,536 +0,0 @@ -// Copyright (C) 2018-2023, John Chadwick -// -// Permission to use, copy, modify, and/or distribute this software for any purpose -// with or without fee is hereby granted, provided that the above copyright notice -// and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -// THIS SOFTWARE. -// -// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick -// SPDX-License-Identifier: ISC - -package game - -import ( - "context" - "encoding/binary" - "fmt" - "math/rand" - - "github.com/bufbuild/connect-go" - "github.com/go-restruct/restruct" - "github.com/pangbox/server/common" - "github.com/pangbox/server/gen/dbmodels" - "github.com/pangbox/server/gen/proto/go/topologypb" - "github.com/pangbox/server/pangya" -) - -// Conn holds the state for a connection to the server. -type Conn struct { - common.ServerConn[ClientMessage, ServerMessage] - s *Server -} - -// SendHello sends the initial handshake bytes to the client. -func (c *Conn) SendHello() error { - data, err := restruct.Pack(binary.LittleEndian, &ConnectMessage{ - Unknown: [8]byte{0x00, 0x06, 0x00, 0x00, 0x3f, 0x00, 0x01, 0x01}, - Key: c.Key, - }) - if err != nil { - return err - } - - _, err = c.Socket.Write(data) - if err != nil { - return err - } - - return nil -} - -// Handle runs the main connection loop. -func (c *Conn) Handle(ctx context.Context) error { - log := c.Log - c.Key = uint8(rand.Intn(16)) - - err := c.SendHello() - if err != nil { - return fmt.Errorf("sending hello message: %w", err) - } - - msg, err := c.ReadMessage() - if err != nil { - return fmt.Errorf("reading handshake: %w", err) - } - - var session dbmodels.Session - var player dbmodels.Player - switch t := msg.(type) { - case *ClientAuth: - session, err = c.s.accountsService.GetSessionByKey(ctx, t.LoginKey.Value) - if err != nil { - // TODO: error handling - return nil - } - player, err = c.s.accountsService.GetPlayer(ctx, session.PlayerID) - if err != nil { - // TODO: error handling - return nil - } - log.Debugf("Client auth: %#v", msg) - - default: - return fmt.Errorf("expected client auth, got %T", t) - } - - playerCharacters, err := c.s.accountsService.GetCharacters(ctx, session.PlayerID) - if err != nil { - // TODO: handle error for client - return fmt.Errorf("database error: %w", err) - } - - playerGameData := ServerUserData{ - ClientVersion: common.ToPString("824.00"), - ServerVersion: common.ToPString("Pangbox"), - Game: 0xFFFF, - UserInfo: pangya.UserInfo{ - Username: player.Username, - Nickname: player.Nickname.String, - PlayerID: uint32(player.PlayerID), - ConnnectionID: uint32(session.SessionID), - }, - PlayerStats: pangya.PlayerStats{ - Pangs: uint64(player.Pang), - }, - Items: pangya.PlayerEquipment{ - CaddieID: 0, - CharacterID: playerCharacters[0].ID, - ClubSetID: 0x1754, - AztecIffID: 0x14000000, - Items: pangya.PlayerEquippedItems{ - ItemIDs: [10]uint32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - }, - }, - EquippedCharacter: playerCharacters[0], - EquippedClub: pangya.PlayerClubData{ - Item: pangya.PlayerItem{ - ID: 0x1754, - IFFID: 0x10000000, - }, - Stats: pangya.ClubStats{ - UpgradeStats: [5]uint16{8, 9, 8, 3, 3}, - }, - }, - } - - c.SendMessage(&playerGameData) - - c.SendMessage(&ServerCharData{ - Count1: uint16(len(playerCharacters)), - Count2: uint16(len(playerCharacters)), - Characters: playerCharacters, - }) - - c.SendMessage(&ServerAchievementProgress{ - Remaining: 0, - Count: 0, - }) - - c.SendMessage(&ServerMessageConnect{}) - - message := &ServerChannelList{} - response, err := c.s.topologyClient.ListServers(ctx, connect.NewRequest(&topologypb.ListServersRequest{ - Type: topologypb.Server_TYPE_GAME_SERVER, - })) - if err != nil { - // TODO: error handling to client - return err - } - for _, server := range response.Msg.Server { - entry := pangya.ServerEntry{ - ServerName: server.Name, - ServerID: server.Id, - NumUsers: server.NumUsers, - MaxUsers: server.MaxUsers, - IPAddress: server.Address, - Port: uint16(server.Port), - Flags: uint16(server.Flags), - } - if server.Id == c.s.serverID { - // TODO: support multiple channels? - entry.Channels = append(entry.Channels, pangya.ChannelEntry{ - ChannelName: c.s.channelName, - MaxUsers: 200, // TODO - NumUsers: 0, // TODO - Unknown2: 0x0008, // TODO - }) - } - message.Servers = append(message.Servers, entry) - } - message.Count = uint8(len(response.Msg.Server)) - c.SendMessage(message) - - status := &ServerRoomStatus{} - - for { - msg, err = c.ReadMessage() - if err != nil { - return fmt.Errorf("reading next message: %w", err) - } - - switch t := msg.(type) { - case *ClientException: - log.WithField("exception", t.Message).Debug("Client exception") - return fmt.Errorf("client reported exception: %v", t.Message) - case *ClientMessageSend: - event := &ServerGlobalEvent{Type: ChatMessageData} - event.Data.Message = t.Message - event.Data.Nickname = t.Nickname - c.SendMessage(event) - log.Debug(t.Message.Value) - case *ClientRequestMessengerList: - // TODO - log.Debug("TODO: messenger list") - case *ClientGetUserOnlineStatus: - // TODO - log.Debug("TODO: online status") - case *ClientGetUserData: - // TODO - log.Debug("TODO: user data") - case *ClientRoomLoungeAction: - c.SendMessage(&ServerRoomLoungeAction{ - ConnID: uint32(session.SessionID), - LoungeAction: t.LoungeAction, - }) - case *ClientRoomCreate: - c.SendMessage(&ServerRoomJoin{ - RoomName: t.RoomName.Value, - RoomNumber: 1, - EventNumber: 0, - }) - status = &ServerRoomStatus{ - RoomType: t.RoomType, - Course: t.Course, - NumHoles: t.NumHoles, - HoleProgression: 1, - NaturalWind: 0, - MaxUsers: t.MaxUsers, - ShotTimerMS: t.ShotTimerMS, - GameTimerMS: t.GameTimerMS, - Flags: 0, - Owner: true, - RoomName: t.RoomName, - } - c.SendMessage(status) - self := RoomListUser{ - ConnID: uint32(session.SessionID), - Nickname: player.Nickname.String, - Rank: uint8(player.Rank), - GuildName: "", - Slot: 1, - CharTypeID: playerGameData.EquippedCharacter.CharTypeID, - Flag2: 520, - GuildEmblemImage: "guildmark", - UserID: uint32(player.PlayerID), - CharacterData: playerGameData.EquippedCharacter, - } - other := RoomListUser{ - ConnID: 0xFEEE, - Nickname: "other", - GuildName: "", - PortraitSlotID: 0x38C00083, - Rank: uint8(pangya.JuniorA), - Slot: 2, - CharTypeID: 0x04000007, - Flag2: 0, - GuildEmblemImage: "guildmark", - UserID: 0x2000, - CharacterData: pangya.PlayerCharacterData{ - CharTypeID: 0x04000007, - ID: 0x50000, - HairColor: 0, - }, - } - c.SendMessage(&ServerRoomCensus{ - Type: byte(ListSet), - Unknown: 0xFFFF, - ListSet: &RoomCensusListSet{ - UserCount: 2, - UserList: []RoomListUser{self, other}, - }, - }) - c.SendMessage(&ServerPlayerReady{ - ConnID: 0xFEEE, - State: 0, - }) - case *ClientAssistModeToggle: - c.SendMessage(&ServerAssistModeToggled{}) - // TODO: Should send user status update; need to look at packet dumps. - case *ClientPlayerReady, *ClientPlayerStartGame: - c.SendMessage(&Server0230{}) - c.SendMessage(&Server0231{}) - c.SendRaw([]byte{0x77, 0x00, 0x64, 0x00, 0x00, 0x00}) - c.SendMessage(&ServerGameInit{ - SubType: GameInitTypeFull, - Full: &GameInitFull{ - NumPlayers: 2, - Players: []GamePlayer{ - { - Number: 1, - Info: PlayerInfo{ - Username: player.Username, - Nickname: player.Nickname.String, - ConnID: uint32(session.SessionID), - UserID: uint32(player.PlayerID), - }, - Stats: PlayerStats{}, - Character: playerGameData.EquippedCharacter, - Caddie: playerGameData.EquippedCaddie, - ClubSet: playerGameData.EquippedClub, - Mascot: playerGameData.EquippedMascot, - StartTime: pangya.SystemTime{}, - NumCards: 0, - }, - { - Number: 2, - Info: PlayerInfo{ - Username: "otheru", - Nickname: "other", - ConnID: uint32(0xFEEE), - UserID: uint32(0x2000), - }, - Stats: PlayerStats{}, - Character: playerGameData.EquippedCharacter, - Caddie: playerGameData.EquippedCaddie, - ClubSet: playerGameData.EquippedClub, - Mascot: playerGameData.EquippedMascot, - StartTime: pangya.SystemTime{}, - NumCards: 0, - }, - }, - }, - }) - gameData := &ServerRoomGameData{ - Course: status.Course, - Unknown: 0x0, - HoleProgression: status.HoleProgression, - NumHoles: status.NumHoles, - Unknown2: 0x0, - ShotTimerMS: status.ShotTimerMS, - GameTimerMS: status.GameTimerMS, - RandomSeed: rand.Uint32(), - } - for i := byte(0); i < gameData.NumHoles; i++ { - gameData.Holes = append(gameData.Holes, HoleInfo{ - HoleID: rand.Uint32(), - Pin: 0x0, - Course: status.Course, - Num: i, - }) - } - c.SendMessage(gameData) - /*c.SendMessage(&ServerRoomGameData{ - Course: 11, - Unknown: 0, - HoleProgression: 3, - NumHoles: 3, - Unknown2: 0, - ShotTimerMS: 300000, - GameTimerMS: 0, - Holes: []HoleInfo{ - { - HoleID: 2159514729, - Pin: 0, - Course: 11, - Num: 14, - }, - { - HoleID: 358258534, - Pin: 1, - Course: 11, - Num: 6, - }, - { - HoleID: 3739427069, - Pin: 2, - Course: 11, - Num: 3, - }, - }, - })*/ - // (currently crashes...) - case *ClientRequestInboxList: - // TODO: need new sql message table - msg := &ServerInboxList{ - PageNum: t.PageNum, - NumPages: 1, - NumMessages: 1, - Messages: []InboxMessage{ - {ID: 0x1, SenderNickname: "@Pangbox"}, - }, - } - c.DebugMsg(msg) - c.SendMessage(msg) - case *ClientRequestInboxMessage: - c.SendMessage(&ServerMailMessage{ - Message: MailMessage{ - ID: 0x1, - SenderNickname: common.ToPString("@Pangbox"), - DateTime: common.ToPString("2023-06-03 01:21:00"), - Message: common.ToPString("Welcome to the first Pangbox server release! Not much works yet..."), - }, - }) - case *ClientUnknownCounter: - // Do nothing. - case *Client001A: - // Do nothing. - case *ClientJoinChannel: - c.SendMessage(&Server004E{Unknown: []byte{0x01}}) - c.SendMessage(&Server01F6{Unknown: []byte{0x00, 0x00, 0x00, 0x00}}) - c.SendMessage(&ServerLoginBonusStatus{Unknown: []byte{0x0, 0x0, 0x0, 0x0, 0x1, 0x4, 0x0, 0x0, 0x18, 0x3, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x18, 0x3, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0}}) - case *ClientRequestDailyReward: - c.SendMessage(&ServerMoneyUpdate{ - Type: uint16(MoneyUpdateRewardUnknown), - RewardUnknown: &UpdateRewardUnknownData{ - Unknown: 1, - }, - }) - case *Client009C: - c.SendMessage(&Server010E{Unknown: make([]byte, 0x104)}) - case *ClientMultiplayerJoin: - c.SendMessage(&ServerRoomList{ - Count: 0, - Type: ListSet, - RoomList: []RoomListRoom{}, - }) - // TODO: lobby room new sql impl - c.SendMessage(&ServerUserCensus{ - Count: 1, - Type: UserListSet, - UserList: []CensusUser{ - { - UserID: uint32(player.PlayerID), - ConnID: uint32(session.SessionID), - RoomNumber: -1, - Nickname: player.Nickname.String, - Rank: byte(player.Rank), - GuildEmblemID: "guildmark", // TODO - GlobalID: player.Username, // TODO - }, - }, - }) - c.SendMessage(&ServerMultiplayerJoined{}) - case *ClientMultiplayerLeave: - c.SendMessage(&ServerMultiplayerLeft{}) - case *ClientEventLobbyJoin: - c.SendMessage(&ServerEventLobbyJoined{}) - case *ClientEventLobbyLeave: - c.SendMessage(&ServerEventLobbyLeft{}) - case *ClientRoomLeave: - log.Println("Client leave room") - c.SendMessage(&ServerRoomLeave{RoomNumber: t.RoomNumber}) - case *ClientRoomEdit: - log.Printf("%#v\n", t) - for _, change := range t.Changes { - if change.RoomName != nil { - status.RoomName = *change.RoomName - } - if change.RoomType != nil { - status.RoomType = *change.RoomType - } - if change.Course != nil { - status.Course = *change.Course - } - if change.NumHoles != nil { - status.NumHoles = *change.NumHoles - } - if change.HoleProgression != nil { - status.HoleProgression = *change.HoleProgression - } - if change.ShotTimerSeconds != nil { - status.ShotTimerMS = uint32(*change.ShotTimerSeconds) * 1000 - } - if change.MaxUsers != nil { - status.MaxUsers = *change.MaxUsers - } - if change.GameTimerMinutes != nil { - status.GameTimerMS = uint32(*change.GameTimerMinutes) * 60 * 1000 - } - if change.NaturalWind != nil { - status.NaturalWind = *change.NaturalWind - } - } - c.SendMessage(status) - case *Client0088: - // Unknown tutorial-related message. - case *ClientRoomUserEquipmentChange: - // TODO - case *ClientTutorialStart: - // TODO - c.SendMessage(&ServerRoomEquipmentData{ - Unknown: []byte{ - 0x00, 0x00, 0x00, 0x01, 0x04, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x04, 0xdd, - 0x77, 0x94, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x14, 0x08, 0x00, 0x24, 0x14, 0x08, 0x00, - 0x44, 0x14, 0x08, 0x00, 0x64, 0x14, 0x08, 0x00, 0x84, 0x14, 0x08, 0x00, 0xa4, 0x14, 0x08, 0x00, - 0xc4, 0x14, 0x08, 0x00, 0xe4, 0x14, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - }) - case *ClientTutorialClear: - // After clearing first tutorial - // TODO - c.SendMessage(&ServerTutorialStatus{ - Unknown: [6]byte{0x00, 0x01, 0x03, 0x00, 0x00, 0x00}, - }) - case *ClientUserMacrosSet: - // TODO: server-side macro storage - log.Debugf("Set macros: %+v", t.MacroList) - case *ClientEquipmentUpdate: - // TODO - log.Debug("TODO: 0020") - case *Client00FE: - // TODO - log.Debug("TODO: 00FE") - case *ClientShopJoin: - // Enter shop, not sure what responses need to go here? - log.Debug("TODO: 0140") - default: - return fmt.Errorf("unexpected message: %T", t) - } - } -} diff --git a/common/pubsub/pubsub.go b/game/model/lobby.go old mode 100755 new mode 100644 similarity index 65% rename from common/pubsub/pubsub.go rename to game/model/lobby.go index cb68db4..e92bfc7 --- a/common/pubsub/pubsub.go +++ b/game/model/lobby.go @@ -15,29 +15,19 @@ // SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick // SPDX-License-Identifier: ISC -package pubsub +package gamemodel -import ( - "time" - - "github.com/google/uuid" -) - -// Message is a pub/sub message. -type Message struct { - UUID uuid.UUID - Time time.Time - Payload []byte -} - -// Stream is a stream of incoming pub/sub messages. -type Stream interface { - Channel() <-chan Message - Close() error -} - -// PubSub is an interface for publish/subscribe messaging systems. -type PubSub interface { - Publish(channel string, message Message) error - Subscribe(channel string) (Stream, error) +type LobbyPlayer struct { + PlayerID uint32 + ConnID uint32 + RoomNumber int16 + Nickname string `struct:"[22]byte"` + Rank byte + Unknown uint32 + Badge uint32 + Unknown2 uint32 + Unknown3 uint32 + Unknown4 byte + GuildEmblemImage string `struct:"[18]byte"` + GlobalID string `struct:"[128]byte"` } diff --git a/game/model/room.go b/game/model/room.go new file mode 100644 index 0000000..1bbf0bc --- /dev/null +++ b/game/model/room.go @@ -0,0 +1,168 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package gamemodel + +import ( + "github.com/pangbox/server/common" + "github.com/pangbox/server/pangya" +) + +type RoomStatusFlag uint16 + +const ( + RoomStateUnknown1 RoomStatusFlag = 1 << iota + RoomStateUnknown2 + RoomStateAway + RoomStateMaster + RoomStateUnknown3 + RoomStateUnknown4 + RoomStateUnknown5 + RoomStateUnknown6 + RoomStateUnknown7 + RoomStateReady +) + +type RoomPlayerEntry struct { + ConnID uint32 + Nickname string `struct:"[22]byte"` + GuildName string `struct:"[17]byte"` + Slot uint8 + PlayerFlags uint32 // (I think GM flag goes here!) + TitleID uint32 + CharTypeID uint32 + PortraitBGID uint32 + PortraitFrameID uint32 + PortraitStickerID uint32 + PortraitSlotID uint32 + SlotCutInID uint32 + SlotRankBannerID uint32 + StatusFlags RoomStatusFlag + Rank uint8 + UnknownPadding [3]byte + Unknown uint8 + Unknown2 uint16 + GuildID uint32 + GuildEmblemImage string `struct:"[12]byte"` + GuildEmblemID uint8 + PlayerID uint32 + LoungeState uint32 + Unknown3 uint16 + Unknown4 uint32 + X float32 + Y float32 + Z float32 + Angle float32 + ShopUnknown uint32 + ShopName string `struct:"[64]byte"` + MascotTypeID uint32 + GlobalID string `struct:"[22]byte"` + Unknown5 [106]byte + Guest bool `struct:"byte"` + AverageScore float32 + CharacterData pangya.PlayerCharacterData +} + +type RoomActionRotation struct { + Z float32 +} + +type RoomActionPosition struct { + X, Y, Z float32 +} + +type RoomAction struct { + ActionType byte + Rotation *RoomActionRotation `struct-if:"ActionType == 0"` + PositionAbs *RoomActionRotation `struct-if:"ActionType == 4"` + PositionRel *RoomActionRotation `struct-if:"ActionType == 6"` + Emote *common.PString `struct-if:"ActionType == 7"` + Departure *uint32 `struct-if:"ActionType == 8"` +} + +type GamePhase int + +const ( + LobbyPhase GamePhase = 1 + WaitingLoad + InGame +) + +type RoomState struct { + Active bool + Open bool + ShotTimerMS uint32 + GameTimerMS uint32 + NumUsers uint8 + MaxUsers uint8 + RoomType byte + NumHoles byte + CurrentHole byte + HoleProgression byte + Course byte + RoomName string + RoomNumber int16 + Password string + OwnerConnID uint32 + NaturalWind uint32 + + GamePhase GamePhase + ShotSync *ShotSyncData + ActiveConnID uint32 +} + +type RoomSettingsChange struct { + Type byte + RoomName *common.PString `struct-if:"Type == 0"` + Password *common.PString `struct-if:"Type == 1"` + RoomType *byte `struct-if:"Type == 2"` + Course *byte `struct-if:"Type == 3"` + NumHoles *uint8 `struct-if:"Type == 4"` + HoleProgression *uint8 `struct-if:"Type == 5"` + ShotTimerSeconds *uint8 `struct-if:"Type == 6"` + MaxUsers *uint8 `struct-if:"Type == 7"` + GameTimerMinutes *uint8 `struct-if:"Type == 8"` + ArtifactID *uint32 `struct-if:"Type == 13"` + NaturalWind *uint32 `struct-if:"Type == 14"` +} + +// Used when viewing room info in the lobby. +type RoomInfo struct { + PlayerCount uint32 + NumHoles uint8 + Unknown uint32 + Course uint8 + RoomType uint8 + Mode uint8 + Trophy uint32 + Users []RoomInfoPlayer `struct:"sizefrom=UserCount"` +} + +type RoomInfoPlayer struct { + ConnID uint32 + Rank uint8 + Unknown uint8 + PlayerFlags uint32 + TitleID uint32 + Unknown2 uint32 +} + +type ShotSyncData struct { + ActiveConnID uint32 + X, Y, Z float32 + Unknown [22]byte +} diff --git a/game/msgclient.go b/game/packet/client.go similarity index 63% rename from game/msgclient.go rename to game/packet/client.go index aa74b6d..ca9aa11 100755 --- a/game/msgclient.go +++ b/game/packet/client.go @@ -15,10 +15,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick // SPDX-License-Identifier: ISC -package game +package gamepacket import ( "github.com/pangbox/server/common" + gamemodel "github.com/pangbox/server/game/model" "github.com/pangbox/server/pangya" ) @@ -26,28 +27,49 @@ var ClientMessageTable = common.NewMessageTable(map[uint16]ClientMessage{ 0x0002: &ClientAuth{}, 0x0003: &ClientMessageSend{}, 0x0004: &ClientJoinChannel{}, + 0x0006: &ClientGameEnd{}, 0x0007: &ClientGetUserOnlineStatus{}, 0x0008: &ClientRoomCreate{}, + 0x0009: &ClientRoomJoin{}, 0x000A: &ClientRoomEdit{}, 0x000B: &ClientTutorialStart{}, 0x000C: &ClientRoomUserEquipmentChange{}, 0x000D: &ClientPlayerReady{}, 0x000E: &ClientPlayerStartGame{}, 0x000F: &ClientRoomLeave{}, + 0x0011: &ClientReadyStartHole{}, + 0x0012: &ClientShotCommit{}, + 0x0013: &ClientShotRotate{}, + 0x0014: &ClientShotMeterInput{}, + 0x0015: &ClientShotPower{}, + 0x0016: &ClientShotClubChange{}, + 0x0017: &ClientShotItemUse{}, + 0x0018: &ClientUserTypingIndicator{}, + 0x0019: &ClientShotCometRelief{}, 0x001A: &Client001A{}, + 0x001B: &ClientShotSync{}, + 0x001C: &ClientRoomSync{}, 0x001D: &ClientBuyItem{}, 0x0020: &ClientEquipmentUpdate{}, + 0x0022: &ClientShotActiveUserAcknowledge{}, + 0x0026: &ClientRoomKick{}, + 0x002D: &ClientRoomInfo{}, 0x002F: &ClientGetUserData{}, + 0x0030: &ClientPauseGame{}, + 0x0031: &ClientHoleEnd{}, + 0x0032: &ClientSetIdleStatus{}, 0x0033: &ClientException{}, + 0x0034: &ClientFirstShotReady{}, + 0x0042: &ClientShotArrow{}, 0x0043: &ClientRequestServerList{}, - 0x0048: &ClientUnknownCounter{}, + 0x0048: &ClientLoadProgress{}, 0x0063: &ClientRoomLoungeAction{}, 0x0069: &ClientUserMacrosSet{}, 0x0081: &ClientMultiplayerJoin{}, 0x0082: &ClientMultiplayerLeave{}, 0x0088: &Client0088{}, 0x008B: &ClientRequestMessengerList{}, - 0x009C: &Client009C{}, + 0x009C: &ClientRequestPlayerHistory{}, 0x00AE: &ClientTutorialClear{}, 0x00FE: &Client00FE{}, 0x0140: &ClientShopJoin{}, @@ -84,27 +106,12 @@ type ClientGetUserOnlineStatus struct { Username common.PString } -type SettingsChange struct { - Type byte - RoomName *common.PString `struct-if:"Type == 0"` - Password *common.PString `struct-if:"Type == 1"` - RoomType *byte `struct-if:"Type == 2"` - Course *byte `struct-if:"Type == 3"` - NumHoles *uint8 `struct-if:"Type == 4"` - HoleProgression *uint8 `struct-if:"Type == 5"` - ShotTimerSeconds *uint8 `struct-if:"Type == 6"` - MaxUsers *uint8 `struct-if:"Type == 7"` - GameTimerMinutes *uint8 `struct-if:"Type == 8"` - ArtifactID *uint32 `struct-if:"Type == 13"` - NaturalWind *uint32 `struct-if:"Type == 14"` -} - // ClientRoomEdit is sent when the client changes room settings. type ClientRoomEdit struct { ClientMessage_ Unknown uint16 NumChanges uint8 `struct:"sizeof=Changes"` - Changes []SettingsChange + Changes []gamemodel.RoomSettingsChange } // ClientRoomCreate is sent by the client when creating a room. @@ -123,12 +130,25 @@ type ClientRoomCreate struct { Unknown3 [4]byte } +// ClientRoomJoin is sent by the client when joining a room. +type ClientRoomJoin struct { + ClientMessage_ + RoomNumber int16 + RoomPassword common.PString +} + // ClientJoinChannel is a message sent when the client joins a channel. type ClientJoinChannel struct { ClientMessage_ ChannelID byte } +// ClientGameEnd contains information after the end of a game. +type ClientGameEnd struct { + ClientMessage_ + // TODO +} + // ClientTutorialStart is sent when starting a tutorial. type ClientTutorialStart struct { ClientMessage_ @@ -157,17 +177,87 @@ type ClientPlayerStartGame struct { type ClientRoomLeave struct { ClientMessage_ Unknown byte - RoomNumber uint16 + RoomNumber int16 Unknown2 uint32 Unknown3 uint32 Unknown4 uint32 Unknown5 uint32 } +type ClientReadyStartHole struct { + ClientMessage_ +} + +type ClientShotCommit struct { + ClientMessage_ + UnknownFlag bool `struct:"uint16"` + Unknown [9]byte `struct-if:"UnknownFlag"` + ShotStrength float32 + ShotAccuracy float32 + ShotEnglishCurve float32 + ShotEnglishSpin float32 + Unknown2 [30]byte + Unknown3 [4]float32 +} + +type ClientShotRotate struct { + ClientMessage_ + Angle float32 +} + +type ClientShotMeterInput struct { + ClientMessage_ + Sequence uint8 + Value float32 +} + +type ClientShotPower struct { + ClientMessage_ + Level uint8 +} + +type ClientShotClubChange struct { + ClientMessage_ + Club uint8 +} + +type ClientShotItemUse struct { + ClientMessage_ + ItemTypeID uint32 +} + +type ClientUserTypingIndicator struct { + ClientMessage_ + Status int16 // 1 = started, -1 = stopped +} + +type ClientShotCometRelief struct { + ClientMessage_ + X, Y, Z float32 +} + type Client001A struct { ClientMessage_ } +type SyncEntry struct { + Unknown1 uint8 + Unknown2 uint32 +} + +type ClientShotSync struct { + ClientMessage_ + Data gamemodel.ShotSyncData + Unknown [16]byte +} + +type ClientRoomSync struct { + ClientMessage_ + Unknown1 uint8 + EntryCount uint8 + Entries []SyncEntry +} + type PurchaseItem struct { Unknown uint32 ItemID uint32 @@ -186,43 +276,87 @@ type ClientBuyItem struct { Items []PurchaseItem } -// ClientEquipmentUpdate +type UpdateCaddie struct { + CaddieID uint32 +} + +type UpdateConsumables struct { + ItemTypeID [10]uint32 +} + +type UpdateComet struct { + ItemTypeID uint32 +} + +type UpdateDecoration struct { + BackgroundTypeID uint32 + FrameTypeID uint32 + StickerTypeID uint32 + SlotTypeID uint32 + CutInTypeID uint32 + TitleTypeID uint32 +} + +type UpdateCharacter struct { + CharacterID uint32 +} + +type UpdateUnknown1 struct { + Unknown uint32 +} + +type UpdateUnknown2 struct { + CharacterID uint32 + Unknown [4]uint32 +} + +// ClientEquipmentUpdate updates the user's equipment. type ClientEquipmentUpdate struct { ClientMessage_ + Type uint8 + Caddie *UpdateCaddie `struct-if:"Type == 1"` + Consumables *UpdateConsumables `struct-if:"Type == 2"` + Comet *UpdateComet `struct-if:"Type == 3"` + Decoration *UpdateDecoration `struct-if:"Type == 4"` + Character *UpdateCharacter `struct-if:"Type == 5"` + Unknown1 *UpdateUnknown1 `struct-if:"Type == 8"` + Unknown2 *UpdateUnknown2 `struct-if:"Type == 9"` } -// ClientRequestServerList is a message sent to request the current -// list of game servers. -type ClientRequestServerList struct { +type ClientShotActiveUserAcknowledge struct { ClientMessage_ } -type ClientUnknownCounter struct { +type ClientRoomKick struct { ClientMessage_ - Unknown uint8 + ConnID uint32 } -type LoungeActionRotation struct { - Z float32 +type ClientRoomInfo struct { + ClientMessage_ + RoomNumber uint16 } -type LoungeActionPosition struct { - X, Y, Z float32 +type ClientShotArrow struct { + ClientMessage_ + // TODO } -type LoungeAction struct { - ActionType byte - Rotation *LoungeActionRotation `struct-if:"ActionType == 0"` - PositionAbs *LoungeActionRotation `struct-if:"ActionType == 4"` - PositionRel *LoungeActionRotation `struct-if:"ActionType == 6"` - Emote *common.PString `struct-if:"ActionType == 7"` - Departure *uint32 `struct-if:"ActionType == 8"` +// ClientRequestServerList is a message sent to request the current +// list of game servers. +type ClientRequestServerList struct { + ClientMessage_ +} + +type ClientLoadProgress struct { + ClientMessage_ + Progress uint8 } // ClientRoomLoungeAction type ClientRoomLoungeAction struct { ClientMessage_ - LoungeAction + gamemodel.RoomAction } // ClientRequestMessengerList is a message sent to request the current @@ -239,6 +373,22 @@ type ClientGetUserData struct { Request byte } +type ClientPauseGame struct { + ClientMessage_ + Pause bool `struct:"byte"` +} + +type ClientHoleEnd struct { + ClientMessage_ + // TODO +} + +// ClientSetIdleStatus sets whether or not the client is idle in a room. +type ClientSetIdleStatus struct { + ClientMessage_ + Idle bool `struct:"byte"` +} + // ClientException is a message sent when the client encounters an // error. type ClientException struct { @@ -247,8 +397,12 @@ type ClientException struct { Message common.PString } -// Client009C is an unknown message. -type Client009C struct { +type ClientFirstShotReady struct { + ClientMessage_ +} + +// ClientRequestPlayerHistory is an unknown message. +type ClientRequestPlayerHistory struct { ClientMessage_ } diff --git a/game/message.go b/game/packet/message.go similarity index 98% rename from game/message.go rename to game/packet/message.go index 5558640..02f0788 100644 --- a/game/message.go +++ b/game/packet/message.go @@ -15,7 +15,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick // SPDX-License-Identifier: ISC -package game +package gamepacket import "github.com/pangbox/server/common" diff --git a/game/msgserver.go b/game/packet/server.go similarity index 68% rename from game/msgserver.go rename to game/packet/server.go index 7b1d886..a77056f 100755 --- a/game/msgserver.go +++ b/game/packet/server.go @@ -15,15 +15,18 @@ // SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick // SPDX-License-Identifier: ISC -package game +package gamepacket import ( "github.com/pangbox/server/common" + gamemodel "github.com/pangbox/server/game/model" "github.com/pangbox/server/pangya" ) +type ServerConn = common.ServerConn[ClientMessage, ServerMessage] + var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{ - 0x0040: &ServerGlobalEvent{}, + 0x0040: &ServerEvent{}, 0x0044: &ServerUserData{}, 0x0046: &ServerUserCensus{}, 0x0047: &ServerRoomList{}, @@ -34,20 +37,42 @@ var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{ 0x004C: &ServerRoomLeave{}, 0x004E: &Server004E{}, 0x0052: &ServerRoomGameData{}, + 0x0053: &ServerRoomStartHole{}, + 0x0055: &ServerRoomShotAnnounce{}, + 0x0056: &ServerRoomShotRotateAnnounce{}, + 0x0058: &ServerRoomShotPowerAnnounce{}, + 0x0059: &ServerRoomClubChangeAnnounce{}, + 0x005A: &ServerRoomItemUseAnnounce{}, + 0x005B: &ServerRoomSetWind{}, + 0x005D: &ServerRoomUserTypingAnnounce{}, + 0x0060: &ServerRoomShotCometReliefAnnounce{}, + 0x0063: &ServerRoomActiveUserAnnounce{}, + 0x0064: &ServerRoomShotSync{}, + 0x0065: &ServerRoomFinishHole{}, + 0x0066: &ServerRoomFinishGame{}, 0x0070: &ServerCharData{}, 0x0076: &ServerGameInit{}, + 0x0077: &Server0077{}, 0x0078: &ServerPlayerReady{}, + 0x0086: &ServerRoomInfoResponse{}, + 0x0090: &ServerPlayerFirstShotReady{}, + 0x0092: &ServerOpponentQuit{}, 0x0095: &ServerMoneyUpdate{}, + 0x009E: &ServerRoomSetWeather{}, 0x009F: &ServerChannelList{}, 0x00A1: &ServerUserInfo{}, - 0x00C4: &ServerRoomLoungeAction{}, + 0x00A3: &ServerPlayerLoadProgress{}, + 0x00C4: &ServerRoomAction{}, 0x00C8: &ServerPangPurchaseData{}, + 0x00CC: &ServerRoomShotEnd{}, 0x00F1: &ServerMessageConnect{}, 0x00F5: &ServerMultiplayerJoined{}, 0x00F6: &ServerMultiplayerLeft{}, - 0x010E: &Server010E{}, + 0x010E: &ServerPlayerHistory{}, 0x011F: &ServerTutorialStatus{}, + 0x0151: &Server0151{}, 0x0158: &ServerPlayerStats{}, + 0x016A: &Server016A{}, 0x01F6: &Server01F6{}, 0x0210: &ServerInboxNotify{}, 0x0211: &ServerInboxList{}, @@ -68,24 +93,36 @@ type ConnectMessage struct { Key byte } -// MessageDataType enumerates message data event types. -type MessageDataType byte +func (c *ConnectMessage) SetKey(key uint8) { + c.Key = key +} + +// EventType enumerates message data event types. +type EventType byte const ( - ChatMessageData = 0 + ChatMessageEvent = 0 + GameEndEvent = 16 ) -// GlobalChatMessage contains a global chat message -type GlobalChatMessage struct { +// ChatMessage contains a global chat message +type ChatMessage struct { Nickname common.PString Message common.PString } -// ServerGlobalEvent is a message that contains global chat events. -type ServerGlobalEvent struct { +type GameEnd struct { + Score int32 + Pang uint64 + Unknown uint8 +} + +// ServerEvent is a message that contains events. +type ServerEvent struct { ServerMessage_ - Type MessageDataType - Data GlobalChatMessage + Type byte + Data ChatMessage + GameEnd *GameEnd `struct-if:"Type == 16"` } // ServerChannelList is a message that contains a list of all of the @@ -96,23 +133,20 @@ type ServerChannelList struct { Servers []pangya.ServerEntry } +// PlayerMainData contains the main player information, sent after logging in. +type PlayerMainData struct { + ClientVersion common.PString + ServerVersion common.PString + Game uint16 + PlayerData pangya.PlayerData + Unknown2 [321]byte +} + // ServerUserData contains important state information. type ServerUserData struct { ServerMessage_ - Empty byte - ClientVersion common.PString - ServerVersion common.PString - Game uint16 - UserInfo pangya.UserInfo - PlayerStats pangya.PlayerStats - Unknown [78]byte - Items pangya.PlayerEquipment - JunkData [252 * 43]byte - EquippedCharacter pangya.PlayerCharacterData - EquippedCaddie pangya.PlayerCaddieData - EquippedClub pangya.PlayerClubData - EquippedMascot pangya.PlayerMascotData - Unknown2 [321]byte + SubType byte + MainData *PlayerMainData `struct-if:"SubType == 0"` } // ServerCharData contains the user's characters. @@ -123,79 +157,11 @@ type ServerCharData struct { Characters []pangya.PlayerCharacterData } -type PlayerInfo struct { - Username string `struct:"[22]byte"` - Nickname string `struct:"[22]byte"` - GuildName string `struct:"[21]byte"` - GuildEmblemImage string `struct:"[24]byte"` - ConnID uint32 - Unknown [12]byte - Unknown2 uint32 - Unknown3 uint32 - Unknown4 uint16 - Unknown5 [6]byte - Unknown6 [16]byte - GlobalID string `struct:"[128]byte"` - UserID uint32 -} - -type PlayerStats struct { - TotalStrokes uint32 - TotalPutts uint32 - Time uint32 - StrokeTime uint32 - LongestDrive float32 - Unknown1 uint32 - Unknown2 uint32 - Unknown3 uint32 - Unknown4 uint32 - TotalHoles uint32 - Unknown5 uint32 - TotalHIO uint32 - Unknown6 uint16 - Unknown7 uint32 - TotalAlbatross uint32 - Unknown8 uint32 - Unknown9 uint32 - LongestPutt float32 - LongestChip float32 - TotalXP uint32 - Level byte - Pang uint64 - TotalScore int32 - Unknown10 [5]byte - Unknown11 [49]byte - Unknown12 uint32 - Unknown13 uint32 - Unknown14 uint32 - Unknown15 uint32 - Unknown16 uint32 - Unknown17 [16]byte - ComboNum uint32 - ComboDenom uint32 - Unknown18 uint32 - PangBattleTotal int32 - Unknown19 uint32 - Unknown20 uint32 - Unknown21 uint32 - Unknown22 uint32 - Unknown23 uint32 - Unknown24 [10]byte - Unknown25 uint32 - Unknown26 [8]byte -} - type GamePlayer struct { - Number uint16 - Info PlayerInfo - Stats PlayerStats - Unknown [78]byte - Character pangya.PlayerCharacterData - Caddie pangya.PlayerCaddieData - ClubSet pangya.PlayerClubData - Mascot pangya.PlayerMascotData - StartTime pangya.SystemTime - NumCards uint8 + Number uint16 + PlayerData pangya.PlayerData + StartTime pangya.SystemTime + NumCards uint8 } type GameInitFull struct { @@ -222,18 +188,29 @@ type ServerGameInit struct { Minimal *GameInitMinimal `struct-if:"SubType == 4"` } +type Server0077 struct { + ServerMessage_ + Unknown uint32 +} + // ServerUserInfo contains requested user information. type ServerUserInfo struct { ServerMessage_ ResponseCode uint8 PlayerID uint32 - UserInfo pangya.UserInfo + UserInfo pangya.PlayerInfo } -type ServerRoomLoungeAction struct { +type ServerPlayerLoadProgress struct { + ServerMessage_ + ConnID uint32 + Progress uint8 +} + +type ServerRoomAction struct { ServerMessage_ ConnID uint32 - LoungeAction + gamemodel.RoomAction } // ServerPangPurchaseData is sent after a pang purchase succeeds. @@ -243,6 +220,11 @@ type ServerPangPurchaseData struct { PangsSpent uint64 } +type ServerRoomShotEnd struct { + ServerMessage_ + ConnID uint32 +} + // ServerPlayerID is a message that contains the PlayerID and some // other unknown data. type ServerPlayerID struct { @@ -257,6 +239,7 @@ type UserCensusType byte const ( UserAdd UserCensusType = 1 + UserChange UserCensusType = 2 UserRemove UserCensusType = 3 UserListSet UserCensusType = 4 UserListAppend UserCensusType = 5 @@ -264,28 +247,13 @@ const ( const CensusMaxUsers = 36 -type CensusUser struct { - UserID uint32 - ConnID uint32 - RoomNumber int16 - Nickname string `struct:"[22]byte"` - Rank byte - Unknown uint32 - Badge uint32 - Unknown2 uint32 - Unknown3 uint32 - Unknown4 byte - GuildEmblemID string `struct:"[19]byte"` - GlobalID string `struct:"[128]byte"` -} - // ServerUserCensus contains information about users currently online in // multiplayer type ServerUserCensus struct { ServerMessage_ - Type UserCensusType - Count uint8 `struct:"sizeof=UserList"` - UserList []CensusUser + Type UserCensusType + Count uint8 `struct:"sizeof=PlayerList"` + PlayerList []gamemodel.LobbyPlayer } // ListType enumerates the types of room list messages. @@ -304,28 +272,31 @@ const ( type RoomListRoom struct { Name string `struct:"[64]byte"` Public bool `struct:"byte"` - Unknown uint16 + Open bool `struct:"uint16"` UserMax uint8 UserCount uint8 - Unknown2 [18]byte + Key [16]byte // Known thanks to SuperSS; XOR pad for shot sync data + Unknown3 uint8 + Unknown4 uint8 NumHoles uint8 - Number uint16 HoleProgression uint8 + Number int16 + Unknown5 uint8 Course uint8 ShotTimerMS uint32 GameTimerMS uint32 Flags uint32 - Unknown3 [76]byte - Unknown4 uint32 - Unknown5 uint32 + Unknown6 [68]byte + Unknown7 uint32 + Unknown8 uint32 OwnerID uint32 Class byte ArtifactID uint32 - Unknown6 uint32 + Unknown9 uint32 EventNum uint32 EventNumTop uint32 EventShotTimerMS uint32 - Unknown7 uint32 + Unknown10 uint32 } // ServerRoomList contains information about rooms currently open in @@ -338,55 +309,24 @@ type ServerRoomList struct { RoomList []RoomListRoom } -type RoomListUser struct { - ConnID uint32 - Nickname string `struct:"[22]byte"` - GuildName string `struct:"[17]byte"` - Slot uint8 - Flag uint32 - TitleID uint32 - CharTypeID uint32 - PortraitBGID uint32 - PortraitFrameID uint32 - PortraitStickerID uint32 - PortraitSlotID uint32 - SkinUnknown1 uint32 - SkinUnknown2 uint32 - Flag2 uint16 - Rank uint8 - UnknownPadding [3]byte - Unknown uint8 - Unknown2 uint16 - GuildID uint32 - GuildEmblemImage string `struct:"[12]byte"` - GuildEmblemID uint8 - UserID uint32 - LoungeState uint32 - Unknown3 uint16 - Unknown4 uint32 - X float32 - Y float32 - Z float32 - Angle float32 - ShopUnknown uint32 - ShopName string `struct:"[64]byte"` - MascotTypeID uint32 - GlobalID string `struct:"[22]byte"` - Unknown5 [106]byte - Guest bool `struct:"byte"` - AverageScore float32 - Unknown6 [3]byte - UnknownMisalign byte // TODO: something either before or after here is misaligned - CharacterData pangya.PlayerCharacterData +// ServerRoomCensus reports on the users in a game room. +type ServerRoomCensus struct { + ServerMessage_ + Type byte + Unknown int16 + ListSet *RoomCensusListSet `struct-if:"Type == 0"` + ListAdd *RoomCensusListAdd `struct-if:"Type == 1"` + ListRemove *RoomCensusListRemove `struct-if:"Type == 2"` + ListChange *RoomCensusListChange `struct-if:"Type == 3"` } type RoomCensusListSet struct { - UserCount uint8 `struct:"sizeof=UserList"` - UserList []RoomListUser + PlayerCount uint8 `struct:"sizeof=PlayerList"` + PlayerList []gamemodel.RoomPlayerEntry } type RoomCensusListAdd struct { - User RoomListUser + User gamemodel.RoomPlayerEntry } type RoomCensusListRemove struct { @@ -395,18 +335,7 @@ type RoomCensusListRemove struct { type RoomCensusListChange struct { ConnID uint32 - User RoomListUser -} - -// ServerRoomCensus reports on the users in a game room. -type ServerRoomCensus struct { - ServerMessage_ - Type byte - Unknown uint16 - ListSet *RoomCensusListSet `struct-if:"Type == 0"` - ListAdd *RoomCensusListAdd `struct-if:"Type == 1"` - ListRemove *RoomCensusListRemove `struct-if:"Type == 2"` - ListChange *RoomCensusListChange `struct-if:"Type == 3"` + User gamemodel.RoomPlayerEntry } // ServerRoomStatus is sent when a room's settings or status changes. @@ -434,7 +363,7 @@ type ServerRoomEquipmentData struct { type ServerRoomLeave struct { ServerMessage_ - RoomNumber uint16 + RoomNumber int16 } type Server004E struct { @@ -458,21 +387,119 @@ type ServerRoomGameData struct { Unknown2 uint32 ShotTimerMS uint32 GameTimerMS uint32 - Holes []HoleInfo `struct:"sizefrom=NumHoles"` + Holes [18]HoleInfo // `struct:"sizefrom=NumHoles"` RandomSeed uint32 + Unknown3 [18]byte +} + +type ServerRoomStartHole struct { + ServerMessage_ + ConnID uint32 +} + +type ServerRoomShotAnnounce struct { + ServerMessage_ + ConnID uint32 + ShotStrength float32 + ShotAccuracy float32 + ShotEnglishCurve float32 + ShotEnglishSpin float32 + Unknown2 [30]byte + Unknown3 [4]float32 +} + +type ServerRoomShotRotateAnnounce struct { + ServerMessage_ + ConnID uint32 + Angle float32 +} + +type ServerRoomShotPowerAnnounce struct { + ServerMessage_ + ConnID uint32 + Level uint8 +} + +type ServerRoomClubChangeAnnounce struct { + ServerMessage_ + ConnID uint32 + Club uint8 +} + +type ServerRoomItemUseAnnounce struct { + ServerMessage_ + ItemTypeID uint32 + Unknown uint32 + ConnID uint32 +} + +type ServerRoomSetWind struct { + ServerMessage_ + Wind uint8 + Unknown uint8 + Unknown2 uint16 + Reset bool `struct:"bool"` +} + +type ServerRoomUserTypingAnnounce struct { + ServerMessage_ + ConnID uint32 + Status int16 +} + +type ServerRoomShotCometReliefAnnounce struct { + ServerMessage_ + ConnID uint32 + X, Y, Z float32 +} + +type ServerRoomActiveUserAnnounce struct { + ServerMessage_ + ConnID uint32 +} + +type ServerRoomShotSync struct { + ServerMessage_ + Data gamemodel.ShotSyncData +} + +type ServerRoomFinishHole struct { + ServerMessage_ +} + +type PlayerGameResult struct { + ConnID uint32 + Place uint8 + Score int8 + Unknown uint8 + Unknown2 uint16 + Pang uint64 + BonusPang uint64 + Unknown3 uint64 +} + +type ServerRoomFinishGame struct { + ServerMessage_ + NumPlayers uint8 + Results []PlayerGameResult `struct:"sizefrom=NumPlayers"` +} + +type Server016A struct { + ServerMessage_ + Unknown byte + Unknown2 uint32 } // ServerRoomJoin is sent when a room is joined. type ServerRoomJoin struct { ServerMessage_ - Status byte - Unknown byte + Status uint16 RoomName string `struct:"[64]byte"` Unknown2 [25]byte - RoomNumber uint16 + RoomNumber int16 Unknown3 [111]byte EventNumber uint32 - Unknown4 [12]byte + Unknown4 uint32 } type ServerPlayerReady struct { @@ -481,6 +508,19 @@ type ServerPlayerReady struct { State byte } +type ServerRoomInfoResponse struct { + ServerMessage_ + RoomInfo gamemodel.RoomInfo +} + +type ServerPlayerFirstShotReady struct { + ServerMessage_ +} + +type ServerOpponentQuit struct { + ServerMessage_ +} + type MoneyUpdateType uint16 const ( @@ -506,6 +546,12 @@ type ServerMoneyUpdate struct { PangBalance *UpdatePangBalanceData `struct-if:"Type == 273"` } +type ServerRoomSetWeather struct { + ServerMessage_ + Weather uint16 + Unknown uint8 +} + // ServerMessageConnect seems to make the client connect to the message server. // TODO: need to do more reverse engineering effort type ServerMessageConnect struct { @@ -521,9 +567,16 @@ type ServerMultiplayerLeft struct { ServerMessage_ } -type Server010E struct { +type RecentPlayer struct { + Unknown uint32 + Nickname string `struct:"[22]byte"` + Username string `struct:"[22]byte"` + PlayerID uint32 +} + +type ServerPlayerHistory struct { ServerMessage_ - Unknown []byte + RecentPlayers [5]RecentPlayer } type ServerTutorialStatus struct { @@ -531,12 +584,17 @@ type ServerTutorialStatus struct { Unknown [6]byte } +type Server0151 struct { + ServerMessage_ + Unknown []byte +} + type ServerPlayerStats struct { ServerMessage_ SessionID uint32 Unknown byte - Stats PlayerStats + Stats pangya.PlayerStats } type Server01F6 struct { diff --git a/game/room/event.go b/game/room/event.go new file mode 100644 index 0000000..6d455f4 --- /dev/null +++ b/game/room/event.go @@ -0,0 +1,206 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package room + +import ( + gamemodel "github.com/pangbox/server/game/model" + gamepacket "github.com/pangbox/server/game/packet" + "github.com/pangbox/server/pangya" +) + +type lobbyEvent struct{} +type LobbyEvent interface{ isLobbyEvent() } +type roomEvent struct{} +type RoomEvent interface{ isRoomEvent() } + +func (lobbyEvent) isLobbyEvent() {} +func (roomEvent) isRoomEvent() {} + +type LobbyPlayerJoin struct { + lobbyEvent + Entry gamemodel.LobbyPlayer + Conn *gamepacket.ServerConn +} + +type LobbyPlayerUpdate struct { + lobbyEvent + Entry gamemodel.LobbyPlayer +} + +type LobbyPlayerUpdateRoom struct { + lobbyEvent + ConnID uint32 + RoomNumber int16 +} + +type LobbyPlayerLeave struct { + lobbyEvent + ConnID uint32 +} + +type LobbyRoomCreate struct { + lobbyEvent + Room gamemodel.RoomState +} + +type LobbyRoomUpdate struct { + lobbyEvent + Room gamemodel.RoomState +} + +type LobbyRoomRemove struct { + lobbyEvent + Room gamemodel.RoomState +} + +type RoomGetInfo struct { + roomEvent +} + +type RoomPlayerJoin struct { + roomEvent + Entry *gamemodel.RoomPlayerEntry + PlayerData pangya.PlayerData + Conn *gamepacket.ServerConn +} + +type RoomPlayerLeave struct { + roomEvent + ConnID uint32 +} + +type RoomAction struct { + roomEvent + ConnID uint32 + Action gamemodel.RoomAction +} + +type RoomPlayerIdle struct { + roomEvent + ConnID uint32 + Idle bool +} + +type RoomPlayerReady struct { + roomEvent + ConnID uint32 + Ready bool +} + +type RoomPlayerKick struct { + roomEvent + ConnID uint32 + KickConnID uint32 +} + +type RoomLoadingProgress struct { + roomEvent + ConnID uint32 + Progress uint8 +} + +type RoomSettingsChange struct { + roomEvent + ConnID uint32 + Changes []gamemodel.RoomSettingsChange +} + +type RoomStartGame struct { + roomEvent + ConnID uint32 +} + +type RoomGameReady struct { + roomEvent + ConnID uint32 +} + +type RoomGameShotCommit struct { + roomEvent + ConnID uint32 + ShotStrength float32 + ShotAccuracy float32 + ShotEnglishCurve float32 + ShotEnglishSpin float32 + Unknown2 [30]byte + Unknown3 [4]float32 +} + +type RoomGameShotRotate struct { + roomEvent + ConnID uint32 + Angle float32 +} + +type RoomGameShotPower struct { + roomEvent + ConnID uint32 + Level uint8 +} + +type RoomGameShotClubChange struct { + roomEvent + ConnID uint32 + Club uint8 +} + +type RoomGameShotItemUse struct { + roomEvent + ConnID uint32 + ItemTypeID uint32 +} + +type RoomGameTypingIndicator struct { + roomEvent + ConnID uint32 + Status int16 +} + +type RoomGameShotCometRelief struct { + roomEvent + ConnID uint32 + X, Y, Z float32 +} + +type RoomGameTurn struct { + roomEvent + ConnID uint32 +} + +type RoomGameTurnEnd struct { + roomEvent + ConnID uint32 +} + +type RoomGameHoleEnd struct { + roomEvent + ConnID uint32 +} + +type RoomGameShotSync struct { + roomEvent + ConnID uint32 + Data gamemodel.ShotSyncData +} + +type ChatMessage struct { + lobbyEvent + roomEvent + Nickname string + Message string +} diff --git a/game/room/lobby.go b/game/room/lobby.go new file mode 100644 index 0000000..bfac69d --- /dev/null +++ b/game/room/lobby.go @@ -0,0 +1,346 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package room + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/pangbox/server/common" + "github.com/pangbox/server/common/actor" + gamemodel "github.com/pangbox/server/game/model" + gamepacket "github.com/pangbox/server/game/packet" + log "github.com/sirupsen/logrus" + orderedmap "github.com/wk8/go-ordered-map/v2" + "golang.org/x/sync/errgroup" +) + +type Lobby struct { + actor.Base[LobbyEvent] + logger *log.Entry + storage *Storage + players *orderedmap.OrderedMap[uint32, *LobbyPlayer] +} + +type LobbyPlayer struct { + Entry gamemodel.LobbyPlayer + Conn *gamepacket.ServerConn + Joined time.Time +} + +func NewLobby(ctx context.Context, logger *log.Entry) *Lobby { + lobby := &Lobby{ + logger: logger, + storage: new(Storage), + players: orderedmap.New[uint32, *LobbyPlayer](), + } + lobby.TryStart(ctx, lobby.task) + return lobby +} + +func (l *Lobby) NewRoom(ctx context.Context, room gamemodel.RoomState) (*Room, error) { + promise, err := l.Send(ctx, LobbyRoomCreate{Room: room}) + if err != nil { + return nil, err + } + result, err := promise.Wait(ctx) + if err != nil { + return nil, err + } + return result.(*Room), nil +} + +func (l *Lobby) GetRoom(ctx context.Context, roomNumber int16) *Room { + return l.storage.GetRoom(ctx, roomNumber) +} + +func (l *Lobby) broadcast(ctx context.Context, message gamepacket.ServerMessage) error { + group, ctx := errgroup.WithContext(ctx) + for pair := l.players.Oldest(); pair != nil; pair = pair.Next() { + player := pair.Value + + // Only broadcast to users in the main lobby area. + if player.Entry.RoomNumber != -1 { + continue + } + + group.Go(func() error { + return player.Conn.SendMessage(ctx, message) + }) + } + return group.Wait() +} + +func (l *Lobby) task(ctx context.Context, t *actor.Task[LobbyEvent]) error { + for { + msg, err := t.Receive() + if err != nil { + return err + } + if err := l.handleEvent(ctx, t, msg); err != nil { + return err + } + } +} + +func (l *Lobby) handleEvent(ctx context.Context, t *actor.Task[LobbyEvent], msg actor.Message[LobbyEvent]) error { + defer msg.Promise.Close() + + rejectOnError := func(err error) error { + if err != nil { + msg.Promise.Reject(err) + } else { + msg.Promise.Resolve(nil) + } + return nil + } + + switch event := msg.Value.(type) { + case LobbyPlayerJoin: + return rejectOnError(l.lobbyPlayerJoin(ctx, &event)) + + case LobbyPlayerUpdate: + return rejectOnError(l.lobbyPlayerUpdate(ctx, &event)) + + case LobbyPlayerUpdateRoom: + return rejectOnError(l.lobbyPlayerUpdateRoom(ctx, &event)) + + case LobbyPlayerLeave: + return rejectOnError(l.lobbyPlayerLeave(ctx, &event)) + + case LobbyRoomCreate: + room, err := l.lobbyRoomCreate(ctx, &event) + if err != nil { + msg.Promise.Reject(err) + } else { + msg.Promise.Resolve(room) + } + return nil + + case LobbyRoomUpdate: + return rejectOnError(l.lobbyRoomUpdate(ctx, &event)) + + case LobbyRoomRemove: + return rejectOnError(l.lobbyRoomRemove(ctx, &event)) + + case ChatMessage: + return rejectOnError(l.lobbyChat(ctx, &event)) + + default: + return fmt.Errorf("unknown event: %T", event) + } +} + +func (l *Lobby) lobbyRoomCreate(ctx context.Context, e *LobbyRoomCreate) (*Room, error) { + room := l.storage.NewRoom(ctx) + room.Start(ctx, e.Room, l) + e.Room.RoomNumber = room.Number() + l.broadcast(ctx, &gamepacket.ServerRoomList{ + Count: 1, + Type: gamepacket.ListAdd, + Unknown: 0xFFFF, + RoomList: []gamepacket.RoomListRoom{ + roomToList(&e.Room), + }, + }) + return room, nil +} + +func (l *Lobby) lobbyRoomUpdate(ctx context.Context, e *LobbyRoomUpdate) error { + err := l.storage.UpdateRoom(ctx, e.Room) + if err != nil { + return err + } + return l.broadcast(ctx, &gamepacket.ServerRoomList{ + Count: 1, + Type: gamepacket.ListChange, + Unknown: 0xFFFF, + RoomList: []gamepacket.RoomListRoom{ + roomToList(&e.Room), + }, + }) +} + +func (l *Lobby) lobbyRoomRemove(ctx context.Context, e *LobbyRoomRemove) error { + err := l.storage.UpdateRoom(ctx, e.Room) + if err != nil { + return err + } + return l.broadcast(ctx, &gamepacket.ServerRoomList{ + Count: 1, + Type: gamepacket.ListRemove, + Unknown: 0xFFFF, + RoomList: []gamepacket.RoomListRoom{ + roomToList(&e.Room), + }, + }) +} + +func (l *Lobby) lobbyPlayerUpdate(ctx context.Context, e *LobbyPlayerUpdate) error { + if player, ok := l.players.Get(e.Entry.ConnID); ok { + player.Entry = e.Entry + + l.broadcast(ctx, &gamepacket.ServerUserCensus{ + Type: gamepacket.UserChange, + Count: 1, + PlayerList: []gamemodel.LobbyPlayer{ + player.Entry, + }, + }) + } + return nil +} + +func (l *Lobby) lobbyPlayerUpdateRoom(ctx context.Context, e *LobbyPlayerUpdateRoom) error { + if player, ok := l.players.Get(e.ConnID); ok { + // Player has left a room + if player.Entry.RoomNumber != -1 && e.RoomNumber == -1 { + l.playerSyncLobbyState(ctx, player.Conn) + } + + player.Entry.RoomNumber = e.RoomNumber + + l.broadcast(ctx, &gamepacket.ServerUserCensus{ + Type: gamepacket.UserChange, + Count: 1, + PlayerList: []gamemodel.LobbyPlayer{ + player.Entry, + }, + }) + } + return nil +} + +func (l *Lobby) lobbyPlayerJoin(ctx context.Context, e *LobbyPlayerJoin) error { + l.broadcast(ctx, &gamepacket.ServerUserCensus{ + Type: gamepacket.UserAdd, + Count: 1, + PlayerList: []gamemodel.LobbyPlayer{ + e.Entry, + }, + }) + + l.players.Set(e.Entry.ConnID, &LobbyPlayer{ + Entry: e.Entry, + Conn: e.Conn, + }) + + l.playerSyncLobbyState(ctx, e.Conn) + + if err := e.Conn.SendMessage(ctx, &gamepacket.ServerMultiplayerJoined{}); err != nil { + log.WithError(err).Error("error sending multiplayer joined") + } + + return nil +} + +func (l *Lobby) playerSyncLobbyState(ctx context.Context, conn *gamepacket.ServerConn) error { + msg := &gamepacket.ServerUserCensus{ + Type: gamepacket.UserListSet, + } + + playerList := make([]gamemodel.LobbyPlayer, 0, gamepacket.CensusMaxUsers) + for pair := l.players.Oldest(); pair != nil; pair = pair.Next() { + player := pair.Value + playerList = append(playerList, player.Entry) + if len(playerList) == gamepacket.CensusMaxUsers { + msg.Count = uint8(len(playerList)) + msg.PlayerList = playerList + if err := conn.SendMessage(ctx, msg); err != nil { + log.WithError(err).Error("error sending player list") + } + playerList = playerList[0:0] + msg.Type = gamepacket.UserListAppend + } + } + if len(playerList) > 0 { + msg.Count = uint8(len(playerList)) + msg.PlayerList = playerList + if err := conn.SendMessage(ctx, msg); err != nil { + log.WithError(err).Error("error sending player list") + } + } + + roomList := l.storage.GetRoomList() + + roomListMsg := &gamepacket.ServerRoomList{ + Count: uint8(len(roomList)), + Type: gamepacket.ListSet, + Unknown: 0xFFFF, + RoomList: []gamepacket.RoomListRoom{}, + } + + for _, room := range roomList { + roomListMsg.RoomList = append(roomListMsg.RoomList, roomToList(&room.state)) + } + + if err := conn.SendMessage(ctx, roomListMsg); err != nil { + log.WithError(err).Error("error sending room list") + } + + return nil +} + +func (l *Lobby) lobbyPlayerLeave(ctx context.Context, e *LobbyPlayerLeave) error { + player, ok := l.players.Delete(e.ConnID) + if !ok { + return errors.New("no such player") + } + player.Conn.SendMessage(ctx, &gamepacket.ServerMultiplayerLeft{}) + return l.broadcast(ctx, &gamepacket.ServerUserCensus{ + Type: gamepacket.UserRemove, + Count: 1, + PlayerList: []gamemodel.LobbyPlayer{ + player.Entry, + }, + }) +} + +func (l *Lobby) lobbyChat(ctx context.Context, e *ChatMessage) error { + event := &gamepacket.ServerEvent{Type: gamepacket.ChatMessageEvent} + event.Data.Message = common.ToPString(e.Message) + event.Data.Nickname = common.ToPString(e.Nickname) + err := l.broadcast(ctx, event) + if err != nil { + log.WithError(err).Error("error broadcasting lobby chat message") + } + + return nil +} + +func roomToList(state *gamemodel.RoomState) gamepacket.RoomListRoom { + return gamepacket.RoomListRoom{ + Name: state.RoomName, + Public: true, // TODO + Open: state.Open, + UserMax: state.MaxUsers, + UserCount: state.NumUsers, + Unknown3: 0x1E, + NumHoles: state.NumHoles, + Number: state.RoomNumber, + HoleProgression: 0, // TODO + Course: state.Course, + ShotTimerMS: state.ShotTimerMS, + GameTimerMS: state.GameTimerMS, + OwnerID: state.OwnerConnID, + Class: 255, // TODO + ArtifactID: 0, // TODO + } +} diff --git a/game/room/room.go b/game/room/room.go new file mode 100644 index 0000000..fd76609 --- /dev/null +++ b/game/room/room.go @@ -0,0 +1,790 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package room + +import ( + "context" + "errors" + "fmt" + "math/rand" + "time" + + "github.com/pangbox/server/common" + "github.com/pangbox/server/common/actor" + gamemodel "github.com/pangbox/server/game/model" + gamepacket "github.com/pangbox/server/game/packet" + "github.com/pangbox/server/pangya" + log "github.com/sirupsen/logrus" + orderedmap "github.com/wk8/go-ordered-map/v2" + "golang.org/x/sync/errgroup" +) + +type Room struct { + actor.Base[RoomEvent] + state gamemodel.RoomState + players *orderedmap.OrderedMap[uint32, RoomPlayer] + lobby *Lobby +} + +type PlayerGameState struct { + GameReady bool + ShotSync *gamemodel.ShotSyncData + TurnEnd bool + HoleEnd bool +} + +type RoomPlayer struct { + Entry *gamemodel.RoomPlayerEntry + Conn *gamepacket.ServerConn + PlayerData pangya.PlayerData + GameState *PlayerGameState +} + +func (r *Room) Start(ctx context.Context, state gamemodel.RoomState, lobby *Lobby) bool { + return r.TryStart(ctx, func(ctx context.Context, t *actor.Task[RoomEvent]) error { + r.state.Active = true + r.state.Open = true + r.state.ShotTimerMS = state.ShotTimerMS + r.state.GameTimerMS = state.GameTimerMS + r.state.NumUsers = state.NumUsers + r.state.MaxUsers = state.MaxUsers + r.state.RoomType = state.RoomType + r.state.NumHoles = state.NumHoles + r.state.Course = state.Course + r.state.RoomName = state.RoomName + r.state.Password = state.Password + r.state.HoleProgression = state.HoleProgression + r.state.NaturalWind = state.NaturalWind + r.state.GamePhase = gamemodel.LobbyPhase + r.players = orderedmap.New[uint32, RoomPlayer]() + r.lobby = lobby + return r.task(ctx, t) + }) +} + +func (r *Room) Number() int16 { + if r == nil { + return -1 + } + return r.state.RoomNumber +} + +func (r *Room) GetRoomInfo(ctx context.Context) (gamemodel.RoomInfo, error) { + promise, err := r.Send(ctx, RoomGetInfo{}) + if err != nil { + return gamemodel.RoomInfo{}, err + } + result, err := promise.Wait(ctx) + if err != nil { + return gamemodel.RoomInfo{}, err + } + return result.(gamemodel.RoomInfo), nil +} + +func (r *Room) broadcast(ctx context.Context, msg gamepacket.ServerMessage) error { + group, ctx := errgroup.WithContext(ctx) + for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { + player := pair.Value + group.Go(func() error { + return player.Conn.SendMessage(ctx, msg) + }) + } + return group.Wait() +} + +func (r *Room) stateUpdated(ctx context.Context) error { + r.lobby.Send(ctx, LobbyRoomUpdate{ + Room: r.state, + }) + r.broadcast(ctx, r.roomStatus()) + return nil +} + +func (r *Room) getRoomPlayerList() []gamemodel.RoomPlayerEntry { + playerList := make([]gamemodel.RoomPlayerEntry, r.players.Len()) + for i, pair := 0, r.players.Oldest(); pair != nil; pair = pair.Next() { + playerList[i] = *pair.Value.Entry + playerList[i].Slot = uint8(i + 1) + i++ + } + return playerList +} + +func (r *Room) task(ctx context.Context, t *actor.Task[RoomEvent]) error { + defer func() { + r.state.Active = false + r.lobby.Send(ctx, LobbyRoomRemove{ + Room: r.state, + }) + }() + + for { + msg, err := t.Receive() + if err != nil { + return err + } + if err := r.handleEvent(ctx, t, msg); err != nil { + return err + } + if r.players.Len() == 0 { + break + } + } + + return nil +} + +func (r *Room) handleEvent(ctx context.Context, t *actor.Task[RoomEvent], msg actor.Message[RoomEvent]) error { + defer msg.Promise.Close() + + rejectOnError := func(err error) error { + if err != nil { + msg.Promise.Reject(err) + } else { + msg.Promise.Resolve(nil) + } + return nil + } + + switch event := msg.Value.(type) { + case RoomGetInfo: + info, err := r.handleRoomInfo(ctx, event) + if err != nil { + msg.Promise.Reject(err) + } else { + msg.Promise.Resolve(info) + } + return nil + + case RoomPlayerJoin: + return rejectOnError(r.handlePlayerJoin(ctx, event)) + + case RoomPlayerLeave: + return rejectOnError(r.handlePlayerLeave(ctx, event)) + + case RoomAction: + return rejectOnError(r.handleRoomAction(ctx, event)) + + case RoomPlayerIdle: + return rejectOnError(r.handleRoomPlayerIdle(ctx, event)) + + case RoomPlayerReady: + return rejectOnError(r.handleRoomPlayerReady(ctx, event)) + + case RoomPlayerKick: + return rejectOnError(r.handleRoomPlayerKick(ctx, event)) + + case RoomSettingsChange: + return rejectOnError(r.handleRoomSettingsChange(ctx, event)) + + case RoomStartGame: + return rejectOnError(r.handleRoomStartGame(ctx, event)) + + case RoomLoadingProgress: + return rejectOnError(r.handleRoomLoadingProgress(ctx, event)) + + case RoomGameReady: + return rejectOnError(r.handleRoomGameReady(ctx, event)) + + case RoomGameShotCommit: + return rejectOnError(r.handleRoomGameShotCommit(ctx, event)) + + case RoomGameShotRotate: + return rejectOnError(r.handleRoomGameShotRotate(ctx, event)) + + case RoomGameShotPower: + return rejectOnError(r.handleRoomGameShotPower(ctx, event)) + + case RoomGameShotClubChange: + return rejectOnError(r.handleRoomGameShotClubChange(ctx, event)) + + case RoomGameShotItemUse: + return rejectOnError(r.handleRoomGameShotItemUse(ctx, event)) + + case RoomGameTypingIndicator: + return rejectOnError(r.handleRoomGameTypingIndicator(ctx, event)) + + case RoomGameShotCometRelief: + return rejectOnError(r.handleRoomGameShotCometRelief(ctx, event)) + + case RoomGameTurn: + return rejectOnError(r.handleRoomGameTurn(ctx, event)) + + case RoomGameTurnEnd: + return rejectOnError(r.handleRoomGameTurnEnd(ctx, event)) + + case RoomGameHoleEnd: + return rejectOnError(r.handleRoomGameHoleEnd(ctx, event)) + + case RoomGameShotSync: + return rejectOnError(r.handleRoomGameShotSync(ctx, event)) + + case ChatMessage: + return rejectOnError(r.handleChatMessage(ctx, event)) + + default: + return fmt.Errorf("unknown event: %T", event) + } +} + +func (r *Room) handleRoomInfo(ctx context.Context, event RoomGetInfo) (gamemodel.RoomInfo, error) { + info := gamemodel.RoomInfo{ + PlayerCount: uint32(r.players.Len()), + NumHoles: r.state.NumHoles, + Unknown: 0, + Course: r.state.Course, + RoomType: r.state.RoomType, + Users: make([]gamemodel.RoomInfoPlayer, r.players.Len()), + } + + for i, pair := 0, r.players.Oldest(); pair != nil; pair = pair.Next() { + player := pair.Value + info.Users[i] = gamemodel.RoomInfoPlayer{ + ConnID: player.Entry.ConnID, + Rank: player.Entry.Rank, + PlayerFlags: player.Entry.PlayerFlags, + TitleID: player.Entry.TitleID, + } + i++ + } + + return info, nil +} + +func (r *Room) handlePlayerJoin(ctx context.Context, event RoomPlayerJoin) error { + if r.players.Len() >= int(r.state.MaxUsers) { + return errors.New("room full") + } + if r.players.Len() == 0 { + // New room + event.Entry.StatusFlags |= gamemodel.RoomStateMaster + r.state.OwnerConnID = event.Entry.ConnID + } + _, present := r.players.Set(event.Entry.ConnID, RoomPlayer{ + Entry: event.Entry, + Conn: event.Conn, + PlayerData: event.PlayerData, + GameState: &PlayerGameState{}, + }) + if present { + return errors.New("already in room") + } + + event.Conn.SendMessage(ctx, &gamepacket.ServerRoomJoin{ + RoomName: r.state.RoomName, + RoomNumber: r.state.RoomNumber, + EventNumber: 0, + }) + + event.Conn.SendMessage(ctx, r.roomStatus()) + + r.lobby.Send(ctx, LobbyPlayerUpdateRoom{ + ConnID: event.Entry.ConnID, + RoomNumber: -1, + }) + + err := r.broadcastPlayerList(ctx) + if err != nil { + log.WithError(err).Error("error broadcasting room status") + } + + r.state.NumUsers = uint8(r.players.Len()) + r.stateUpdated(ctx) + + return nil +} + +func (r *Room) handlePlayerLeave(ctx context.Context, event RoomPlayerLeave) error { + return r.removePlayer(ctx, event.ConnID) +} + +func (r *Room) handleRoomAction(ctx context.Context, event RoomAction) error { + if event.Action.Rotation != nil { + if player, ok := r.players.Get(event.ConnID); ok { + player.Entry.Angle = event.Action.Rotation.Z + return r.broadcast(ctx, &gamepacket.ServerRoomAction{ + ConnID: player.Entry.ConnID, + RoomAction: event.Action, + }) + } + } + return nil +} + +func (r *Room) handleRoomPlayerIdle(ctx context.Context, event RoomPlayerIdle) error { + if player, ok := r.players.Get(event.ConnID); ok { + if event.Idle { + player.Entry.StatusFlags |= gamemodel.RoomStateAway + } else { + player.Entry.StatusFlags &^= gamemodel.RoomStateAway + } + // TODO: not sure what to broadcast + return r.broadcastPlayerList(ctx) + } + return nil +} + +func (r *Room) handleRoomPlayerReady(ctx context.Context, event RoomPlayerReady) error { + if player, ok := r.players.Get(event.ConnID); ok { + state := byte(0) + if event.Ready { + player.Entry.StatusFlags |= gamemodel.RoomStateReady + } else { + player.Entry.StatusFlags &^= gamemodel.RoomStateReady + state = 1 + } + return r.broadcast(ctx, &gamepacket.ServerPlayerReady{ + ConnID: player.Entry.ConnID, + State: state, + }) + } + return nil +} + +func (r *Room) handleRoomPlayerKick(ctx context.Context, event RoomPlayerKick) error { + player, ok := r.players.Get(event.ConnID) + if !ok { + return nil + } + subject, ok := r.players.Get(event.KickConnID) + if !ok { + return nil + } + if player.Entry.ConnID != r.state.OwnerConnID { + return nil + } + return r.removePlayer(ctx, subject.Entry.ConnID) +} + +func (r *Room) handleRoomSettingsChange(ctx context.Context, event RoomSettingsChange) error { + if event.ConnID != r.state.OwnerConnID { + return nil + } + + for _, change := range event.Changes { + if change.RoomName != nil { + r.state.RoomName = change.RoomName.Value + } + if change.RoomType != nil { + r.state.RoomType = *change.RoomType + } + if change.Course != nil { + r.state.Course = *change.Course + } + if change.NumHoles != nil { + r.state.NumHoles = *change.NumHoles + } + if change.HoleProgression != nil { + r.state.HoleProgression = *change.HoleProgression + } + if change.ShotTimerSeconds != nil { + r.state.ShotTimerMS = uint32(*change.ShotTimerSeconds) * 1000 + } + if change.MaxUsers != nil { + r.state.MaxUsers = *change.MaxUsers + } + if change.GameTimerMinutes != nil { + r.state.GameTimerMS = uint32(*change.GameTimerMinutes) * 60 * 1000 + } + if change.NaturalWind != nil { + r.state.NaturalWind = *change.NaturalWind + } + } + + r.stateUpdated(ctx) + return nil +} + +func (r *Room) handleRoomStartGame(ctx context.Context, event RoomStartGame) error { + r.state.Open = false + r.state.GamePhase = gamemodel.WaitingLoad + r.stateUpdated(ctx) + + r.broadcast(ctx, &gamepacket.Server0230{}) + r.broadcast(ctx, &gamepacket.Server0231{}) + r.broadcast(ctx, &gamepacket.Server0077{Unknown: 0x64}) + now := time.Now() + gameInit := &gamepacket.ServerGameInit{ + SubType: gamepacket.GameInitTypeFull, + Full: &gamepacket.GameInitFull{ + NumPlayers: byte(r.players.Len()), + Players: make([]gamepacket.GamePlayer, r.players.Len()), + }, + } + r.state.CurrentHole = 1 + for i, pair := 0, r.players.Oldest(); pair != nil; pair = pair.Next() { + player := pair.Value + gameInit.Full.Players[i] = gamepacket.GamePlayer{ + Number: uint16(i + 1), + PlayerData: player.PlayerData, + StartTime: pangya.NewSystemTime(now), + NumCards: 0, + } + i++ + } + r.broadcast(ctx, gameInit) + gameData := &gamepacket.ServerRoomGameData{ + Course: r.state.Course, + Unknown: 0x0, + HoleProgression: r.state.HoleProgression, + NumHoles: r.state.NumHoles, + Unknown2: 0x0, + ShotTimerMS: r.state.ShotTimerMS, + GameTimerMS: r.state.GameTimerMS, + RandomSeed: rand.Uint32(), + } + for i := byte(0); i < 18; i++ { + gameData.Holes[i] = gamepacket.HoleInfo{ + HoleID: rand.Uint32(), + Pin: 0x0, + Course: r.state.Course, + Num: i + 1, + } + } + r.broadcast(ctx, gameData) + r.broadcast(ctx, &gamepacket.Server016A{Unknown: 1, Unknown2: 0x24bd}) + + return nil +} + +func (r *Room) handleRoomLoadingProgress(ctx context.Context, event RoomLoadingProgress) error { + return r.broadcast(ctx, &gamepacket.ServerPlayerLoadProgress{ + ConnID: event.ConnID, + Progress: event.Progress, + }) +} + +func (r *Room) handleRoomGameReady(ctx context.Context, event RoomGameReady) error { + if player, ok := r.players.Get(event.ConnID); ok { + player.GameState.GameReady = true + } + if r.checkGameReady() { + r.startHole(ctx) + } + return nil +} + +func (r *Room) handleRoomGameShotCommit(ctx context.Context, event RoomGameShotCommit) error { + return r.broadcast(ctx, &gamepacket.ServerRoomShotAnnounce{ + ConnID: event.ConnID, + ShotStrength: event.ShotStrength, + ShotAccuracy: event.ShotAccuracy, + ShotEnglishCurve: event.ShotEnglishCurve, + ShotEnglishSpin: event.ShotEnglishSpin, + Unknown2: event.Unknown2, + Unknown3: event.Unknown3, + }) +} + +func (r *Room) handleRoomGameShotRotate(ctx context.Context, event RoomGameShotRotate) error { + return r.broadcast(ctx, &gamepacket.ServerRoomShotRotateAnnounce{ + ConnID: event.ConnID, + Angle: event.Angle, + }) +} + +func (r *Room) handleRoomGameShotPower(ctx context.Context, event RoomGameShotPower) error { + return r.broadcast(ctx, &gamepacket.ServerRoomShotPowerAnnounce{ + ConnID: event.ConnID, + Level: event.Level, + }) +} + +func (r *Room) handleRoomGameShotClubChange(ctx context.Context, event RoomGameShotClubChange) error { + return r.broadcast(ctx, &gamepacket.ServerRoomClubChangeAnnounce{ + ConnID: event.ConnID, + Club: event.Club, + }) +} + +func (r *Room) handleRoomGameShotItemUse(ctx context.Context, event RoomGameShotItemUse) error { + return r.broadcast(ctx, &gamepacket.ServerRoomItemUseAnnounce{ + ConnID: event.ConnID, + ItemTypeID: event.ItemTypeID, + }) +} + +func (r *Room) handleRoomGameTypingIndicator(ctx context.Context, event RoomGameTypingIndicator) error { + return r.broadcast(ctx, &gamepacket.ServerRoomUserTypingAnnounce{ + ConnID: event.ConnID, + Status: event.Status, + }) +} + +func (r *Room) handleRoomGameShotCometRelief(ctx context.Context, event RoomGameShotCometRelief) error { + return r.broadcast(ctx, &gamepacket.ServerRoomShotCometReliefAnnounce{ + ConnID: event.ConnID, + X: event.X, + Y: event.Y, + Z: event.Z, + }) +} + +func (r *Room) handleRoomGameTurn(ctx context.Context, event RoomGameTurn) error { + return nil +} + +func (r *Room) handleRoomGameTurnEnd(ctx context.Context, event RoomGameTurnEnd) error { + if player, ok := r.players.Get(event.ConnID); ok { + player.GameState.TurnEnd = true + } + if r.checkTurnEnd() { + r.broadcast(ctx, &gamepacket.ServerRoomShotEnd{ + ConnID: r.state.ActiveConnID, + }) + for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { + pair.Value.GameState.TurnEnd = false + } + + // TODO: need to find furthest from pin/etc. + + nextPlayer := r.players.GetPair(r.state.ActiveConnID) + if nextPlayer != nil { + nextPlayer = nextPlayer.Next() + } + if nextPlayer == nil { + nextPlayer = r.players.Oldest() + } + for ; nextPlayer != nil; nextPlayer = nextPlayer.Next() { + if !nextPlayer.Value.GameState.HoleEnd { + break + } + } + if nextPlayer == nil { + r.state.CurrentHole++ + if r.state.CurrentHole > r.state.NumHoles { + for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { + // TODO: + // - This doesn't work (client hangs) + // - Need to actually calculate values for real. + r.broadcast(ctx, &gamepacket.ServerEvent{ + Type: gamepacket.GameEndEvent, + Data: gamepacket.ChatMessage{ + Nickname: common.ToPString(pair.Value.Entry.Nickname), + }, + GameEnd: &gamepacket.GameEnd{ + Score: 0, + Pang: 0, + }, + }) + results := &gamepacket.ServerRoomFinishGame{ + NumPlayers: uint8(r.players.Len()), + Results: make([]gamepacket.PlayerGameResult, r.players.Len()), + } + for i, pair := 0, r.players.Oldest(); pair != nil; pair = pair.Next() { + results.Results[i].ConnID = pair.Value.Entry.ConnID + results.Results[i].Place = uint8(i + 1) + } + r.broadcast(ctx, results) + r.state.Open = true + r.state.CurrentHole = 0 + r.state.GamePhase = gamemodel.LobbyPhase + } + } else { + r.broadcast(ctx, &gamepacket.ServerRoomFinishHole{}) + for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { + pair.Value.GameState.HoleEnd = false + } + } + r.stateUpdated(ctx) + return nil + } + r.state.ActiveConnID = nextPlayer.Key + r.broadcast(ctx, &gamepacket.ServerRoomActiveUserAnnounce{ + ConnID: r.state.ActiveConnID, + }) + } + return nil +} + +func (r *Room) handleRoomGameHoleEnd(ctx context.Context, event RoomGameHoleEnd) error { + if player, ok := r.players.Get(event.ConnID); ok { + player.GameState.HoleEnd = true + } + return nil +} + +func (r *Room) handleRoomGameShotSync(ctx context.Context, event RoomGameShotSync) error { + syncData := event.Data + if r.state.ShotSync == nil { + r.state.ShotSync = &syncData + } else { + if *r.state.ShotSync != syncData { + log.Warningf("Shot sync mismatch: %#v vs %#v", r.state.ShotSync, syncData) + } + } + if player, ok := r.players.Get(event.ConnID); ok { + player.GameState.ShotSync = r.state.ShotSync + } + if r.checkShotSync() { + r.broadcast(ctx, &gamepacket.ServerRoomShotSync{ + Data: *r.state.ShotSync, + }) + for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { + pair.Value.GameState.ShotSync = nil + } + r.state.ShotSync = nil + } + return nil +} + +func (r *Room) handleChatMessage(ctx context.Context, event ChatMessage) error { + msg := &gamepacket.ServerEvent{Type: gamepacket.ChatMessageEvent} + msg.Data.Message = common.ToPString(event.Message) + msg.Data.Nickname = common.ToPString(event.Nickname) + return r.broadcast(ctx, msg) +} + +func (r *Room) checkGameReady() bool { + for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { + if !pair.Value.GameState.GameReady { + return false + } + } + return true +} + +func (r *Room) checkShotSync() bool { + for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { + if pair.Value.GameState.ShotSync == nil { + return false + } + } + return true +} + +func (r *Room) checkTurnEnd() bool { + for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { + if !pair.Value.GameState.TurnEnd { + return false + } + } + return true +} + +func (r *Room) startHole(ctx context.Context) error { + r.state.GamePhase = gamemodel.InGame + // TODO: calculate wind/weather. + r.broadcast(ctx, &gamepacket.ServerRoomSetWeather{ + Weather: 0, + }) + r.broadcast(ctx, &gamepacket.ServerRoomSetWind{ + Wind: 10, + Unknown: 0, + Unknown2: 0x98, + Reset: true, + }) + // TODO: select player based on proper order + r.state.ActiveConnID = r.players.Oldest().Key + r.broadcast(ctx, &gamepacket.ServerRoomStartHole{ + ConnID: r.state.ActiveConnID, + }) + // TODO: These blobs are taken from an old packet dump. Not exactly sure what they are for. + r.broadcast(ctx, &gamepacket.Server0151{Unknown: []byte{ + 0x0d, 0x00, 0x57, 0x5f, 0x42, 0x49, 0x47, 0x42, 0x4f, 0x4e, 0x47, 0x44, 0x41, 0x52, 0x49, 0x00, + 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, 0x02, 0x00, 0x02, 0x02, 0x02, 0x03, 0x01, 0x01, 0x00, 0x01, + 0x00, 0x03, 0x02, 0x00, 0x00, 0x00, 0x02, 0x03, 0x03, 0x00, 0x01, 0x01, 0x03, 0x00, 0x02, 0x03, + 0x01, 0x03, 0x03, 0x01, 0x02, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x03, 0x03, 0x03, + 0x02, 0x02, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01, 0x00, 0x03, + 0x00, 0x01, 0x00, 0x03, 0x03, 0x03, 0x02, 0x00, 0x01, 0x01, 0x02, 0x03, 0x03, 0x01, 0x02, 0x00, + 0x00, 0x02, 0x03, 0x02, 0x00, 0x00, 0x03, 0x02, 0x03, 0x00, 0x03, 0x00, 0x03, 0x02, 0x03, 0x02, + 0x03, 0x00, 0x03, + }}) + r.broadcast(ctx, &gamepacket.Server0151{Unknown: []byte{ + 0x0d, 0x00, 0x52, 0x5f, 0x42, 0x49, 0x47, 0x42, 0x4f, 0x4e, 0x47, 0x44, 0x41, 0x52, 0x49, 0x01, + 0x02, 0x00, 0x00, 0x01, 0x03, 0x01, 0x00, 0x01, 0x02, 0x02, 0x02, 0x03, 0x03, 0x02, 0x02, 0x01, + 0x01, 0x03, 0x03, 0x00, 0x02, 0x02, 0x02, 0x03, 0x01, 0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x02, + 0x03, 0x03, 0x01, 0x02, 0x01, 0x03, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x03, + 0x01, 0x00, 0x02, 0x02, 0x00, 0x02, 0x03, 0x00, 0x03, 0x03, 0x01, 0x03, 0x02, 0x01, 0x02, 0x03, + 0x03, 0x03, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x01, 0x02, 0x00, 0x00, + 0x02, 0x02, 0x03, 0x00, 0x01, 0x02, 0x01, 0x02, 0x01, 0x03, 0x02, 0x01, 0x01, 0x03, 0x01, 0x02, + 0x00, 0x02, 0x01, + }}) + r.broadcast(ctx, &gamepacket.Server0151{Unknown: []byte{ + 0x0f, 0x00, 0x43, 0x4c, 0x55, 0x42, 0x53, 0x45, 0x54, 0x5f, 0x4d, 0x49, 0x52, 0x41, 0x43, 0x4c, + 0x45, 0x01, 0x01, 0x01, 0x02, 0x02, 0x00, 0x02, 0x01, 0x02, 0x03, 0x01, 0x03, 0x00, 0x02, 0x02, + 0x03, 0x03, 0x01, 0x01, 0x02, 0x02, 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x03, 0x01, 0x00, 0x02, + 0x01, 0x03, 0x03, 0x03, 0x02, 0x01, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x00, 0x03, 0x00, + 0x01, 0x02, 0x00, 0x02, 0x03, 0x02, 0x02, 0x02, 0x00, 0x03, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x03, 0x02, 0x01, 0x00, 0x01, 0x03, 0x03, 0x03, 0x00, + 0x03, 0x02, 0x02, 0x02, 0x03, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01, 0x03, + 0x01, 0x00, 0x01, 0x02, 0x00, + }}) + + return nil +} + +func (r *Room) removePlayer(ctx context.Context, connID uint32) error { + if player, ok := r.players.Get(connID); ok { + err := player.Conn.SendMessage(ctx, &gamepacket.ServerRoomLeave{ + RoomNumber: -1, + }) + r.broadcast(ctx, &gamepacket.ServerRoomCensus{ + Type: byte(gamepacket.ListRemove), + Unknown: -1, + ListRemove: &gamepacket.RoomCensusListRemove{ + ConnID: player.Entry.ConnID, + }, + }) + r.players.Delete(connID) + r.lobby.Send(ctx, LobbyPlayerUpdateRoom{ + ConnID: connID, + RoomNumber: -1, + }) + if r.state.OwnerConnID == player.Entry.ConnID && r.players.Len() > 0 { + newOwner := r.players.Oldest() + newOwner.Value.Entry.StatusFlags |= gamemodel.RoomStateMaster + r.state.OwnerConnID = newOwner.Value.Entry.ConnID + r.broadcastPlayerList(ctx) + } + r.state.NumUsers = uint8(r.players.Len()) + r.stateUpdated(ctx) + return err + } else { + return errors.New("user not in room") + } +} + +func (r *Room) broadcastPlayerList(ctx context.Context) error { + playerList := r.getRoomPlayerList() + + return r.broadcast(ctx, &gamepacket.ServerRoomCensus{ + Type: byte(gamepacket.ListSet), + Unknown: -1, + ListSet: &gamepacket.RoomCensusListSet{ + PlayerCount: uint8(len(playerList)), + PlayerList: playerList, + }, + }) +} + +func (r *Room) roomStatus() *gamepacket.ServerRoomStatus { + return &gamepacket.ServerRoomStatus{ + RoomType: r.state.RoomType, + Course: r.state.Course, + NumHoles: r.state.NumHoles, + HoleProgression: 1, + NaturalWind: 0, + MaxUsers: r.state.MaxUsers, + ShotTimerMS: r.state.ShotTimerMS, + GameTimerMS: r.state.GameTimerMS, + Flags: 0, + Owner: true, + RoomName: common.ToPString(r.state.RoomName), + } +} diff --git a/game/room/storage.go b/game/room/storage.go new file mode 100644 index 0000000..2adde80 --- /dev/null +++ b/game/room/storage.go @@ -0,0 +1,207 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package room + +import ( + "container/heap" + "context" + "errors" + "sync" + + "github.com/davecgh/go-spew/spew" + gamemodel "github.com/pangbox/server/game/model" +) + +// The primary job of this code is to manage the allocation of rooms in a +// channel. + +type RoomEntry struct { + // Room instance + room Room + + // A copy of the state that gets synced to us + state gamemodel.RoomState + + // The heap index + heapIndex int +} + +// RoomHeap implements a heap.Interface for the room list. +// It provides a min heap such that the lowest-numbered inactive room will be +// at the lowest index. +type RoomHeap []*RoomEntry + +type Storage struct { + // Rooms sorted by room number. Guaranteed to be contiguous. + roomsByNumber []*RoomEntry + resizeMu sync.RWMutex + + // Room min-heap over room number. Used to find inactive room slots to + // use when creating new rooms. + roomHeap RoomHeap +} + +// NewRoom returns a new room. +func (m *Storage) NewRoom(ctx context.Context) *Room { + if len(m.roomHeap) == 0 { + return m.allocRoom(ctx) + } + lowestRoom := m.roomHeap[0] + if lowestRoom.state.Active { + return m.allocRoom(ctx) + } + lowestRoom.state.Active = true + heap.Fix(&m.roomHeap, lowestRoom.heapIndex) + return &lowestRoom.room +} + +// UpdateRoom updates the room state for a given room. +func (m *Storage) UpdateRoom(ctx context.Context, state gamemodel.RoomState) error { + n := int(state.RoomNumber) + + m.resizeMu.RLock() + roomsByNumber := m.roomsByNumber + m.resizeMu.RUnlock() + + if n >= len(roomsByNumber) { + return errors.New("room number too large") + } + entry := roomsByNumber[n] + entry.state = state + + heap.Fix(&m.roomHeap, entry.heapIndex) + + // TODO: we should try to only cull when it's not too busy + if !state.Active { + m.cull() + } + + return nil +} + +// GetRoom gets a room by ID. This can be called from any thread. +func (m *Storage) GetRoom(ctx context.Context, roomNumber int16) *Room { + m.resizeMu.RLock() + roomsByNumber := m.roomsByNumber + m.resizeMu.RUnlock() + + if int(roomNumber) < len(roomsByNumber) { + return &roomsByNumber[roomNumber].room + } + + return nil +} + +// GetRoomList returns a full list of rooms. +func (m *Storage) GetRoomList() []*Room { + results := []*Room{} + + m.resizeMu.RLock() + roomsByNumber := m.roomsByNumber + m.resizeMu.RUnlock() + + for _, room := range roomsByNumber { + if room.state.Active { + results = append(results, &room.room) + spew.Dump(room.state) + } + } + + return results +} + +func (m *Storage) allocRoom(ctx context.Context) *Room { + entry := new(RoomEntry) + + m.resizeMu.Lock() + n := len(m.roomsByNumber) + m.roomsByNumber = append(m.roomsByNumber, entry) + m.resizeMu.Unlock() + + entry.state.RoomNumber = int16(n) + entry.state.Active = true + entry.room.state.RoomNumber = entry.state.RoomNumber + heap.Push(&m.roomHeap, entry) + return &entry.room +} + +// cull removes any extraneous rooms from the list/heap +func (m *Storage) cull() { + // We don't really want to cause too much contention. + // If things are busy, this process can just wait. + /* + if !m.resizeMu.TryLock() { + return + } + defer m.resizeMu.Unlock() + + i := len(m.roomsByNumber) - 1 + for ; i >= 0; i-- { + if m.roomsByNumber[i].state.Active { + break + } + } + + for j, l := i+1, len(m.roomsByNumber); j < l; j++ { + heap.Remove(&m.roomHeap, m.roomsByNumber[j].heapIndex) + m.roomsByNumber[j] = nil + } + + m.roomsByNumber = m.roomsByNumber[:i+1] + */ +} + +func (h RoomHeap) Len() int { return len(h) } + +func (h RoomHeap) Less(i, j int) bool { + if h[i].state.Active != h[j].state.Active { + ia := 0 + if h[i].state.Active { + ia = 1 + } + ja := 0 + if h[j].state.Active { + ja = 1 + } + return ia < ja + } + return h[i].state.RoomNumber < h[j].state.RoomNumber +} + +func (h RoomHeap) Swap(i, j int) { + h[i], h[j] = h[j], h[i] + h[i].heapIndex = i + h[j].heapIndex = j +} + +func (h *RoomHeap) Push(x any) { + n := len(*h) + room := x.(*RoomEntry) + room.heapIndex = n + *h = append(*h, room) +} + +func (h *RoomHeap) Pop() any { + old := *h + n := len(old) + room := old[n-1] + old[n-1] = nil + room.heapIndex = -1 + *h = old[0 : n-1] + return room +} diff --git a/game/server/auth.go b/game/server/auth.go new file mode 100644 index 0000000..ad2cf75 --- /dev/null +++ b/game/server/auth.go @@ -0,0 +1,162 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package gameserver + +import ( + "context" + "fmt" + + "github.com/bufbuild/connect-go" + "github.com/pangbox/server/common" + gamepacket "github.com/pangbox/server/game/packet" + "github.com/pangbox/server/gen/proto/go/topologypb" + "github.com/pangbox/server/pangya" + log "github.com/sirupsen/logrus" +) + +func (c *Conn) handleAuth(ctx context.Context) error { + err := c.SendHello(&gamepacket.ConnectMessage{ + Unknown: [8]byte{0x00, 0x06, 0x00, 0x00, 0x3f, 0x00, 0x01, 0x01}, + }) + if err != nil { + return fmt.Errorf("sending hello message: %w", err) + } + + msg, err := c.ReadMessage() + if err != nil { + return fmt.Errorf("reading handshake: %w", err) + } + + switch t := msg.(type) { + case *gamepacket.ClientAuth: + c.session, err = c.s.accountsService.GetSessionByKey(ctx, t.LoginKey.Value) + if err != nil { + // TODO: error handling + return nil + } + c.player, err = c.s.accountsService.GetPlayer(ctx, c.session.PlayerID) + if err != nil { + // TODO: error handling + return nil + } + log.Debugf("Client auth: %#v", msg) + + default: + return fmt.Errorf("expected client auth, got %T", t) + } + + c.characters, err = c.s.accountsService.GetCharacters(ctx, c.session.PlayerID) + if err != nil { + // TODO: handle error for client + return fmt.Errorf("database error: %w", err) + } + + c.connID = uint32(c.session.SessionID) + + // TODO: need data modelling + c.playerData = pangya.PlayerData{ + UserInfo: pangya.PlayerInfo{ + Username: c.player.Username, + Nickname: c.player.Nickname.String, + PlayerID: uint32(c.player.PlayerID), + ConnID: c.connID, + }, + PlayerStats: pangya.PlayerStats{ + Pang: uint64(c.player.Pang), + }, + Items: pangya.PlayerEquipment{ + CaddieID: 0, + CharacterID: c.characters[0].ID, + ClubSetID: 0x1754, + CometTypeID: 0x14000000, + Items: pangya.PlayerEquippedItems{ + ItemIDs: [10]uint32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + }, + EquippedCharacter: c.characters[0], + EquippedClub: pangya.PlayerClubData{ + Item: pangya.PlayerItem{ + ID: 0x1754, + TypeID: 0x10000000, + }, + Stats: pangya.ClubStats{ + UpgradeStats: [5]uint16{8, 9, 8, 3, 3}, + }, + }, + } + + c.SendMessage(ctx, &gamepacket.ServerUserData{ + SubType: 0, + MainData: &gamepacket.PlayerMainData{ + ClientVersion: common.ToPString("824.00"), + ServerVersion: common.ToPString("Pangbox"), + Game: 0xFFFF, + PlayerData: c.playerData, + }, + }) + + c.SendMessage(ctx, &gamepacket.ServerCharData{ + Count1: uint16(len(c.characters)), + Count2: uint16(len(c.characters)), + Characters: c.characters, + }) + + c.SendMessage(ctx, &gamepacket.ServerAchievementProgress{ + Remaining: 0, + Count: 0, + }) + + c.SendMessage(ctx, &gamepacket.ServerMessageConnect{}) + + c.sendServerList(ctx) + + return nil +} + +func (c *Conn) sendServerList(ctx context.Context) error { + message := &gamepacket.ServerChannelList{} + response, err := c.s.topologyClient.ListServers(ctx, connect.NewRequest(&topologypb.ListServersRequest{ + Type: topologypb.Server_TYPE_GAME_SERVER, + })) + if err != nil { + return err + } + for _, server := range response.Msg.Server { + entry := pangya.ServerEntry{ + ServerName: server.Name, + ServerID: server.Id, + NumUsers: server.NumUsers, + MaxUsers: server.MaxUsers, + IPAddress: server.Address, + Port: uint16(server.Port), + Flags: uint16(server.Flags), + } + if server.Id == c.s.serverID { + // TODO: support multiple channels? + entry.Channels = append(entry.Channels, pangya.ChannelEntry{ + ChannelName: c.s.channelName, + MaxUsers: 200, // TODO + NumUsers: 0, // TODO + Unknown2: 0x0008, // TODO + }) + } + message.Servers = append(message.Servers, entry) + } + message.Count = uint8(len(response.Msg.Server)) + return c.SendMessage(ctx, message) +} diff --git a/game/server/conn.go b/game/server/conn.go new file mode 100755 index 0000000..2e020c9 --- /dev/null +++ b/game/server/conn.go @@ -0,0 +1,455 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package gameserver + +import ( + "context" + "fmt" + + "github.com/pangbox/server/common" + gamemodel "github.com/pangbox/server/game/model" + gamepacket "github.com/pangbox/server/game/packet" + "github.com/pangbox/server/game/room" + "github.com/pangbox/server/gen/dbmodels" + "github.com/pangbox/server/pangya" +) + +// Conn holds the state for a connection to the server. +type Conn struct { + *gamepacket.ServerConn + s *Server + + connID uint32 + session dbmodels.Session + player dbmodels.Player + playerData pangya.PlayerData + characters []pangya.PlayerCharacterData + + currentLobby *room.Lobby + currentRoom *room.Room +} + +func (c *Conn) getLobbyPlayer() gamemodel.LobbyPlayer { + return gamemodel.LobbyPlayer{ + PlayerID: uint32(c.player.PlayerID), + ConnID: c.connID, + RoomNumber: c.currentRoom.Number(), + Nickname: c.player.Nickname.String, + Rank: byte(c.player.Rank), + GuildEmblemImage: "guildmark", // TODO + GlobalID: c.player.Username, // TODO + } +} + +func (c *Conn) getRoomPlayer() *gamemodel.RoomPlayerEntry { + return &gamemodel.RoomPlayerEntry{ + ConnID: c.connID, + Nickname: c.player.Nickname.String, + Rank: uint8(c.player.Rank), + GuildName: "", + CharTypeID: c.playerData.EquippedCharacter.CharTypeID, + StatusFlags: 0, + GuildEmblemImage: "guildmark", // TODO + PlayerID: uint32(c.player.PlayerID), + CharacterData: c.playerData.EquippedCharacter, + } +} + +func (c *Conn) leaveRoom(ctx context.Context) error { + if c.currentRoom != nil { + promise, err := c.currentRoom.Send(ctx, room.RoomPlayerLeave{ + ConnID: c.connID, + }) + if err != nil { + return err + } + _, err = promise.Wait(ctx) + if err != nil { + return err + } + c.currentRoom = nil + } + return nil +} + +func (c *Conn) leaveMultiplayerLobby(ctx context.Context) error { + if c.currentLobby != nil { + promise, err := c.currentLobby.Send(ctx, room.LobbyPlayerLeave{ + ConnID: c.connID, + }) + if err != nil { + return err + } + _, err = promise.Wait(ctx) + if err != nil { + return err + } + c.currentLobby = nil + } + return nil +} + +// Handle runs the main connection loop. +func (c *Conn) Handle(ctx context.Context) error { + log := c.s.logger + + // Handle the authentication phase. + if err := c.handleAuth(ctx); err != nil { + return err + } + + defer func() { + c.leaveRoom(ctx) + c.leaveMultiplayerLobby(ctx) + }() + + for { + msg, err := c.ReadMessage() + if err != nil { + return fmt.Errorf("reading next message: %w", err) + } + + switch t := msg.(type) { + case *gamepacket.ClientException: + log.WithField("exception", t.Message).Debug("Client exception") + case *gamepacket.ClientMessageSend: + chatMsg := room.ChatMessage{ + Nickname: t.Nickname.Value, + Message: t.Message.Value, + } + if c.currentRoom != nil { + c.currentRoom.Send(ctx, chatMsg) + } else if c.currentLobby != nil { + c.currentLobby.Send(ctx, chatMsg) + } + case *gamepacket.ClientRequestMessengerList: + // TODO + log.Debug("TODO: messenger list") + case *gamepacket.ClientGetUserOnlineStatus: + // TODO + log.Debug("TODO: online status") + case *gamepacket.ClientGetUserData: + // TODO + log.Debug("TODO: user data") + case *gamepacket.ClientRoomLoungeAction: + if c.currentRoom == nil { + break + } + c.currentRoom.Send(ctx, room.RoomAction{ + ConnID: c.connID, + Action: t.RoomAction, + }) + case *gamepacket.ClientRequestServerList: + c.sendServerList(ctx) + case *gamepacket.ClientRoomCreate: + if c.currentLobby == nil { + break + } + newRoom, err := c.currentLobby.NewRoom(context.Background(), gamemodel.RoomState{ + ShotTimerMS: t.ShotTimerMS, + GameTimerMS: t.GameTimerMS, + MaxUsers: t.MaxUsers, + RoomType: t.RoomType, + NumHoles: t.NumHoles, + Course: t.Course, + RoomName: t.RoomName.Value, + Password: t.Password.Value, + // TODO: natural wind, hole progression, more? + }) + if err != nil { + // TODO: handle error + return err + } + c.currentRoom = newRoom + c.currentRoom.Send(ctx, room.RoomPlayerJoin{ + Entry: c.getRoomPlayer(), + Conn: c.ServerConn, + PlayerData: c.playerData, + }) + case *gamepacket.ClientAssistModeToggle: + c.SendMessage(ctx, &gamepacket.ServerAssistModeToggled{}) + // TODO: Should send user status update; need to look at packet dumps. + case *gamepacket.ClientSetIdleStatus: + c.currentRoom.Send(ctx, room.RoomPlayerIdle{ + ConnID: c.connID, + Idle: t.Idle, + }) + case *gamepacket.ClientPlayerReady: + ready := false + if t.State == 0 { + ready = true + } + c.currentRoom.Send(ctx, room.RoomPlayerReady{ + ConnID: c.connID, + Ready: ready, + }) + case *gamepacket.ClientPlayerStartGame: + c.currentRoom.Send(ctx, room.RoomStartGame{ + ConnID: c.connID, + }) + case *gamepacket.ClientLoadProgress: + // TODO: publish to game room + if c.currentRoom == nil { + break + } + c.currentRoom.Send(ctx, room.RoomLoadingProgress{ + ConnID: c.connID, + Progress: t.Progress, + }) + case *gamepacket.ClientReadyStartHole: + c.currentRoom.Send(ctx, room.RoomGameReady{ + ConnID: c.connID, + }) + case *gamepacket.ClientShotCommit: + c.currentRoom.Send(ctx, room.RoomGameShotCommit{ + ConnID: c.connID, + ShotStrength: t.ShotStrength, + ShotAccuracy: t.ShotAccuracy, + ShotEnglishCurve: t.ShotEnglishCurve, + ShotEnglishSpin: t.ShotEnglishSpin, + Unknown2: t.Unknown2, + Unknown3: t.Unknown3, + }) + case *gamepacket.ClientShotSync: + c.currentRoom.Send(ctx, room.RoomGameShotSync{ + ConnID: c.connID, + Data: t.Data, + }) + case *gamepacket.ClientShotRotate: + c.currentRoom.Send(ctx, room.RoomGameShotRotate{ + ConnID: c.connID, + Angle: t.Angle, + }) + case *gamepacket.ClientShotMeterInput: + // TODO? + case *gamepacket.ClientShotArrow: + // TODO? + case *gamepacket.ClientShotPower: + c.currentRoom.Send(ctx, room.RoomGameShotPower{ + ConnID: c.connID, + Level: t.Level, + }) + case *gamepacket.ClientShotClubChange: + c.currentRoom.Send(ctx, room.RoomGameShotClubChange{ + ConnID: c.connID, + Club: t.Club, + }) + case *gamepacket.ClientShotItemUse: + c.currentRoom.Send(ctx, room.RoomGameShotItemUse{ + ConnID: c.connID, + ItemTypeID: t.ItemTypeID, + }) + case *gamepacket.ClientUserTypingIndicator: + c.currentRoom.Send(ctx, room.RoomGameTypingIndicator{ + ConnID: c.connID, + Status: t.Status, + }) + case *gamepacket.ClientShotCometRelief: + c.currentRoom.Send(ctx, room.RoomGameShotCometRelief{ + ConnID: c.connID, + X: t.X, + Y: t.Y, + Z: t.Z, + }) + case *gamepacket.ClientRoomSync: + c.currentRoom.Send(ctx, room.RoomGameTurnEnd{ + ConnID: c.connID, + }) + case *gamepacket.ClientHoleEnd: + c.currentRoom.Send(ctx, room.RoomGameHoleEnd{ + ConnID: c.connID, + }) + case *gamepacket.ClientGameEnd: + // TODO + case *gamepacket.ClientPauseGame: + // TODO + case *gamepacket.ClientShotActiveUserAcknowledge: + c.currentRoom.Send(ctx, room.RoomGameTurn{ + ConnID: c.connID, + }) + case *gamepacket.ClientFirstShotReady: + c.SendMessage(ctx, &gamepacket.ServerPlayerFirstShotReady{}) + case *gamepacket.ClientRoomInfo: + room := c.currentLobby.GetRoom(ctx, int16(t.RoomNumber)) + if room != nil { + info, err := room.GetRoomInfo(ctx) + if err != nil { + log.Printf("Error getting room info: %v", err) + } else { + c.SendMessage(ctx, &gamepacket.ServerRoomInfoResponse{ + RoomInfo: info, + }) + } + } + case *gamepacket.ClientRequestInboxList: + // TODO: need new sql message table + msg := &gamepacket.ServerInboxList{ + PageNum: t.PageNum, + NumPages: 1, + NumMessages: 1, + Messages: []gamepacket.InboxMessage{ + {ID: 0x1, SenderNickname: "@Pangbox"}, + }, + } + c.DebugMsg(msg) + c.SendMessage(ctx, msg) + case *gamepacket.ClientRequestInboxMessage: + c.SendMessage(ctx, &gamepacket.ServerMailMessage{ + Message: gamepacket.MailMessage{ + ID: 0x1, + SenderNickname: common.ToPString("@Pangbox"), + DateTime: common.ToPString("2023-06-03 01:21:00"), + Message: common.ToPString("Welcome to the first Pangbox server release! Not much works yet..."), + }, + }) + case *gamepacket.Client001A: + // Do nothing. + case *gamepacket.ClientJoinChannel: + c.SendMessage(ctx, &gamepacket.Server004E{Unknown: []byte{0x01}}) + c.SendMessage(ctx, &gamepacket.Server01F6{Unknown: []byte{0x00, 0x00, 0x00, 0x00}}) + c.SendMessage(ctx, &gamepacket.ServerLoginBonusStatus{Unknown: []byte{0x0, 0x0, 0x0, 0x0, 0x1, 0x4, 0x0, 0x0, 0x18, 0x3, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x18, 0x3, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0}}) + case *gamepacket.ClientRequestDailyReward: + c.SendMessage(ctx, &gamepacket.ServerMoneyUpdate{ + Type: uint16(gamepacket.MoneyUpdateRewardUnknown), + RewardUnknown: &gamepacket.UpdateRewardUnknownData{ + Unknown: 1, + }, + }) + case *gamepacket.ClientRequestPlayerHistory: + c.SendMessage(ctx, &gamepacket.ServerPlayerHistory{}) + case *gamepacket.ClientMultiplayerJoin: + if c.currentLobby != nil { + break + } + log.Println("Join Lobby") + c.currentLobby = c.s.lobby + c.currentLobby.Send(ctx, room.LobbyPlayerJoin{ + Entry: c.getLobbyPlayer(), + Conn: c.ServerConn, + }) + case *gamepacket.ClientMultiplayerLeave: + if err := c.leaveMultiplayerLobby(ctx); err != nil { + // TODO: handle error + return err + } + case *gamepacket.ClientEventLobbyJoin: + // TODO + c.SendMessage(ctx, &gamepacket.ServerEventLobbyJoined{}) + case *gamepacket.ClientEventLobbyLeave: + // TODO + c.SendMessage(ctx, &gamepacket.ServerEventLobbyLeft{}) + case *gamepacket.ClientRoomJoin: + if c.currentLobby == nil || c.currentRoom != nil { + break + } + joinRoom := c.currentLobby.GetRoom(context.Background(), t.RoomNumber) + if joinRoom != nil { + joinRoom.Send(ctx, room.RoomPlayerJoin{ + Entry: c.getRoomPlayer(), + Conn: c.ServerConn, + PlayerData: c.playerData, + }) + c.currentRoom = joinRoom + } + case *gamepacket.ClientRoomLeave: + if err := c.leaveRoom(ctx); err != nil { + // TODO: handle error + return err + } + case *gamepacket.ClientRoomKick: + if c.currentRoom == nil { + break + } + c.currentRoom.Send(ctx, room.RoomPlayerKick{ + ConnID: c.connID, + KickConnID: t.ConnID, + }) + case *gamepacket.ClientRoomEdit: + if c.currentRoom == nil { + break + } + c.currentRoom.Send(ctx, room.RoomSettingsChange{ + ConnID: c.connID, + Changes: t.Changes, + }) + case *gamepacket.Client0088: + // Unknown tutorial-related message. + case *gamepacket.ClientRoomUserEquipmentChange: + // TODO + case *gamepacket.ClientTutorialStart: + // TODO + c.SendMessage(ctx, &gamepacket.ServerRoomEquipmentData{ + Unknown: []byte{ + 0x00, 0x00, 0x00, 0x01, 0x04, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x04, 0xdd, + 0x77, 0x94, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x14, 0x08, 0x00, 0x24, 0x14, 0x08, 0x00, + 0x44, 0x14, 0x08, 0x00, 0x64, 0x14, 0x08, 0x00, 0x84, 0x14, 0x08, 0x00, 0xa4, 0x14, 0x08, 0x00, + 0xc4, 0x14, 0x08, 0x00, 0xe4, 0x14, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }) + case *gamepacket.ClientTutorialClear: + // After clearing first tutorial + // TODO + c.SendMessage(ctx, &gamepacket.ServerTutorialStatus{ + Unknown: [6]byte{0x00, 0x01, 0x03, 0x00, 0x00, 0x00}, + }) + case *gamepacket.ClientUserMacrosSet: + // TODO: server-side macro storage + log.Debugf("Set macros: %+v", t.MacroList) + case *gamepacket.ClientEquipmentUpdate: + // TODO + log.Debug("TODO: 0020") + case *gamepacket.Client00FE: + // TODO + log.Debug("TODO: 00FE") + case *gamepacket.ClientShopJoin: + // Enter shop, not sure what responses need to go here? + log.Debug("TODO: 0140") + default: + return fmt.Errorf("unexpected message: %T", t) + } + } +} diff --git a/game/server.go b/game/server/server.go similarity index 84% rename from game/server.go rename to game/server/server.go index 779f488..11712e3 100755 --- a/game/server.go +++ b/game/server/server.go @@ -15,7 +15,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick // SPDX-License-Identifier: ISC -package game +package gameserver import ( "context" @@ -23,6 +23,8 @@ import ( "github.com/pangbox/server/common" "github.com/pangbox/server/database/accounts" + gamepacket "github.com/pangbox/server/game/packet" + "github.com/pangbox/server/game/room" "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" "github.com/pangbox/server/pangya/iff" log "github.com/sirupsen/logrus" @@ -45,10 +47,13 @@ type Server struct { pangyaIFF *iff.Archive serverID uint32 channelName string + lobby *room.Lobby + logger *log.Entry } // New creates a new instance of the game server. func New(opts Options) *Server { + logger := log.WithField("server", "GameServer") return &Server{ baseServer: &common.BaseServer{}, topologyClient: opts.TopologyClient, @@ -56,20 +61,21 @@ func New(opts Options) *Server { pangyaIFF: opts.PangyaIFF, serverID: opts.ServerID, channelName: opts.ChannelName, + logger: logger, } } // Listen listens for connections on a given address and blocks indefinitely. func (s *Server) Listen(ctx context.Context, addr string) error { - logger := log.WithField("server", "GameServer") - return s.baseServer.Listen(logger, addr, func(logger *log.Entry, socket net.Conn) error { + s.lobby = room.NewLobby(ctx, s.logger) + return s.baseServer.Listen(s.logger, addr, func(logger *log.Entry, socket net.Conn) error { conn := Conn{ - ServerConn: common.ServerConn[ClientMessage, ServerMessage]{ - Socket: socket, - Log: logger, - ClientMsg: ClientMessageTable, - ServerMsg: ServerMessageTable, - }, + ServerConn: common.NewServerConn( + socket, + logger, + gamepacket.ClientMessageTable, + gamepacket.ServerMessageTable, + ), s: s, } return conn.Handle(ctx) diff --git a/go.mod b/go.mod index 909be38..f84f4a9 100755 --- a/go.mod +++ b/go.mod @@ -3,26 +3,35 @@ module github.com/pangbox/server go 1.20 require ( + github.com/akavel/rsrc v0.10.2 github.com/bufbuild/buf v1.19.0 github.com/bufbuild/connect-go v1.7.0 github.com/davecgh/go-spew v1.1.1 github.com/go-restruct/restruct v1.2.0-alpha github.com/google/uuid v1.3.0 github.com/jackc/pgx/v5 v5.3.1 + github.com/josephspurrier/goversioninfo v1.4.0 + github.com/julienschmidt/httprouter v1.3.0 github.com/kyleconroy/sqlc v1.18.0 - github.com/lib/pq v1.10.9 + github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 + github.com/manifoldco/promptui v0.9.0 github.com/pangbox/pangcrypt v0.0.0-20181124232112-60117463a15d github.com/pangbox/pangfiles v0.0.2-alpha.0.20230603175117-d8085775e1b5 - github.com/pkg/errors v0.9.1 + github.com/pangbox/rugburn v0.0.0-20230528190157-e19211ef1659 github.com/pressly/goose/v3 v3.11.2 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.2 github.com/syndtr/goleveldb v1.0.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.20.1 + github.com/wk8/go-ordered-map/v2 v2.1.7 github.com/xo/dburl v0.14.2 golang.org/x/crypto v0.9.0 + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 golang.org/x/net v0.10.0 + golang.org/x/sync v0.2.0 + golang.org/x/sys v0.8.0 google.golang.org/protobuf v1.30.0 + gvisor.dev/gvisor v0.0.0-20230615202126-6c8187194adf modernc.org/sqlite v1.22.1 ) @@ -30,11 +39,12 @@ require ( bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/akavel/rsrc v0.10.2 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/benbjohnson/clock v1.3.3 // indirect github.com/billziss-gh/cgofuse v1.5.0 // indirect github.com/bufbuild/protocompile v0.5.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/bytecodealliance/wasmtime-go/v8 v8.0.0 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/chzyer/readline v1.5.1 // indirect @@ -67,16 +77,12 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/josephspurrier/goversioninfo v1.4.0 // indirect - github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/pgzip v1.2.6 // indirect - github.com/lxn/polyglot v0.0.0-20120605161256-3427b7be6a34 // indirect - github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 // indirect github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/manifoldco/promptui v0.9.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/patternmatcher v0.5.0 // indirect @@ -86,12 +92,12 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect github.com/opencontainers/runc v1.1.7 // indirect - github.com/pangbox/rugburn v0.0.0-20230528190157-e19211ef1659 // indirect github.com/pganalyze/pg_query_go/v4 v4.2.0 // indirect github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 // indirect github.com/pingcap/tidb/parser v0.0.0-20220725134311-c80026e61f00 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pkg/profile v1.7.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rasky/go-lzo v0.0.0-20151023001055-affec0788321 // indirect @@ -111,10 +117,7 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/sync v0.2.0 // indirect - golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/tools v0.9.1 // indirect diff --git a/go.sum b/go.sum index a3597dc..fbdf616 100755 --- a/go.sum +++ b/go.sum @@ -2,7 +2,6 @@ bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05 h1:UrYe9YkT4Wpm6D+zByEyCJQzDqT bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05/go.mod h1:h0h5FBYpXThbvSfTqthw+0I4nmHnhTHkO5BoOHsBWqg= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= @@ -36,6 +35,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.3 h1:g+rSsSaAzhHJYcIQE78hJ3AhyjjtQvleKDjlhdBnIhc= github.com/benbjohnson/clock v1.3.3/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -43,8 +44,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/billziss-gh/cgofuse v1.3.0 h1:mFj8XQg/vvxMFywNy1F7IqFYcMeBqceYTh1+iUhpsk8= -github.com/billziss-gh/cgofuse v1.3.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM= github.com/billziss-gh/cgofuse v1.5.0 h1:kH516I/s+Ab4diL/Y/ayFeUjjA8ey+JK12xDfBf4HEs= github.com/billziss-gh/cgofuse v1.5.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM= github.com/bufbuild/buf v1.19.0 h1:8c/XL39hO2hGgKWgUnRT3bCc8KvMa+V1jpoWWdN2bsw= @@ -53,6 +52,8 @@ github.com/bufbuild/connect-go v1.7.0 h1:MGp82v7SCza+3RhsVhV7aMikwxvI3ZfD72YiGt8 github.com/bufbuild/connect-go v1.7.0/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk= github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg= github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytecodealliance/wasmtime-go/v8 v8.0.0 h1:jP4sqm2PHgm3+eQ50zCoCdIyQFkIL/Rtkw6TT8OYPFI= github.com/bytecodealliance/wasmtime-go/v8 v8.0.0/go.mod h1:tgazNLU7xSC2gfRAM8L4WyE+dgs5yp9FF5/tGebEQyM= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= @@ -62,11 +63,13 @@ github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -142,7 +145,6 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -248,6 +250,7 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -274,11 +277,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kyleconroy/sqlc v1.18.0 h1:+a0Fc0sRCGCFyeSLiA9iKbHXufRGOWOqZjCdtfSgvL8= github.com/kyleconroy/sqlc v1.18.0/go.mod h1:FfVkspjWAR6NHR9BwfwqRK0UvrrAq9vT1WGn8q+v9Ug= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lxn/polyglot v0.0.0-20120605161256-3427b7be6a34 h1:HoxaEYFKd1Vw/lvZp6WFt8M/ArvepjowSQeFOi238TE= -github.com/lxn/polyglot v0.0.0-20120605161256-3427b7be6a34/go.mod h1:9aD+HECjP98XuvEqwLeehq0tXwnzKcAksbcpd6ILZKg= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= @@ -286,6 +286,8 @@ github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -353,8 +355,6 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pangbox/pangcrypt v0.0.0-20181124232112-60117463a15d h1:WZa3yBhrPN1+6aFHkQhD5xbTttYwxVw0RsXG5lsJOj0= github.com/pangbox/pangcrypt v0.0.0-20181124232112-60117463a15d/go.mod h1:5Bd6YEPYgUrM2LygkXw8oMERGUItIxlxdpnmuE+5pkM= -github.com/pangbox/pangfiles v0.0.1 h1:Zj9prIhbbMdgqHh7s8DBiM8nRUojpJlpt2dCKbmKkdI= -github.com/pangbox/pangfiles v0.0.1/go.mod h1:3AiPs/VZB0+xPxB2C5HVGPyUMVh2XoSybq9Bu3Z2ngE= github.com/pangbox/pangfiles v0.0.2-alpha.0.20230603175117-d8085775e1b5 h1:afO/IX2XRr9hUaXoFm0LkEj99xM4KgSci7I0eaEK1Fc= github.com/pangbox/pangfiles v0.0.2-alpha.0.20230603175117-d8085775e1b5/go.mod h1:OOPiXQJWNAvkaBIKWeUgG4t3hcqYeQxIJKJspS8BAJI= github.com/pangbox/rugburn v0.0.0-20230528190157-e19211ef1659 h1:JqL1lkshTqE7jmA14REvJt8MNFYFXfXwCQiaunPF3Yk= @@ -476,6 +476,8 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/wk8/go-ordered-map/v2 v2.1.7 h1:aUZ1xBMdbvY8wnNt77qqo4nyT3y0pX4Usat48Vm+hik= +github.com/wk8/go-ordered-map/v2 v2.1.7/go.mod h1:9Xvgm2mV2kSq2SAm0Y608tBmu8akTzI7c2bz7/G7ZN4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xo/dburl v0.14.2 h1:tqiXv1glyxFph3LA39RXE4TYidr/yp7kG2YDrgJVjiA= github.com/xo/dburl v0.14.2/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= @@ -530,9 +532,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20210715201039-d37aa40e8013/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= @@ -544,12 +544,10 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -596,13 +594,11 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -647,7 +643,6 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200423201157-2723c5de0d66/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -719,6 +714,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gvisor.dev/gvisor v0.0.0-20230615202126-6c8187194adf h1:HgN+lHLgclBB7X4c/fKYfk30vGMTWkMAOtsdi8o11BI= +gvisor.dev/gvisor v0.0.0-20230615202126-6c8187194adf/go.mod h1:sQuqOkxbfJq/GS2uSnqHphtXclHyk/ZrAGhZBxxsq6g= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/login/conn.go b/login/conn.go index cbae9c9..b4855bd 100755 --- a/login/conn.go +++ b/login/conn.go @@ -19,12 +19,9 @@ package login import ( "context" - "encoding/binary" "fmt" - "math/rand" "github.com/bufbuild/connect-go" - "github.com/go-restruct/restruct" "github.com/pangbox/server/common" "github.com/pangbox/server/database/accounts" "github.com/pangbox/server/gen/dbmodels" @@ -35,34 +32,11 @@ import ( // Conn holds the state for a connection to the server. type Conn struct { - common.ServerConn[ClientMessage, ServerMessage] + *common.ServerConn[ClientMessage, ServerMessage] topologyClient topologypbconnect.TopologyServiceClient accountsService *accounts.Service } -// SendHello sends the initial handshake bytes to the client. -func (c *Conn) SendHello() error { - data, err := restruct.Pack(binary.LittleEndian, &ConnectMessage{ - Unknown1: 0x0b00, - Unknown2: 0x0000, - Unknown3: 0x0000, - Key: uint16(c.Key), - Unknown4: 0x0000, - ServerID: 0x2775, - Unknown6: 0x0000, - }) - if err != nil { - return err - } - - _, err = c.Socket.Write(data) - if err != nil { - return err - } - - return nil -} - // GetServerList returns a server list using the topology store. func (c *Conn) GetServerList(ctx context.Context, typ topologypb.Server_Type) (*ServerList, error) { message := &ServerList{} @@ -90,10 +64,16 @@ func (c *Conn) GetServerList(ctx context.Context, typ topologypb.Server_Type) (* // Handle runs the main connection loop. func (c *Conn) Handle(ctx context.Context) error { - log := c.Log - c.Key = uint8(rand.Intn(16)) + log := c.Log() - err := c.SendHello() + err := c.SendHello(&ConnectMessage{ + Unknown1: 0x0b00, + Unknown2: 0x0000, + Unknown3: 0x0000, + Unknown4: 0x0000, + ServerID: 0x2775, + Unknown6: 0x0000, + }) if err != nil { return fmt.Errorf("sending hello: %w", err) } @@ -113,20 +93,19 @@ func (c *Conn) Handle(ctx context.Context) error { if err == accounts.ErrUnknownUsername || err == accounts.ErrInvalidPassword { log.Infof("Bad credentials.") - c.SendMessage(&ServerLogin{ + c.SendMessage(ctx, &ServerLogin{ Status: LoginStatusError, Error: &LoginError{ Error: LoginErrorInvalidCredentials, }, }) - c.Socket.Close() return nil } else if err != nil { return fmt.Errorf("database error during authentication: %w", err) } if !player.Nickname.Valid { - c.SendMessage(&ServerLogin{ + c.SendMessage(ctx, &ServerLogin{ Status: LoginStatusSetNickname, SetNickname: &LoginSetNickname{ Unknown: 0xFFFFFFFF, @@ -144,7 +123,7 @@ func (c *Conn) Handle(ctx context.Context) error { case *ClientCheckNickname: // TODO log.Infof("TODO: check nickname %s", t.Nickname.Value) - c.SendMessage(&ServerNicknameCheckResponse{ + c.SendMessage(ctx, &ServerNicknameCheckResponse{ Nickname: t.Nickname, }) case *ClientSetNickname: @@ -168,7 +147,7 @@ func (c *Conn) Handle(ctx context.Context) error { } if !haveCharacters { - c.SendMessage(&ServerLogin{ + c.SendMessage(ctx, &ServerLogin{ Status: LoginStatusSetCharacter, SetCharacter: &LoginSetCharacter{}, }) @@ -186,7 +165,7 @@ func (c *Conn) Handle(ctx context.Context) error { CharTypeID: t.CharacterID, HairColor: t.HairColor, }) - c.SendMessage(&Server0011{}) + c.SendMessage(ctx, &Server0011{}) break CharacterSetup default: return fmt.Errorf("expected ClientSelectCharacter, got %T", t) @@ -194,17 +173,17 @@ func (c *Conn) Handle(ctx context.Context) error { } } - session, err := c.accountsService.AddSession(ctx, player.PlayerID, c.Socket.RemoteAddr().String()) + session, err := c.accountsService.AddSession(ctx, player.PlayerID, c.RemoteAddr().String()) if err != nil { log.Errorf("Error creating session in DB: %v", err) } // TODO: make token - c.SendMessage(&ServerLoginSessionKey{ + c.SendMessage(ctx, &ServerLoginSessionKey{ SessionKey: common.ToPString(session.SessionKey), }) - c.SendMessage(&ServerLogin{ + c.SendMessage(ctx, &ServerLogin{ Success: &LoginSuccess{ Username: common.ToPString(player.Username), Nickname: common.ToPString(player.Nickname.String), @@ -217,14 +196,14 @@ func (c *Conn) Handle(ctx context.Context) error { if err != nil { return fmt.Errorf("listing message servers: %w", err) } - c.SendMessage(&ServerMessageServerList{ServerList: *messageServers}) + c.SendMessage(ctx, &ServerMessageServerList{ServerList: *messageServers}) log.Info("sending game server list") gameServers, err := c.GetServerList(ctx, topologypb.Server_TYPE_GAME_SERVER) if err != nil { return fmt.Errorf("listing game servers: %w", err) } - c.SendMessage(&ServerGameServerList{ServerList: *gameServers}) + c.SendMessage(ctx, &ServerGameServerList{ServerList: *gameServers}) log.Info("waiting for response.") msg, err = c.ReadMessage() @@ -239,7 +218,7 @@ func (c *Conn) Handle(ctx context.Context) error { return fmt.Errorf("expected ClientSelectServer, got %T", t) } - c.SendMessage(&ServerGameSessionKey{ + c.SendMessage(ctx, &ServerGameSessionKey{ SessionKey: common.ToPString(session.SessionKey), }) diff --git a/login/msgserver.go b/login/msgserver.go index a649050..e5443f9 100755 --- a/login/msgserver.go +++ b/login/msgserver.go @@ -44,6 +44,10 @@ type ConnectMessage struct { Unknown6 uint16 } +func (c *ConnectMessage) SetKey(key uint8) { + c.Key = uint16(key) +} + // ServerEntry represents a server in a ServerListMessage. type ServerEntry struct { ServerName string `struct:"[40]byte"` @@ -64,9 +68,9 @@ type ServerList struct { const ( LoginStatusSuccess = 0 + LoginStatusError = 1 LoginStatusSetNickname = 216 LoginStatusSetCharacter = 217 - LoginStatusError = 227 ) type LoginSuccess struct { @@ -84,9 +88,9 @@ type LoginSetCharacter struct { } const ( + LoginErrorInvalidCredentials = 0 LoginErrorAlreadyLoggedIn = 5100019 LoginErrorDuplicateConn = 5100107 - LoginErrorInvalidCredentials = 5100143 LoginErrorInvalidReconnectToken = 5157002 ) @@ -99,9 +103,9 @@ type ServerLogin struct { Status byte Success *LoginSuccess `struct-if:"Status == 0"` + Error *LoginError `struct-if:"Status == 1"` SetNickname *LoginSetNickname `struct-if:"Status == 216"` SetCharacter *LoginSetCharacter `struct-if:"Status == 217"` - Error *LoginError `struct-if:"Status == 227"` } type ServerGameServerList struct { diff --git a/login/server.go b/login/server.go index 843c9ba..f450c7b 100755 --- a/login/server.go +++ b/login/server.go @@ -54,12 +54,12 @@ func (s *Server) Listen(ctx context.Context, addr string) error { logger := log.WithField("server", "LoginServer") return s.baseServer.Listen(logger, addr, func(logger *log.Entry, socket net.Conn) error { conn := Conn{ - ServerConn: common.ServerConn[ClientMessage, ServerMessage]{ - Socket: socket, - Log: logger, - ClientMsg: ClientMessageTable, - ServerMsg: ServerMessageTable, - }, + ServerConn: common.NewServerConn( + socket, + logger, + ClientMessageTable, + ServerMessageTable, + ), topologyClient: s.topologyClient, accountsService: s.accountsService, } diff --git a/message/conn.go b/message/conn.go index 4f784ca..ca3c1a8 100755 --- a/message/conn.go +++ b/message/conn.go @@ -18,47 +18,27 @@ package message import ( - "encoding/binary" "fmt" - "math/rand" - "github.com/go-restruct/restruct" "github.com/pangbox/server/common" ) // Conn holds the state for a connection to the server. type Conn struct { - common.ServerConn[ClientMessage, ServerMessage] + *common.ServerConn[ClientMessage, ServerMessage] } -// SendHello sends the initial handshake bytes to the client. -func (c *Conn) SendHello() error { - data, err := restruct.Pack(binary.LittleEndian, &ConnectMessage{ +// Handle runs the main connection loop. +func (c *Conn) Handle() error { + log := c.Log() + + err := c.SendHello(&ConnectMessage{ Unknown1: 0x0900, Unknown2: 0x0000, Unknown3: 0x002E, Unknown4: 0x0101, - Key: uint16(c.Key), Unknown5: 0x0000, }) - if err != nil { - return err - } - - _, err = c.Socket.Write(data) - if err != nil { - return err - } - - return nil -} - -// Handle runs the main connection loop. -func (c *Conn) Handle() error { - log := c.Log - c.Key = uint8(rand.Intn(16)) - - err := c.SendHello() if err != nil { return fmt.Errorf("sending hello: %w", err) } diff --git a/message/msgserver.go b/message/msgserver.go index 445843c..7c7fb66 100755 --- a/message/msgserver.go +++ b/message/msgserver.go @@ -33,6 +33,10 @@ type ConnectMessage struct { Unknown5 uint16 } +func (c *ConnectMessage) SetKey(key uint8) { + c.Key = uint16(key) +} + // Server0001 is an unknown message. type Server0001 struct { ServerMessage_ diff --git a/message/server.go b/message/server.go index 4be6413..7d1faf8 100755 --- a/message/server.go +++ b/message/server.go @@ -54,12 +54,12 @@ func (s *Server) Listen(ctx context.Context, addr string) error { logger := log.WithField("server", "MessageServer") return s.baseServer.Listen(logger, addr, func(logger *log.Entry, socket net.Conn) error { conn := Conn{ - ServerConn: common.ServerConn[ClientMessage, ServerMessage]{ - Socket: socket, - Log: logger, - ClientMsg: ClientMessageTable, - ServerMsg: ServerMessageTable, - }, + ServerConn: common.NewServerConn( + socket, + logger, + ClientMessageTable, + ServerMessageTable, + ), } return conn.Handle() }) diff --git a/minibox/gameserver.go b/minibox/gameserver.go index 1da63da..5deb92e 100644 --- a/minibox/gameserver.go +++ b/minibox/gameserver.go @@ -21,7 +21,7 @@ import ( "context" "github.com/pangbox/server/database/accounts" - "github.com/pangbox/server/game" + gameserver "github.com/pangbox/server/game/server" "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" "github.com/pangbox/server/pangya/iff" log "github.com/sirupsen/logrus" @@ -48,7 +48,7 @@ func NewGameServer(ctx context.Context) *GameServer { func (g *GameServer) Configure(opts GameOptions) error { spawn := func(ctx context.Context, service *Service) { - gameServer := game.New(game.Options{ + gameServer := gameserver.New(gameserver.Options{ TopologyClient: opts.TopologyClient, AccountsService: opts.AccountsService, PangyaIFF: opts.PangyaIFF, diff --git a/minibox/minibox.go b/minibox/minibox.go index 582a551..f06476e 100644 --- a/minibox/minibox.go +++ b/minibox/minibox.go @@ -59,28 +59,42 @@ type Options struct { type Server struct { mu sync.RWMutex - log *log.Entry + log *log.Entry // +checklocksignore // Fabric services + // +checklocks:mu accountsService *accounts.Service // Network services + // +checklocks:mu Topology *TopologyServer - Web *WebServer - Admin *AdminServer - Login *LoginServer - Game *GameServer - Message *MessageServer - QAAuth *QAAuthServer + // +checklocks:mu + Web *WebServer + // +checklocks:mu + Admin *AdminServer + // +checklocks:mu + Login *LoginServer + // +checklocks:mu + Game *GameServer + // +checklocks:mu + Message *MessageServer + // +checklocks:mu + QAAuth *QAAuthServer // Misc + // +checklocks:mu Rugburn *RugburnPatcher - pangyaKey pyxtea.Key - pangyaFS *pak.FS - pangyaIFF *iff.Archive + // +checklocks:mu + pangyaKey pyxtea.Key + // +checklocks:mu + pangyaFS *pak.FS + // +checklocks:mu + pangyaIFF *iff.Archive + // +checklocks:mu lastDbOpts *DataOptions - lastOpts *Options + // +checklocks:mu + lastOpts *Options } func topologyOptions(opts Options) (TopologyServerOptions, error) { @@ -214,9 +228,9 @@ func (server *Server) ConfigureServices(opts Options) error { if server.lastOpts.ShouldConfigureWeb(opts) { if err := server.Web.Configure(WebOptions{ - Addr: opts.WebAddr, - PangyaKey: server.pangyaKey, - PangyaDir: opts.PangyaDir, + Addr: opts.WebAddr, + PangyaKey: server.pangyaKey, + PangyaDir: opts.PangyaDir, AccountsService: server.accountsService, }); err != nil { return fmt.Errorf("configuring web server: %w", err) diff --git a/minibox/rugburn.go b/minibox/rugburn.go index 20b2d45..2e8634d 100644 --- a/minibox/rugburn.go +++ b/minibox/rugburn.go @@ -35,11 +35,16 @@ type RugburnOptions struct { type RugburnPatcher struct { mu sync.RWMutex + // +checklocks:mu path string + // +checklocks:mu calc int64 - haveOrig bool - rugburnVer string + // +checklocks:mu + haveOrig bool + // +checklocks:mu + rugburnVer string + // +checklocks:mu rugburnVerErr error } @@ -55,14 +60,14 @@ func (p *RugburnPatcher) Configure(opts RugburnOptions) { } func (p *RugburnPatcher) recalc() error { + p.mu.Lock() + defer p.mu.Unlock() + finfo, err := os.Stat(p.path) if err != nil { return err } - p.mu.Lock() - defer p.mu.Unlock() - ncalc := finfo.Size() ^ finfo.ModTime().Unix() if p.calc == ncalc { return nil @@ -110,6 +115,9 @@ func (p *RugburnPatcher) HaveOriginal() bool { } func (p *RugburnPatcher) Patch() error { + p.mu.RLock() + defer p.mu.RUnlock() + ijl15, err := os.ReadFile(p.path) if err != nil { return err @@ -136,6 +144,9 @@ func (p *RugburnPatcher) Patch() error { } func (p *RugburnPatcher) Unpatch() error { + p.mu.RLock() + defer p.mu.RUnlock() + rugburn, err := os.ReadFile(p.path) if err != nil { return err diff --git a/minibox/service.go b/minibox/service.go index 4fc4681..6adcf0f 100644 --- a/minibox/service.go +++ b/minibox/service.go @@ -24,10 +24,10 @@ import ( "time" ) -var ErrServiceRunning = errors.New("service is already running") -var ErrServiceStopped = errors.New("service is already stopped") -var ErrServiceNotConfigured = errors.New("service is not configured") -var ErrStopping = errors.New("stopping service") +var ErrServiceRunning = errors.New("service is already running") // +checklocksignore +var ErrServiceStopped = errors.New("service is already stopped") // +checklocksignore +var ErrServiceNotConfigured = errors.New("service is not configured") // +checklocksignore +var ErrStopping = errors.New("stopping service") // +checklocksignore const ShutdownTimeout = 10 * time.Second diff --git a/pangya/iff/file.go b/pangya/iff/file.go index 86adb3b..0504dba 100644 --- a/pangya/iff/file.go +++ b/pangya/iff/file.go @@ -37,6 +37,7 @@ type Archive struct { // Filenames to look for to find client IFF. var iffSearchOrder = []string{ "pangya_gb.iff", + "pangya_us.iff", // older US, before global "pangya_jp.iff", "pangya_eu.iff", "pangya_th.iff", diff --git a/pangya/player.go b/pangya/player.go index fb0d64f..27e17d5 100755 --- a/pangya/player.go +++ b/pangya/player.go @@ -17,78 +17,133 @@ package pangya -type UserInfo struct { - Username string `struct:"[22]byte"` - Nickname string `struct:"[22]byte"` - Unknown [33]byte - GMFlag byte - Unknown2 [7]byte - ConnnectionID uint32 - Unknown3 [32]byte - ChatFlag byte - Unknown4 [139]byte - PlayerID uint32 +type PlayerInfo struct { + Username string `struct:"[22]byte"` + Nickname string `struct:"[22]byte"` + GuildName string `struct:"[17]byte"` + GuildEmblemImage string `struct:"[24]byte"` + ConnID uint32 + Unknown [12]byte + Unknown2 uint32 + Unknown3 uint32 + Unknown4 uint16 + Unknown5 [6]byte + Unknown6 [16]byte + GlobalID string `struct:"[128]byte"` + PlayerID uint32 } type PlayerStats struct { - Unknown uint32 TotalStrokes uint32 - TotalPlayTime uint32 - AverageStrokeTime uint32 - Unknown2 [12]byte - OBRate uint32 + TotalPutts uint32 + Time uint32 + TimeHitting uint32 + LongestDrive float32 + PangyaHits uint32 + Timeouts uint32 + OBs uint32 TotalDistance uint32 TotalHoles uint32 - Unknown3 uint32 - HIO uint32 - Unknown4 [26]byte - Experience uint32 - Rank Rank - Pangs uint64 - Unknown5 [58]byte - QuitRateY uint32 - Unknown6 [32]byte - GameComboX uint32 - GameComboY uint32 - QuitRateX uint32 - TotalPangsWin uint64 - Unknown7 [38]byte + HoleUnfinished uint32 //Holes that you don't end up putting/chipping in due to mode; e.g. match play + TotalHIO uint32 + BunkersHit uint16 + FairwaysHit uint32 + TotalAlbatross uint32 + Warnings uint32 + PuttIns uint32 + LongestPutt float32 + LongestChip float32 + TotalXP uint32 + Level byte + Pang uint64 + TotalScore int32 + Difficulty1Score uint8 //Not 100% sure on these + Difficulty2Score uint8 + Difficulty3Score uint8 + Difficulty4Score uint8 + Difficulty5Score uint8 + UnknownFlag uint8 //possibly total? + BestPang1 uint64 + BestPang2 uint64 + BestPang3 uint64 + BestPang4 uint64 + BestPang5 uint64 + BestPangTotal uint64 + GamesPlayed uint32 + TeamHole uint32 + TeamWin uint32 + TeamGame uint32 + LadderMMR uint32 + LadderHoles uint32 + LadderWins uint32 + LadderLosses uint32 + LadderDraws uint32 + ComboNum uint32 + ComboDenom uint32 + Quits uint32 + PangBattleTotal int32 + PangBattleWins uint32 + PangBattleLosses uint32 + PangBattleAllIn uint32 + PangBattleCombo uint32 + PangBattleUnknown uint32 //could be first medal - there are 6 + Unknown24 [10]byte //other 5 medals? However, this also could be school related stuff according to jp + GameCountSeason uint32 + Unknown26 [8]byte // } type PlayerEquippedItems struct { ItemIDs [10]uint32 } -type Decorations struct { - Background uint32 - Frame uint32 - Sticker uint32 - Slot uint32 - Unknown uint32 - Title uint32 -} - type PlayerEquipment struct { CaddieID uint32 CharacterID uint32 ClubSetID uint32 - AztecIffID uint32 + CometTypeID uint32 Items PlayerEquippedItems - Unknown13 uint32 - Unknown14 uint32 - Unknown15 uint32 - Unknown16 uint32 - Unknown17 uint32 - Unknown18 uint32 + BackgroundID uint32 + FrameID uint32 + StickerID uint32 + SlotID uint32 + SlotCutInID uint32 + SlotRankBannerID uint32 - Decorations Decorations + BackgroundTypeID uint32 + FrameTypeID uint32 + StickerTypeID uint32 + SlotTypeID uint32 + CutInTypeID uint32 + TitleTypeID uint32 MascotID uint32 + PosterID [2]uint32 +} + +type PlayerCourseData struct { + CourseID uint8 + TotalStrokes uint32 + TotalPutts uint32 + NumHoles uint32 + Unknown uint32 + Unknown2 uint32 + Unknown3 uint32 + TotalScore uint32 + BestScore int8 + BestPang uint32 + Unknown4 uint32 + CharTypeID uint32 + Unknown5 int8 +} + +type PlayerSeasonData struct { + Courses [21]PlayerCourseData +} - Unknown26 uint32 - Unknown27 uint32 +type PlayerSeasonHistory struct { + Seasons [12]PlayerSeasonData } type PlayerCharacterData struct { @@ -103,7 +158,7 @@ type PlayerCharacterData struct { Unknown3 [216]byte AuxParts [5]uint32 CutInID uint32 - Unknown4 [12]byte + Unknown4 [16]byte Stats [5]byte Mastery int CardChar [4]uint32 @@ -112,8 +167,8 @@ type PlayerCharacterData struct { } type PlayerItem struct { - ID uint32 - IFFID uint32 + ID uint32 + TypeID uint32 } type PlayerCaddieData struct { @@ -139,11 +194,11 @@ type PlayerMascotData struct { } type PlayerData struct { - UserInfo UserInfo + UserInfo PlayerInfo PlayerStats PlayerStats - Unknown [78]byte + Trophy [13][3]uint16 Items PlayerEquipment - JunkData [10836]byte + SeasonHistory PlayerSeasonHistory EquippedCharacter PlayerCharacterData EquippedCaddie PlayerCaddieData EquippedClub PlayerClubData diff --git a/pangya/systemtime.go b/pangya/systemtime.go index b87a16f..7bae013 100644 --- a/pangya/systemtime.go +++ b/pangya/systemtime.go @@ -17,12 +17,40 @@ package pangya +import "time" + type SystemTime struct { /* 0x00 */ Year, Month, DayOfWeek, Day uint16 /* 0x08 */ Hour, Minute, Second, Milliseconds uint16 /* 0x10 */ } +func NewSystemTime(t time.Time) SystemTime { + return SystemTime{ + Year: uint16(t.Year()), + Month: uint16(t.Month()), + DayOfWeek: uint16(t.Weekday()), + Day: uint16(t.Day()), + Hour: uint16(t.Hour()), + Minute: uint16(t.Minute()), + Second: uint16(t.Second()), + Milliseconds: uint16(t.Nanosecond() / int(time.Millisecond)), + } +} + +func (s SystemTime) Time() time.Time { + return time.Date( + int(s.Year), + time.Month(s.Month), + int(s.Day), + int(s.Hour), + int(s.Minute), + int(s.Second), + int(s.Milliseconds)*int(time.Millisecond), + time.UTC, + ) +} + func (s SystemTime) IsZero() bool { return (s.Year == 0 && s.Month == 0 && s.DayOfWeek == 0 && s.Day == 0 && s.Hour == 0 && s.Minute == 0 && s.Second == 0 && s.Milliseconds == 0) diff --git a/tools.go b/tools.go index 41fded1..13ff5f0 100644 --- a/tools.go +++ b/tools.go @@ -25,4 +25,5 @@ import ( _ "github.com/josephspurrier/goversioninfo" _ "github.com/kyleconroy/sqlc/cmd/sqlc" _ "google.golang.org/protobuf/cmd/protoc-gen-go" + _ "gvisor.dev/gvisor/tools/checklocks/cmd/checklocks" ) diff --git a/web/updatelist.go b/web/updatelist.go index 2790459..4165422 100644 --- a/web/updatelist.go +++ b/web/updatelist.go @@ -49,9 +49,13 @@ type updateListCacheEntry struct { } type updateHandler struct { - key pyxtea.Key - dir string + key pyxtea.Key + + dir string + + // +checklocks:mutex cache map[string]updateListCacheEntry + mutex sync.RWMutex }