From 3104a3f35111f332430481d7b0ab6c901bff5a6f Mon Sep 17 00:00:00 2001 From: RutsuKun Date: Tue, 5 Dec 2023 22:27:24 +0100 Subject: [PATCH] Server: refactor server code for readability and maintainability (#6) * Server: refactor server code for readability and maintainability Tools: create bat files to build and release executable file * CI: add CGO_LDFLAGS env var in build-server.yml --- .github/workflows/build-server.yml | 2 + .gitignore | 4 +- server/automatamp/Client.go | 11 - server/automatamp/MockClient.go | 19 +- server/automatamp/Server.go | 571 ++++-------------- server/automatamp/{ => core}/ErrorHandling.go | 2 +- .../{PacketUtility.go => core/Packet.go} | 46 +- server/automatamp/core/Server.go | 70 +++ server/automatamp/core/Vector.go | 21 + server/automatamp/handlers/AnimationStart.go | 28 + server/automatamp/handlers/Buttons.go | 17 + server/automatamp/handlers/DestroyEntity.go | 27 + server/automatamp/handlers/DestroyPlayer.go | 18 + .../handlers/EntityAnimationStart.go | 35 ++ server/automatamp/handlers/EntityData.go | 18 + server/automatamp/handlers/Hello.go | 163 +++++ server/automatamp/handlers/NewMasterClient.go | 20 + server/automatamp/handlers/PacketHandler.go | 35 ++ server/automatamp/handlers/Ping.go | 14 + server/automatamp/handlers/PlayerData.go | 19 + server/automatamp/handlers/SpawnEntity.go | 35 ++ server/automatamp/structs/Client.go | 11 + server/automatamp/{ => structs}/Connection.go | 6 +- server/automatamp/structs/Entity.go | 11 + server/automatamp/{ => structs}/Room.go | 2 +- server/automatamp/structs/Server.go | 18 + server/main.go | 3 +- tools/steamtwice/build.bat | 1 + tools/steamtwice/release.bat | 1 + 29 files changed, 701 insertions(+), 527 deletions(-) delete mode 100644 server/automatamp/Client.go rename server/automatamp/{ => core}/ErrorHandling.go (90%) rename server/automatamp/{PacketUtility.go => core/Packet.go} (59%) create mode 100644 server/automatamp/core/Server.go create mode 100644 server/automatamp/core/Vector.go create mode 100644 server/automatamp/handlers/AnimationStart.go create mode 100644 server/automatamp/handlers/Buttons.go create mode 100644 server/automatamp/handlers/DestroyEntity.go create mode 100644 server/automatamp/handlers/DestroyPlayer.go create mode 100644 server/automatamp/handlers/EntityAnimationStart.go create mode 100644 server/automatamp/handlers/EntityData.go create mode 100644 server/automatamp/handlers/Hello.go create mode 100644 server/automatamp/handlers/NewMasterClient.go create mode 100644 server/automatamp/handlers/PacketHandler.go create mode 100644 server/automatamp/handlers/Ping.go create mode 100644 server/automatamp/handlers/PlayerData.go create mode 100644 server/automatamp/handlers/SpawnEntity.go create mode 100644 server/automatamp/structs/Client.go rename server/automatamp/{ => structs}/Connection.go (56%) create mode 100644 server/automatamp/structs/Entity.go rename server/automatamp/{ => structs}/Room.go (75%) create mode 100644 server/automatamp/structs/Server.go create mode 100644 tools/steamtwice/build.bat create mode 100644 tools/steamtwice/release.bat diff --git a/.github/workflows/build-server.yml b/.github/workflows/build-server.yml index ef402e6..42d4aba 100644 --- a/.github/workflows/build-server.yml +++ b/.github/workflows/build-server.yml @@ -1,5 +1,7 @@ name: Build Server on: [push, pull_request, workflow_dispatch] +env: + CGO_LDFLAGS: -Wl,--no-as-needed jobs: build: runs-on: ${{matrix.os}} diff --git a/.gitignore b/.gitignore index e20f230..576315f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ build/* .vscode/* out/* .vs/* -tools/steamtwice/build/* \ No newline at end of file +tools/steamtwice/build/* +tools/steamtwice/CMakeFiles +tools/steamtwice/CMakeCache* \ No newline at end of file diff --git a/server/automatamp/Client.go b/server/automatamp/Client.go deleted file mode 100644 index bbb7f71..0000000 --- a/server/automatamp/Client.go +++ /dev/null @@ -1,11 +0,0 @@ -package automatamp - -import nier "github.com/praydog/AutomataMP/server/automatamp/nier" - -type Client struct { - guid uint64 - model uint32 - name string - isMasterClient bool - lastPlayerData *nier.PlayerData -} diff --git a/server/automatamp/MockClient.go b/server/automatamp/MockClient.go index da1a7db..b85e628 100644 --- a/server/automatamp/MockClient.go +++ b/server/automatamp/MockClient.go @@ -4,6 +4,7 @@ import ( "math/rand" "time" + core "github.com/praydog/AutomataMP/server/automatamp/core" nier "github.com/praydog/AutomataMP/server/automatamp/nier" "github.com/codecat/go-enet" @@ -30,11 +31,11 @@ type MockClient struct { } func (mock *MockClient) sendPing(peer enet.Peer) { - peer.SendBytes(makeEmptyPacketBytes(nier.PacketTypeID_PING), 0, enet.PacketFlagReliable) + peer.SendBytes(core.MakeEmptyPacketBytes(nier.PacketTypeID_PING), 0, enet.PacketFlagReliable) } func (mock *MockClient) sendHello(peer enet.Peer, name string, password string) { - helloBytes := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + helloBytes := core.BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { name_pkt := builder.CreateString(name) pwd_pkt := builder.CreateString(password) nier.HelloStart(builder) @@ -48,7 +49,7 @@ func (mock *MockClient) sendHello(peer enet.Peer, name string, password string) return nier.HelloEnd(builder) }) - pkt := makePacketBytes(nier.PacketTypeID_HELLO, helloBytes) + pkt := core.MakePacketBytes(nier.PacketTypeID_HELLO, helloBytes) peer.SendBytes(pkt, 0, enet.PacketFlagReliable) } @@ -64,7 +65,7 @@ func (mock *MockClient) getNextPacket(ev enet.Event) *nier.Packet { data := nier.GetRootAsPacket(packetBytes, 0) - if !checkValidPacket(data) { + if !core.CheckValidPacket(data) { return nil } @@ -190,11 +191,11 @@ func (mock *MockClient) Run() { if ev.GetType() == enet.EventNone { if once_test { log.Info("Sending animation start") - animationStartBytes := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + animationStartBytes := core.BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return nier.CreateAnimationStart(builder, 1, 2, 3, 4) }) - animationData := makePacketBytes(nier.PacketTypeID_ANIMATION_START, animationStartBytes) + animationData := core.MakePacketBytes(nier.PacketTypeID_ANIMATION_START, animationStartBytes) peer.SendBytes(animationData, 0, enet.PacketFlagReliable) once_test = false @@ -211,11 +212,11 @@ func (mock *MockClient) Run() { if now.Sub(sendUpdateTime) >= (time.Second / 60) { //log.Info("Sending update") - playerDataBytes := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + playerDataBytes := core.BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return nier.CreatePlayerData(builder, true, 0.1, 0.5, 0.25, 1, 0, 0, rand.Float32(), rand.Float32(), 250.0) }) - packetData := makePacketBytes(nier.PacketTypeID_PLAYER_DATA, playerDataBytes) + packetData := core.MakePacketBytes(nier.PacketTypeID_PLAYER_DATA, playerDataBytes) peer.SendBytes(packetData, 0, enet.PacketFlagReliable) sendUpdateTime = now continue @@ -240,7 +241,7 @@ func (mock *MockClient) Run() { data := nier.GetRootAsPacket(packetBytes, 0) - if !checkValidPacket(data) { + if !core.CheckValidPacket(data) { continue } diff --git a/server/automatamp/Server.go b/server/automatamp/Server.go index 501481c..1a5960e 100644 --- a/server/automatamp/Server.go +++ b/server/automatamp/Server.go @@ -10,482 +10,112 @@ import ( "strconv" "time" + core "github.com/praydog/AutomataMP/server/automatamp/core" + handlers "github.com/praydog/AutomataMP/server/automatamp/handlers" nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" "github.com/codecat/go-enet" "github.com/codecat/go-libs/log" - flatbuffers "github.com/google/flatbuffers/go" ) -type ActiveEntity struct { - guid uint32 - spawnInfo *nier.EntitySpawnParams - //lastEntityData *nier.EntityData // to be seen if it needs to be used. -} - -type EntityList map[uint32]*ActiveEntity - -type Server struct { - host enet.Host - connections map[enet.Peer]*Connection - clients map[*Connection]*Client - entities EntityList - connectionCount uint64 - highestEntityGuid uint32 - config map[string]interface{} - lastHeartbeat time.Time -} - -func (server *Server) BroadcastPacketToAll(id nier.PacketType, data []uint8) { - broadcastData := makePacketBytes(id, data) - - for conn := range server.clients { - conn.peer.SendBytes(broadcastData, 0, enet.PacketFlagReliable) - } -} - -func (server *Server) BroadcastPacketToAllExceptSender(sender enet.Peer, id nier.PacketType, data []uint8) { - broadcastData := makePacketBytes(id, data) +var currentServer *structs.Server - for conn := range server.clients { - if conn.peer == sender { - continue - } - - conn.peer.SendBytes(broadcastData, 0, enet.PacketFlagReliable) +func handleEnetEvent(ev enet.Event) { + switch ev.GetType() { + case enet.EventConnect: + handleEnetConnectEvent(ev) + case enet.EventDisconnect: + handleEnetDisconnectEvent(ev) + case enet.EventReceive: + handleEnetReceiveEvent(ev) } } -func (server *Server) BroadcastPlayerPacketToAll(connection *Connection, id nier.PacketType, data []uint8) { - broadcastData := makePlayerPacketBytes(connection, id, data) - - for conn := range server.clients { - conn.peer.SendBytes(broadcastData, 0, enet.PacketFlagReliable) - } +func handleEnetConnectEvent(ev enet.Event) { + log.Info("New peer connected: %s", ev.GetPeer().GetAddress()) + connection := &structs.Connection{} + connection.Peer = ev.GetPeer() + connection.Client = nil + currentServer.Connections[ev.GetPeer()] = connection } -func (server *Server) BroadcastPlayerPacketToAllExceptSender(sender enet.Peer, connection *Connection, id nier.PacketType, data []uint8) { - broadcastData := makePlayerPacketBytes(connection, id, data) - - for conn := range server.clients { - if conn.peer == sender { - continue - } +func handleEnetDisconnectEvent(ev enet.Event) { + log.Info("Peer disconnected: %s", ev.GetPeer().GetAddress()) + connection := currentServer.Connections[ev.GetPeer()] - conn.peer.SendBytes(broadcastData, 0, enet.PacketFlagReliable) + if connection != nil { + handleDisconnect(connection, ev.GetPeer()) } } -func (server *Server) GetFilteredPlayerName(input []uint8) string { - out := "" - - if input == nil || string(input) == "" { - log.Error("Client sent empty name, assigning random name") - out = "Client" + strconv.FormatInt(int64(len(server.connections)), 10) - } else { - out = string(input) - } - - for _, client := range server.clients { - if client.name == out { - log.Error("Client sent duplicate name, appending number") - out = out + strconv.FormatInt(int64(len(server.connections)), 10) - break - } - } - - return out -} - -func (server *Server) handleHello(ev enet.Event, connection *Connection, data *nier.Packet) { - log.Info("Hello packet received") - - if connection.client != nil { - log.Error("Received hello packet from client that already has a client, discarding") - return - } - - helloData := nier.GetRootAsHello(data.DataBytes(), 0) - - if helloData.Major() != uint32(nier.VersionMajorValue) { - log.Error("Invalid major version: %d", helloData.Major()) - ev.GetPeer().DisconnectNow(0) - return - } - - if helloData.Minor() > uint32(nier.VersionMinorValue) { - log.Error("Client is newer than server, disconnecting") - ev.GetPeer().DisconnectNow(0) - return - } - - if helloData.Patch() != uint32(nier.VersionPatchValue) { - log.Info("Minor version mismatch, this is okay") - } - - log.Info("Version check passed") +func handleDisconnect(connection *structs.Connection, peer enet.Peer) { + isMasterClient := connection.Client != nil && connection.Client.IsMasterClient - if server.config["password"].(string) != "" { - if string(helloData.Password()) != server.config["password"].(string) { - log.Error("Invalid password, client sent: \"%s\"", string(helloData.Password())) - ev.GetPeer().DisconnectNow(0) - return - } + if connection.Client != nil { + handlers.HandleDestroyPlayer(currentServer, connection) + delete(currentServer.Clients, connection) } - log.Info("Password check passed") - - if _, ok := nier.EnumNamesModelType[nier.ModelType(helloData.Model())]; !ok { - log.Error("Invalid model type: %d", helloData.Model()) - ev.GetPeer().DisconnectNow(0) - return + if isMasterClient { + handlers.HandleNewMasterClient(currentServer) } - log.Info("Model type check passed") - - clientName := server.GetFilteredPlayerName(helloData.Name()) - - server.connectionCount++ - - // Create a new client for the peer - client := &Client{ - guid: server.connectionCount, - name: clientName, - model: helloData.Model(), - - // Allows the first person that connects to be the master client - // In an ideal world, the server would run all of the simulation logic - // like movement, physics, enemy AI & movement, but this would be - // a monumental task because this is a mod, not a game where we have the source code. - // So we let the master client control the simulation. - isMasterClient: len(server.clients) == 0, + if len(currentServer.Clients) == 0 { + currentServer.Entities = make(structs.EntityList) + currentServer.HighestEntityGuid = 0 } - log.Info("Client name: %s", clientName) - log.Info("Client GUID: %d", client.guid) - log.Info("Client is master client: %t", client.isMasterClient) - - // Add the client to the map - connection.client = client - server.clients[connection] = client - - // Send a welcome packet - welcomeBytes := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { - nier.WelcomeStart(builder) - nier.WelcomeAddGuid(builder, client.guid) - nier.WelcomeAddIsMasterClient(builder, client.isMasterClient) - nier.WelcomeAddHighestEntityGuid(builder, server.highestEntityGuid) - return nier.WelcomeEnd(builder) - }) - - log.Info("Sending welcome packet") - ev.GetPeer().SendBytes(makePacketBytes(nier.PacketTypeID_WELCOME, welcomeBytes), 0, enet.PacketFlagReliable) - - // Send the player creation packet - createPlayerBytes := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { - playerName := builder.CreateString(client.name) - nier.CreatePlayerStart(builder) - nier.CreatePlayerAddGuid(builder, client.guid) - nier.CreatePlayerAddName(builder, playerName) - nier.CreatePlayerAddModel(builder, client.model) - return nier.CreatePlayerEnd(builder) - }) - - log.Info("Sending create player packet to everyone") - server.BroadcastPacketToAll(nier.PacketTypeID_CREATE_PLAYER, createPlayerBytes) - - // Broadcast previously connected clients to the new client - for _, prevClient := range server.clients { - if prevClient == nil || prevClient == client { // Skip the new client - continue - } - - createPlayerBytes := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { - playerName := builder.CreateString(prevClient.name) - nier.CreatePlayerStart(builder) - nier.CreatePlayerAddGuid(builder, prevClient.guid) - nier.CreatePlayerAddName(builder, playerName) - nier.CreatePlayerAddModel(builder, prevClient.model) - return nier.CreatePlayerEnd(builder) - }) - - log.Info("Sending create player packet for previous client %d to client %d", prevClient.guid, client.guid) - ev.GetPeer().SendBytes(makePacketBytes(nier.PacketTypeID_CREATE_PLAYER, createPlayerBytes), 0, enet.PacketFlagReliable) - } - - // Broadcast previously spawned entities to the new client - for _, entity := range server.entities { - if entity == nil { - continue - } - - spawnData := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { - name := builder.CreateString(string(entity.spawnInfo.Name())) - posdata := entity.spawnInfo.Positional(nil) - nier.EntitySpawnParamsStart(builder) - nier.EntitySpawnParamsAddName(builder, name) - nier.EntitySpawnParamsAddModel(builder, entity.spawnInfo.Model()) - nier.EntitySpawnParamsAddModel2(builder, entity.spawnInfo.Model2()) - packetPosData := nier.CreateEntitySpawnPositionalData( - builder, - posdata.Forward(nil).X(), posdata.Forward(nil).Y(), posdata.Forward(nil).Z(), posdata.Forward(nil).W(), - posdata.Up(nil).X(), posdata.Up(nil).Y(), posdata.Up(nil).Z(), posdata.Up(nil).W(), - posdata.Right(nil).X(), posdata.Right(nil).Y(), posdata.Right(nil).Z(), posdata.Right(nil).W(), - posdata.W(nil).X(), posdata.W(nil).Y(), posdata.W(nil).Z(), posdata.W(nil).W(), - posdata.Position(nil).X(), posdata.Position(nil).Y(), posdata.Position(nil).Z(), posdata.Position(nil).W(), - posdata.Unknown(nil).X(), posdata.Unknown(nil).Y(), posdata.Unknown(nil).Z(), posdata.Unknown(nil).W(), - posdata.Unknown2(nil).X(), posdata.Unknown2(nil).Y(), posdata.Unknown2(nil).Z(), posdata.Unknown2(nil).W(), - posdata.Unk(), posdata.Unk2(), posdata.Unk3(), posdata.Unk4(), - posdata.Unk5(), posdata.Unk6(), posdata.Unk7(), posdata.Unk8(), - ) - nier.EntitySpawnParamsAddPositional(builder, packetPosData) - return nier.EntitySpawnParamsEnd(builder) - }) - - spawnPacket := makeEntityPacketBytes(entity.guid, nier.PacketTypeID_SPAWN_ENTITY, spawnData) - - log.Info("Sending spawn entity packet for entity %d to client %d", entity.guid, client.guid) - ev.GetPeer().SendBytes(spawnPacket, 0, enet.PacketFlagReliable) - } + delete(currentServer.Connections, peer) } -func (server *Server) handlePlayerData(ev enet.Event, connection *Connection, data *nier.Packet) { - playerData := &nier.PlayerData{} - flatbuffers.GetRootAs(data.DataBytes(), 0, playerData) - - connection.client.lastPlayerData = playerData - - // Broadcast the packet back to all valid clients (except the sender) - server.BroadcastPlayerPacketToAllExceptSender(ev.GetPeer(), connection, nier.PacketTypeID_PLAYER_DATA, data.DataBytes()) -} +func handleEnetReceiveEvent(ev enet.Event) { + connection := currentServer.Connections[ev.GetPeer()] -func (server *Server) handleAnimationStart(ev enet.Event, connection *Connection, data *nier.Packet) { - log.Info("Animation start received") + packet := ev.GetPacket() + defer packet.Destroy() + packetBytes := packet.GetData() - animationData := &nier.AnimationStart{} - flatbuffers.GetRootAs(data.DataBytes(), 0, animationData) - - log.Info(" Animation: %d", animationData.Anim()) - log.Info(" Variant: %d", animationData.Variant()) - log.Info(" a3: %d", animationData.A3()) - log.Info(" a4: %d", animationData.A4()) - - // TODO: sanitize the data - - // Broadcast the packet back to all valid clients (except the sender) - server.BroadcastPlayerPacketToAllExceptSender(ev.GetPeer(), connection, nier.PacketTypeID_ANIMATION_START, data.DataBytes()) -} - -func (server *Server) handleButtons(ev enet.Event, connection *Connection, data *nier.Packet) { - log.Info("Buttons received") - - // Broadcast the packet back to all valid clients (except the sender) - server.BroadcastPlayerPacketToAllExceptSender(ev.GetPeer(), connection, nier.PacketTypeID_BUTTONS, data.DataBytes()) -} - -func (server *Server) handleSpawnEntity(ev enet.Event, connection *Connection, data *nier.Packet) { - log.Info("Spawn entity received") - if !connection.client.isMasterClient { - log.Info(" Not a master client, ignoring") - return - } - - // Cache the entity. - entityPkt := &nier.EntityPacket{} - flatbuffers.GetRootAs(data.DataBytes(), 0, entityPkt) - - spawnInfo := &nier.EntitySpawnParams{} - flatbuffers.GetRootAs(entityPkt.DataBytes(), 0, spawnInfo) - - if entityPkt.Guid() > server.highestEntityGuid { - server.highestEntityGuid = entityPkt.Guid() - } - - server.entities[entityPkt.Guid()] = new(ActiveEntity) - server.entities[entityPkt.Guid()].guid = entityPkt.Guid() - server.entities[entityPkt.Guid()].spawnInfo = spawnInfo - - server.BroadcastPacketToAllExceptSender(ev.GetPeer(), nier.PacketTypeID_SPAWN_ENTITY, data.DataBytes()) -} - -func (server *Server) handleDestroyEntity(ev enet.Event, connection *Connection, data *nier.Packet) { - log.Info("Destroy entity received") - if !connection.client.isMasterClient { - log.Info(" Not a master client, ignoring") - return - } - - // Destroy the entity. - entityPkt := &nier.EntityPacket{} - flatbuffers.GetRootAs(data.DataBytes(), 0, entityPkt) - - delete(server.entities, entityPkt.Guid()) - - server.BroadcastPacketToAllExceptSender(ev.GetPeer(), nier.PacketTypeID_DESTROY_ENTITY, data.DataBytes()) -} - -func (server *Server) handleEntityData(ev enet.Event, connection *Connection, data *nier.Packet) { - //log.Info("Entity data received") - if !connection.client.isMasterClient { - log.Info(" Not a master client, ignoring") - return + if checkClientIsAuthorized(ev, packetBytes) { + handlers.PacketHandler(currentServer, connection, ev.GetPeer(), packetBytes) + } else { + log.Info("Client unauthorized") } - - server.BroadcastPacketToAllExceptSender(ev.GetPeer(), nier.PacketTypeID_ENTITY_DATA, data.DataBytes()) } -func (server *Server) handleEntityAnimationStart(ev enet.Event, connection *Connection, data *nier.Packet) { - log.Info("ENTITY Animation start received") +func checkClientIsAuthorized(ev enet.Event, data []byte) bool { + connection := currentServer.Connections[ev.GetPeer()] - if !connection.client.isMasterClient { - log.Info(" Not a master client, ignoring") - return + if connection == nil { + log.Error("Received data from unknown peer, ignoring") + return false } - entityPkt := &nier.EntityPacket{} - flatbuffers.GetRootAs(data.DataBytes(), 0, entityPkt) - - animationData := &nier.AnimationStart{} - flatbuffers.GetRootAs(entityPkt.DataBytes(), 0, animationData) - - log.Info(" Animation: %d", animationData.Anim()) - log.Info(" Variant: %d", animationData.Variant()) - log.Info(" a3: %d", animationData.A3()) - log.Info(" a4: %d", animationData.A4()) + if connection.Client == nil { + log.Info("Peer %s sent data %d bytes", ev.GetPeer().GetAddress().String(), len(data)) + data := nier.GetRootAsPacket(data, 0) - // TODO: sanitize the data - - // Broadcast the packet back to all valid clients (except the sender) - server.BroadcastPacketToAllExceptSender(ev.GetPeer(), nier.PacketTypeID_ENTITY_ANIMATION_START, data.DataBytes()) -} - -func (server *Server) handleEvent(ev enet.Event) { - switch ev.GetType() { - case enet.EventConnect: // A new peer has connected - log.Info("New peer connected: %s", ev.GetPeer().GetAddress()) - connection := &Connection{} - connection.peer = ev.GetPeer() - connection.client = nil - server.connections[ev.GetPeer()] = connection - break - - case enet.EventDisconnect: // A connected peer has disconnected - log.Info("Peer disconnected: %s", ev.GetPeer().GetAddress()) - if server.connections[ev.GetPeer()] != nil { - isMasterClient := server.connections[ev.GetPeer()].client != nil && server.connections[ev.GetPeer()].client.isMasterClient - - // Broadcast a destroy player packet to everyone except the disconnected peer - if server.connections[ev.GetPeer()].client != nil { - destroyPlayerBytes := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { - return nier.CreateDestroyPlayer(builder, server.connections[ev.GetPeer()].client.guid) - }) - - server.BroadcastPacketToAllExceptSender(ev.GetPeer(), nier.PacketTypeID_DESTROY_PLAYER, destroyPlayerBytes) - } - - delete(server.clients, server.connections[ev.GetPeer()]) - - if isMasterClient { - // we must find a new master client - for conn, client := range server.clients { - log.Info("Setting new master client: %s @ %s", client.name, conn.peer.GetAddress()) - - client.isMasterClient = true - conn.peer.SendBytes(makeEmptyPacketBytes(nier.PacketTypeID_SET_MASTER_CLIENT), 0, enet.PacketFlagReliable) - break - } - } - - // We need to delete all existing entities now. - // They will grow to a crazy amount over time otherwise. - if len(server.clients) == 0 { - server.entities = make(EntityList) - server.highestEntityGuid = 0 - } + if !core.CheckValidPacket(data) { + return false } - delete(server.connections, ev.GetPeer()) - break - - case enet.EventReceive: // A peer sent us some data - connection := server.connections[ev.GetPeer()] - - if connection == nil { - log.Error("Received data from unknown peer, ignoring") - break - } - - // Get the packet - packet := ev.GetPacket() - - // We must destroy the packet when we're done with it - defer packet.Destroy() - - // Get the bytes in the packet - packetBytes := packet.GetData() - - if connection.client != nil { - //log.Info("Peer %d @ %s sent data %d bytes", connection.client.guid, ev.GetPeer().GetAddress().String(), len(packetBytes)) - } else { - log.Info("Peer %s sent data %d bytes", ev.GetPeer().GetAddress().String(), len(packetBytes)) - } - - data := nier.GetRootAsPacket(packetBytes, 0) - - if !checkValidPacket(data) { - break - } - - if data.Id() != nier.PacketTypeID_HELLO && connection.client == nil { + if data.Id() != nier.PacketTypeID_HELLO { log.Error("Received packet before hello was sent, discarding") - break - } - - switch data.Id() { - case nier.PacketTypeID_HELLO: - server.handleHello(ev, connection, data) - break - case nier.PacketTypeID_PING: - log.Info("Ping received from %s", connection.client.name) - - ev.GetPeer().SendBytes(makeEmptyPacketBytes(nier.PacketTypeID_PONG), 0, enet.PacketFlagReliable) - break - case nier.PacketTypeID_PLAYER_DATA: - server.handlePlayerData(ev, connection, data) - break - case nier.PacketTypeID_ANIMATION_START: - server.handleAnimationStart(ev, connection, data) - break - case nier.PacketTypeID_BUTTONS: - server.handleButtons(ev, connection, data) - break - case nier.PacketTypeID_SPAWN_ENTITY: - server.handleSpawnEntity(ev, connection, data) - break - case nier.PacketTypeID_DESTROY_ENTITY: - server.handleDestroyEntity(ev, connection, data) - break - case nier.PacketTypeID_ENTITY_DATA: - server.handleEntityData(ev, connection, data) - break - case nier.PacketTypeID_ENTITY_ANIMATION_START: - server.handleEntityAnimationStart(ev, connection, data) - break - default: - log.Error("Unknown packet type: %d", data.Id()) - break + return false } } + + return true } -func (server *Server) service() { +func service() { // Wait until the next event - ev := server.host.Service(0) + ev := currentServer.Host.Service(0) evHandleCount := 0 for ev.GetType() != enet.EventNone { evHandleCount++ - server.handleEvent(ev) + handleEnetEvent(ev) // Break out if we have handled too many events // so we can perform other tasks. We will handle the rest @@ -495,33 +125,33 @@ func (server *Server) service() { break } - ev = server.host.Service(0) + ev = currentServer.Host.Service(0) } } -func (server *Server) cleanup() { +func cleanup() { // Destroy the host when we're done with it - if server.host != nil { - server.host.Destroy() + if currentServer.Host != nil { + currentServer.Host.Destroy() } // Uninitialize enet enet.Deinitialize() } -func (server *Server) sendHeartbeatToMasterServer() { - if !server.config["masterServerNotify"].(bool) { +func sendHeartbeatToMasterServer() { + if !currentServer.Config["masterServerNotify"].(bool) { log.Info("Not sending heartbeats to master server. If you want this then set masterServerNotify to true in server.json") return } log.Info("Sending heartbeat to master server") - server.lastHeartbeat = time.Now() + currentServer.LastHeartbeat = time.Now() jsonValues := make(map[string]interface{}) - jsonValues["Port"] = server.config["port"].(string) - jsonValues["Name"] = server.config["name"].(string) - jsonValues["NumPlayers"] = len(server.clients) + jsonValues["Port"] = currentServer.Config["port"].(string) + jsonValues["Name"] = currentServer.Config["name"].(string) + jsonValues["NumPlayers"] = len(currentServer.Clients) jsonBytes, err := json.Marshal(jsonValues) @@ -532,7 +162,7 @@ func (server *Server) sendHeartbeatToMasterServer() { log.Info(string(jsonBytes)) - url := server.config["masterServer"].(string) + "/heartbeat" + url := currentServer.Config["masterServer"].(string) + "/heartbeat" log.Info("Sending heartbeat to %s", url) r, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes)) @@ -564,8 +194,8 @@ func (server *Server) sendHeartbeatToMasterServer() { } } -func (server *Server) heartbeatGoroutine() { - if !server.config["masterServerNotify"].(bool) { +func heartbeatGoroutine() { + if !currentServer.Config["masterServerNotify"].(bool) { log.Info("Not sending heartbeats to master server. If you want this then set masterServerNotify to true in server.json") return } @@ -573,8 +203,8 @@ func (server *Server) heartbeatGoroutine() { for { // every 30 seconds, send a heartbeat to the master server // containing the server name and how many players are connected - if time.Since(server.lastHeartbeat) >= 30*time.Second { - server.sendHeartbeatToMasterServer() + if time.Since(currentServer.LastHeartbeat) >= 30*time.Second { + sendHeartbeatToMasterServer() } time.Sleep(time.Second) @@ -582,14 +212,14 @@ func (server *Server) heartbeatGoroutine() { } // server start -func (server *Server) Run() { +func Run() { // Initialize enet enet.Initialize() - defer server.cleanup() + defer cleanup() // Convert port to int - port, err := strconv.Atoi(server.config["port"].(string)) + port, err := strconv.Atoi(currentServer.Config["port"].(string)) if err != nil { log.Info("Error reading port: %s, using default (6969).", err) @@ -603,40 +233,43 @@ func (server *Server) Run() { panic(err) } - server.host = host + currentServer.Host = host log.Info("Created host") - go server.heartbeatGoroutine() + go heartbeatGoroutine() // The event loop for { - server.service() + service() } } -func CreateServer() *Server { +func CreateServer() { serverJson, err := os.ReadFile("server.json") if err != nil { log.Error("Server requires a server.json file to be present") panic(err) } - server := &Server{} - server.connections = make(map[enet.Peer]*Connection) - server.clients = make(map[*Connection]*Client) - server.entities = make(EntityList) - server.config = make(map[string]interface{}) - server.connectionCount = 0 - server.highestEntityGuid = 0 - server.config["password"] = "" - server.config["masterServer"] = "http://localhost" - server.config["masterServerNotify"] = true - server.config["name"] = "AutomataMP Server" - server.config["port"] = "6969" - - json.Unmarshal(serverJson, &server.config) - log.Info("Server password: %s", server.config["password"].(string)) - - return server + if currentServer == nil { + currentServer = &structs.Server{} + } + + currentServer.Connections = make(map[enet.Peer]*structs.Connection) + currentServer.Clients = make(map[*structs.Connection]*structs.Client) + currentServer.Entities = make(structs.EntityList) + currentServer.Config = make(map[string]interface{}) + currentServer.ConnectionCount = 0 + currentServer.HighestEntityGuid = 0 + currentServer.Config["password"] = "" + currentServer.Config["masterServer"] = "http://localhost" + currentServer.Config["masterServerNotify"] = true + currentServer.Config["name"] = "AutomataMP Server" + currentServer.Config["port"] = "6969" + + json.Unmarshal(serverJson, ¤tServer.Config) + log.Info("Server password: %s", currentServer.Config["password"].(string)) + + Run() } diff --git a/server/automatamp/ErrorHandling.go b/server/automatamp/core/ErrorHandling.go similarity index 90% rename from server/automatamp/ErrorHandling.go rename to server/automatamp/core/ErrorHandling.go index b821d3d..c260380 100644 --- a/server/automatamp/ErrorHandling.go +++ b/server/automatamp/core/ErrorHandling.go @@ -1,4 +1,4 @@ -package automatamp +package core import "github.com/codecat/go-libs/log" diff --git a/server/automatamp/PacketUtility.go b/server/automatamp/core/Packet.go similarity index 59% rename from server/automatamp/PacketUtility.go rename to server/automatamp/core/Packet.go index 89e8211..109513f 100644 --- a/server/automatamp/PacketUtility.go +++ b/server/automatamp/core/Packet.go @@ -1,4 +1,4 @@ -package automatamp +package core import ( nier "github.com/praydog/AutomataMP/server/automatamp/nier" @@ -7,7 +7,7 @@ import ( flatbuffers "github.com/google/flatbuffers/go" ) -func checkValidPacket(data *nier.Packet) bool { +func CheckValidPacket(data *nier.Packet) bool { // recovering from the panic will return false // so this should be fine // the reason for the panic handler is so some client @@ -27,7 +27,7 @@ func checkValidPacket(data *nier.Packet) bool { return true } -func packetStart(id nier.PacketType) *flatbuffers.Builder { +func PacketStart(id nier.PacketType) *flatbuffers.Builder { builder := flatbuffers.NewBuilder(0) nier.PacketStart(builder) nier.PacketAddMagic(builder, 1347240270) @@ -37,21 +37,7 @@ func packetStart(id nier.PacketType) *flatbuffers.Builder { return builder } -func makeVectorData(builder *flatbuffers.Builder, data []uint8) flatbuffers.UOffsetT { - dataoffs := flatbuffers.UOffsetT(0) - - if len(data) > 0 { - nier.PacketStartDataVector(builder, len(data)) - for i := len(data) - 1; i >= 0; i-- { - builder.PrependUint8(data[i]) - } - dataoffs = builder.EndVector(len(data)) - } - - return dataoffs -} - -func packetStartWithData(id nier.PacketType, data []uint8) *flatbuffers.Builder { +func PacketStartWithData(id nier.PacketType, data []uint8) *flatbuffers.Builder { builder := flatbuffers.NewBuilder(0) dataoffs := makeVectorData(builder, data) @@ -68,40 +54,40 @@ func packetStartWithData(id nier.PacketType, data []uint8) *flatbuffers.Builder return builder } -func makePacketBytes(id nier.PacketType, data []uint8) []uint8 { - builder := packetStartWithData(id, data) +func MakePacketBytes(id nier.PacketType, data []uint8) []uint8 { + builder := PacketStartWithData(id, data) builder.Finish(nier.PacketEnd(builder)) return builder.FinishedBytes() } -func makeEmptyPacketBytes(id nier.PacketType) []uint8 { - builder := packetStart(id) +func MakeEmptyPacketBytes(id nier.PacketType) []uint8 { + builder := PacketStart(id) builder.Finish(nier.PacketEnd(builder)) return builder.FinishedBytes() } -func builderSurround(cb func(*flatbuffers.Builder) flatbuffers.UOffsetT) []uint8 { +func BuilderSurround(cb func(*flatbuffers.Builder) flatbuffers.UOffsetT) []uint8 { builder := flatbuffers.NewBuilder(0) offs := cb(builder) builder.Finish(offs) return builder.FinishedBytes() } -func makePlayerPacketBytes(connection *Connection, id nier.PacketType, data []uint8) []uint8 { - playerPacketData := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { +func MakePlayerPacketBytes(guid uint64, id nier.PacketType, data []uint8) []uint8 { + playerPacketData := BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { dataoffs := makeVectorData(builder, data) nier.PlayerPacketStart(builder) - nier.PlayerPacketAddGuid(builder, connection.client.guid) + nier.PlayerPacketAddGuid(builder, guid) nier.PlayerPacketAddData(builder, dataoffs) return nier.PlayerPacketEnd(builder) }) - return makePacketBytes(id, playerPacketData) + return MakePacketBytes(id, playerPacketData) } -func makeEntityPacketBytes(guid uint32, id nier.PacketType, data []uint8) []uint8 { - entityPacketData := builderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { +func MakeEntityPacketBytes(guid uint32, id nier.PacketType, data []uint8) []uint8 { + entityPacketData := BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { dataoffs := makeVectorData(builder, data) nier.EntityPacketStart(builder) @@ -110,5 +96,5 @@ func makeEntityPacketBytes(guid uint32, id nier.PacketType, data []uint8) []uint return nier.EntityPacketEnd(builder) }) - return makePacketBytes(id, entityPacketData) + return MakePacketBytes(id, entityPacketData) } diff --git a/server/automatamp/core/Server.go b/server/automatamp/core/Server.go new file mode 100644 index 0000000..af67d15 --- /dev/null +++ b/server/automatamp/core/Server.go @@ -0,0 +1,70 @@ +package core + +import ( + "strconv" + + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" + + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" +) + +func GetFilteredPlayerName(server *structs.Server, input []uint8) string { + out := "" + + if input == nil || string(input) == "" { + log.Error("Client sent empty name, assigning random name") + out = "Client" + strconv.FormatInt(int64(len(server.Connections)), 10) + } else { + out = string(input) + } + + for _, client := range server.Clients { + if client.Name == out { + log.Error("Client sent duplicate name, appending number") + out = out + strconv.FormatInt(int64(len(server.Connections)), 10) + break + } + } + + return out +} + +func BroadcastPacketToAll(server *structs.Server, id nier.PacketType, data []uint8) { + broadcastData := MakePacketBytes(id, data) + for conn := range server.Clients { + conn.Peer.SendBytes(broadcastData, 0, enet.PacketFlagReliable) + } +} + +func BroadcastPacketToAllExceptSender(server *structs.Server, sender enet.Peer, id nier.PacketType, data []uint8) { + broadcastData := MakePacketBytes(id, data) + for conn := range server.Clients { + if conn.Peer == sender { + continue + } + + conn.Peer.SendBytes(broadcastData, 0, enet.PacketFlagReliable) + } +} + +func BroadcastPlayerPacketToAll(server *structs.Server, connection *structs.Connection, id nier.PacketType, data []uint8) { + broadcastData := MakePlayerPacketBytes(connection.Client.Guid, id, data) + + for conn := range server.Clients { + conn.Peer.SendBytes(broadcastData, 0, enet.PacketFlagReliable) + } +} + +func BroadcastPlayerPacketToAllExceptSender(server *structs.Server, sender enet.Peer, connection *structs.Connection, id nier.PacketType, data []uint8) { + broadcastData := MakePlayerPacketBytes(connection.Client.Guid, id, data) + + for conn := range server.Clients { + if conn.Peer == sender { + continue + } + + conn.Peer.SendBytes(broadcastData, 0, enet.PacketFlagReliable) + } +} diff --git a/server/automatamp/core/Vector.go b/server/automatamp/core/Vector.go new file mode 100644 index 0000000..4946b3d --- /dev/null +++ b/server/automatamp/core/Vector.go @@ -0,0 +1,21 @@ +package core + +import ( + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + + flatbuffers "github.com/google/flatbuffers/go" +) + +func makeVectorData(builder *flatbuffers.Builder, data []uint8) flatbuffers.UOffsetT { + dataoffs := flatbuffers.UOffsetT(0) + + if len(data) > 0 { + nier.PacketStartDataVector(builder, len(data)) + for i := len(data) - 1; i >= 0; i-- { + builder.PrependUint8(data[i]) + } + dataoffs = builder.EndVector(len(data)) + } + + return dataoffs +} diff --git a/server/automatamp/handlers/AnimationStart.go b/server/automatamp/handlers/AnimationStart.go new file mode 100644 index 0000000..4efa57d --- /dev/null +++ b/server/automatamp/handlers/AnimationStart.go @@ -0,0 +1,28 @@ +package handlers + +import ( + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" + + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + flatbuffers "github.com/google/flatbuffers/go" +) + +func HandleAnimationStart(server *structs.Server, sender enet.Peer, connection *structs.Connection, data *nier.Packet) { + log.Info("Animation start received") + + animationData := &nier.AnimationStart{} + flatbuffers.GetRootAs(data.DataBytes(), 0, animationData) + + // log.Info(" Animation: %d", animationData.Anim()) + // log.Info(" Variant: %d", animationData.Variant()) + // log.Info(" a3: %d", animationData.A3()) + // log.Info(" a4: %d", animationData.A4()) + + // TODO: sanitize the data + + // Broadcast the packet back to all valid clients (except the sender) + core.BroadcastPlayerPacketToAllExceptSender(server, sender, connection, nier.PacketTypeID_ANIMATION_START, data.DataBytes()) +} diff --git a/server/automatamp/handlers/Buttons.go b/server/automatamp/handlers/Buttons.go new file mode 100644 index 0000000..9878a2c --- /dev/null +++ b/server/automatamp/handlers/Buttons.go @@ -0,0 +1,17 @@ +package handlers + +import ( + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" + + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" +) + +func HandleButtons(server *structs.Server, sender enet.Peer, connection *structs.Connection, data *nier.Packet) { + log.Info("Buttons received") + + // Broadcast the packet back to all valid clients (except the sender) + core.BroadcastPlayerPacketToAllExceptSender(server, sender, connection, nier.PacketTypeID_BUTTONS, data.DataBytes()) +} diff --git a/server/automatamp/handlers/DestroyEntity.go b/server/automatamp/handlers/DestroyEntity.go new file mode 100644 index 0000000..be72b09 --- /dev/null +++ b/server/automatamp/handlers/DestroyEntity.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + flatbuffers "github.com/google/flatbuffers/go" + + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandleDestroyEntity(server *structs.Server, sender enet.Peer, connection *structs.Connection, data *nier.Packet) { + log.Info("Destroy entity received") + if !connection.Client.IsMasterClient { + log.Info(" Not a master client, ignoring") + return + } + + // Destroy the entity. + entityPkt := &nier.EntityPacket{} + flatbuffers.GetRootAs(data.DataBytes(), 0, entityPkt) + + delete(server.Entities, entityPkt.Guid()) + + core.BroadcastPacketToAllExceptSender(server, sender, nier.PacketTypeID_DESTROY_ENTITY, data.DataBytes()) +} diff --git a/server/automatamp/handlers/DestroyPlayer.go b/server/automatamp/handlers/DestroyPlayer.go new file mode 100644 index 0000000..318fdc6 --- /dev/null +++ b/server/automatamp/handlers/DestroyPlayer.go @@ -0,0 +1,18 @@ +package handlers + +import ( + "github.com/codecat/go-libs/log" + flatbuffers "github.com/google/flatbuffers/go" + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandleDestroyPlayer(server *structs.Server, connection *structs.Connection) { + log.Info("Destroy Player received") + destroyPlayerBytes := core.BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return nier.CreateDestroyPlayer(builder, connection.Client.Guid) + }) + + core.BroadcastPacketToAllExceptSender(server, connection.Peer, nier.PacketTypeID_DESTROY_PLAYER, destroyPlayerBytes) +} diff --git a/server/automatamp/handlers/EntityAnimationStart.go b/server/automatamp/handlers/EntityAnimationStart.go new file mode 100644 index 0000000..63c43cf --- /dev/null +++ b/server/automatamp/handlers/EntityAnimationStart.go @@ -0,0 +1,35 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + flatbuffers "github.com/google/flatbuffers/go" + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandleEntityAnimationStart(server *structs.Server, sender enet.Peer, connection *structs.Connection, data *nier.Packet) { + log.Info("ENTITY Animation start received") + + if !connection.Client.IsMasterClient { + log.Info(" Not a master client, ignoring") + return + } + + entityPkt := &nier.EntityPacket{} + flatbuffers.GetRootAs(data.DataBytes(), 0, entityPkt) + + animationData := &nier.AnimationStart{} + flatbuffers.GetRootAs(entityPkt.DataBytes(), 0, animationData) + + // log.Info(" Animation: %d", animationData.Anim()) + // log.Info(" Variant: %d", animationData.Variant()) + // log.Info(" a3: %d", animationData.A3()) + // log.Info(" a4: %d", animationData.A4()) + + // TODO: sanitize the data + + // Broadcast the packet back to all valid clients (except the sender) + core.BroadcastPacketToAllExceptSender(server, sender, nier.PacketTypeID_ENTITY_ANIMATION_START, data.DataBytes()) +} diff --git a/server/automatamp/handlers/EntityData.go b/server/automatamp/handlers/EntityData.go new file mode 100644 index 0000000..70ef070 --- /dev/null +++ b/server/automatamp/handlers/EntityData.go @@ -0,0 +1,18 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandleEntityData(server *structs.Server, sender enet.Peer, connection *structs.Connection, data *nier.Packet) { + if !connection.Client.IsMasterClient { + log.Info(" Not a master client, ignoring") + return + } + + core.BroadcastPacketToAllExceptSender(server, sender, nier.PacketTypeID_ENTITY_DATA, data.DataBytes()) +} diff --git a/server/automatamp/handlers/Hello.go b/server/automatamp/handlers/Hello.go new file mode 100644 index 0000000..dcb4ae5 --- /dev/null +++ b/server/automatamp/handlers/Hello.go @@ -0,0 +1,163 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + flatbuffers "github.com/google/flatbuffers/go" + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandleHello(server *structs.Server, sender enet.Peer, connection *structs.Connection, data *nier.Packet) { + log.Info("Hello packet received") + + if connection.Client != nil { + log.Error("Received hello packet from client that already has a client, discarding") + return + } + + helloData := nier.GetRootAsHello(data.DataBytes(), 0) + + if helloData.Major() != uint32(nier.VersionMajorValue) { + log.Error("Invalid major version: %d", helloData.Major()) + sender.DisconnectNow(0) + return + } + + if helloData.Minor() > uint32(nier.VersionMinorValue) { + log.Error("Client is newer than server, disconnecting") + sender.DisconnectNow(0) + return + } + + if helloData.Patch() != uint32(nier.VersionPatchValue) { + log.Info("Minor version mismatch, this is okay") + } + + log.Info("Version check passed") + + if server.Config["password"].(string) != "" { + if string(helloData.Password()) != server.Config["password"].(string) { + log.Error("Invalid password, client sent: \"%s\"", string(helloData.Password())) + sender.DisconnectNow(0) + return + } + } + + log.Info("Password check passed") + + if _, ok := nier.EnumNamesModelType[nier.ModelType(helloData.Model())]; !ok { + log.Error("Invalid model type: %d", helloData.Model()) + sender.DisconnectNow(0) + return + } + + log.Info("Model type check passed") + + clientName := core.GetFilteredPlayerName(server, helloData.Name()) + + server.ConnectionCount++ + + // Create a new client for the peer + client := &structs.Client{ + Guid: server.ConnectionCount, + Name: clientName, + Model: helloData.Model(), + + // Allows the first person that connects to be the master client + // In an ideal world, the server would run all of the simulation logic + // like movement, physics, enemy AI & movement, but this would be + // a monumental task because this is a mod, not a game where we have the source code. + // So we let the master client control the simulation. + IsMasterClient: len(server.Clients) == 0, + } + + log.Info("Client name: %s", clientName) + log.Info("Client GUID: %d", client.Guid) + log.Info("Client is master client: %t", client.IsMasterClient) + log.Info("Client model: %s", nier.EnumNamesModelType[nier.ModelType(helloData.Model())]) + + // Add the client to the map + connection.Client = client + server.Clients[connection] = client + + // Send a welcome packet + welcomeBytes := core.BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + nier.WelcomeStart(builder) + nier.WelcomeAddGuid(builder, client.Guid) + nier.WelcomeAddIsMasterClient(builder, client.IsMasterClient) + nier.WelcomeAddHighestEntityGuid(builder, server.HighestEntityGuid) + return nier.WelcomeEnd(builder) + }) + + log.Info("Sending welcome packet") + sender.SendBytes(core.MakePacketBytes(nier.PacketTypeID_WELCOME, welcomeBytes), 0, enet.PacketFlagReliable) + + // Send the player creation packet + createPlayerBytes := core.BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + playerName := builder.CreateString(client.Name) + nier.CreatePlayerStart(builder) + nier.CreatePlayerAddGuid(builder, client.Guid) + nier.CreatePlayerAddName(builder, playerName) + nier.CreatePlayerAddModel(builder, client.Model) + return nier.CreatePlayerEnd(builder) + }) + + log.Info("Sending create player packet to everyone") + core.BroadcastPacketToAll(server, nier.PacketTypeID_CREATE_PLAYER, createPlayerBytes) + + // Broadcast previously connected clients to the new client + for _, prevClient := range server.Clients { + if prevClient == nil || prevClient == client { // Skip the new client + continue + } + + createPlayerBytes := core.BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + playerName := builder.CreateString(prevClient.Name) + nier.CreatePlayerStart(builder) + nier.CreatePlayerAddGuid(builder, prevClient.Guid) + nier.CreatePlayerAddName(builder, playerName) + nier.CreatePlayerAddModel(builder, prevClient.Model) + return nier.CreatePlayerEnd(builder) + }) + + log.Info("Sending create player packet for previous client %d to client %d", prevClient.Guid, client.Guid) + sender.SendBytes(core.MakePacketBytes(nier.PacketTypeID_CREATE_PLAYER, createPlayerBytes), 0, enet.PacketFlagReliable) + } + + // Broadcast previously spawned entities to the new client + for _, entity := range server.Entities { + if entity == nil { + continue + } + + spawnData := core.BuilderSurround(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + name := builder.CreateString(string(entity.SpawnInfo.Name())) + posdata := entity.SpawnInfo.Positional(nil) + nier.EntitySpawnParamsStart(builder) + nier.EntitySpawnParamsAddName(builder, name) + nier.EntitySpawnParamsAddModel(builder, entity.SpawnInfo.Model()) + nier.EntitySpawnParamsAddModel2(builder, entity.SpawnInfo.Model2()) + packetPosData := nier.CreateEntitySpawnPositionalData( + builder, + posdata.Forward(nil).X(), posdata.Forward(nil).Y(), posdata.Forward(nil).Z(), posdata.Forward(nil).W(), + posdata.Up(nil).X(), posdata.Up(nil).Y(), posdata.Up(nil).Z(), posdata.Up(nil).W(), + posdata.Right(nil).X(), posdata.Right(nil).Y(), posdata.Right(nil).Z(), posdata.Right(nil).W(), + posdata.W(nil).X(), posdata.W(nil).Y(), posdata.W(nil).Z(), posdata.W(nil).W(), + posdata.Position(nil).X(), posdata.Position(nil).Y(), posdata.Position(nil).Z(), posdata.Position(nil).W(), + posdata.Unknown(nil).X(), posdata.Unknown(nil).Y(), posdata.Unknown(nil).Z(), posdata.Unknown(nil).W(), + posdata.Unknown2(nil).X(), posdata.Unknown2(nil).Y(), posdata.Unknown2(nil).Z(), posdata.Unknown2(nil).W(), + posdata.Unk(), posdata.Unk2(), posdata.Unk3(), posdata.Unk4(), + posdata.Unk5(), posdata.Unk6(), posdata.Unk7(), posdata.Unk8(), + ) + nier.EntitySpawnParamsAddPositional(builder, packetPosData) + return nier.EntitySpawnParamsEnd(builder) + }) + + spawnPacket := core.MakeEntityPacketBytes(entity.Guid, nier.PacketTypeID_SPAWN_ENTITY, spawnData) + + log.Info("Sending spawn entity packet for entity %d to client %d", entity.Guid, client.Guid) + sender.SendBytes(spawnPacket, 0, enet.PacketFlagReliable) + } +} diff --git a/server/automatamp/handlers/NewMasterClient.go b/server/automatamp/handlers/NewMasterClient.go new file mode 100644 index 0000000..4dd622d --- /dev/null +++ b/server/automatamp/handlers/NewMasterClient.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandleNewMasterClient(server *structs.Server) { + log.Info("NewMasterClient received") + for conn, client := range server.Clients { + log.Info("Setting new master client: %s @ %s", client.Name, conn.Peer.GetAddress()) + + client.IsMasterClient = true + conn.Peer.SendBytes(core.MakeEmptyPacketBytes(nier.PacketTypeID_SET_MASTER_CLIENT), 0, enet.PacketFlagReliable) + break + } +} diff --git a/server/automatamp/handlers/PacketHandler.go b/server/automatamp/handlers/PacketHandler.go new file mode 100644 index 0000000..44e1ad7 --- /dev/null +++ b/server/automatamp/handlers/PacketHandler.go @@ -0,0 +1,35 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func PacketHandler(server *structs.Server, connection *structs.Connection, sender enet.Peer, data []byte) { + packetData := nier.GetRootAsPacket(data, 0) + + switch packetData.Id() { + case nier.PacketTypeID_HELLO: + HandleHello(server, sender, connection, packetData) + case nier.PacketTypeID_PING: + HandlePing(sender, connection) + case nier.PacketTypeID_PLAYER_DATA: + HandlePlayerData(server, sender, connection, packetData) + case nier.PacketTypeID_ANIMATION_START: + HandleAnimationStart(server, sender, connection, packetData) + case nier.PacketTypeID_BUTTONS: + HandleButtons(server, sender, connection, packetData) + case nier.PacketTypeID_SPAWN_ENTITY: + HandleSpawnEntity(server, sender, connection, packetData) + case nier.PacketTypeID_DESTROY_ENTITY: + HandleDestroyEntity(server, sender, connection, packetData) + case nier.PacketTypeID_ENTITY_DATA: + HandleEntityData(server, sender, connection, packetData) + case nier.PacketTypeID_ENTITY_ANIMATION_START: + HandleEntityAnimationStart(server, sender, connection, packetData) + default: + log.Error("Unknown packet type: %d", packetData.Id()) + } +} diff --git a/server/automatamp/handlers/Ping.go b/server/automatamp/handlers/Ping.go new file mode 100644 index 0000000..63d31a3 --- /dev/null +++ b/server/automatamp/handlers/Ping.go @@ -0,0 +1,14 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandlePing(sender enet.Peer, connection *structs.Connection) { + log.Info("Ping received from %s", connection.Client.Name) + sender.SendBytes(core.MakeEmptyPacketBytes(nier.PacketTypeID_PONG), 0, enet.PacketFlagReliable) +} diff --git a/server/automatamp/handlers/PlayerData.go b/server/automatamp/handlers/PlayerData.go new file mode 100644 index 0000000..61c1987 --- /dev/null +++ b/server/automatamp/handlers/PlayerData.go @@ -0,0 +1,19 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + flatbuffers "github.com/google/flatbuffers/go" + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandlePlayerData(server *structs.Server, sender enet.Peer, connection *structs.Connection, data *nier.Packet) { + playerData := &nier.PlayerData{} + flatbuffers.GetRootAs(data.DataBytes(), 0, playerData) + + connection.Client.LastPlayerData = playerData + + // Broadcast the packet back to all valid clients (except the sender) + core.BroadcastPlayerPacketToAllExceptSender(server, sender, connection, nier.PacketTypeID_PLAYER_DATA, data.DataBytes()) +} diff --git a/server/automatamp/handlers/SpawnEntity.go b/server/automatamp/handlers/SpawnEntity.go new file mode 100644 index 0000000..c39190d --- /dev/null +++ b/server/automatamp/handlers/SpawnEntity.go @@ -0,0 +1,35 @@ +package handlers + +import ( + "github.com/codecat/go-enet" + "github.com/codecat/go-libs/log" + flatbuffers "github.com/google/flatbuffers/go" + core "github.com/praydog/AutomataMP/server/automatamp/core" + nier "github.com/praydog/AutomataMP/server/automatamp/nier" + structs "github.com/praydog/AutomataMP/server/automatamp/structs" +) + +func HandleSpawnEntity(server *structs.Server, sender enet.Peer, connection *structs.Connection, data *nier.Packet) { + log.Info("Spawn entity received") + if !connection.Client.IsMasterClient { + log.Info(" Not a master client, ignoring") + return + } + + // Cache the entity. + entityPkt := &nier.EntityPacket{} + flatbuffers.GetRootAs(data.DataBytes(), 0, entityPkt) + + spawnInfo := &nier.EntitySpawnParams{} + flatbuffers.GetRootAs(entityPkt.DataBytes(), 0, spawnInfo) + + if entityPkt.Guid() > server.HighestEntityGuid { + server.HighestEntityGuid = entityPkt.Guid() + } + + server.Entities[entityPkt.Guid()] = new(structs.ActiveEntity) + server.Entities[entityPkt.Guid()].Guid = entityPkt.Guid() + server.Entities[entityPkt.Guid()].SpawnInfo = spawnInfo + + core.BroadcastPacketToAllExceptSender(server, sender, nier.PacketTypeID_SPAWN_ENTITY, data.DataBytes()) +} diff --git a/server/automatamp/structs/Client.go b/server/automatamp/structs/Client.go new file mode 100644 index 0000000..fefa85d --- /dev/null +++ b/server/automatamp/structs/Client.go @@ -0,0 +1,11 @@ +package structs + +import nier "github.com/praydog/AutomataMP/server/automatamp/nier" + +type Client struct { + Guid uint64 + Model uint32 + Name string + IsMasterClient bool + LastPlayerData *nier.PlayerData +} diff --git a/server/automatamp/Connection.go b/server/automatamp/structs/Connection.go similarity index 56% rename from server/automatamp/Connection.go rename to server/automatamp/structs/Connection.go index 408b3ca..daa7f86 100644 --- a/server/automatamp/Connection.go +++ b/server/automatamp/structs/Connection.go @@ -1,10 +1,10 @@ -package automatamp +package structs import ( "github.com/codecat/go-enet" ) type Connection struct { - peer enet.Peer - client *Client + Peer enet.Peer + Client *Client } diff --git a/server/automatamp/structs/Entity.go b/server/automatamp/structs/Entity.go new file mode 100644 index 0000000..3b0b18e --- /dev/null +++ b/server/automatamp/structs/Entity.go @@ -0,0 +1,11 @@ +package structs + +import nier "github.com/praydog/AutomataMP/server/automatamp/nier" + +type ActiveEntity struct { + Guid uint32 + SpawnInfo *nier.EntitySpawnParams + //lastEntityData *nier.EntityData // to be seen if it needs to be used. +} + +type EntityList map[uint32]*ActiveEntity diff --git a/server/automatamp/Room.go b/server/automatamp/structs/Room.go similarity index 75% rename from server/automatamp/Room.go rename to server/automatamp/structs/Room.go index d2af38c..7b44a4f 100644 --- a/server/automatamp/Room.go +++ b/server/automatamp/structs/Room.go @@ -1,4 +1,4 @@ -package automatamp +package structs type Room struct { name string diff --git a/server/automatamp/structs/Server.go b/server/automatamp/structs/Server.go new file mode 100644 index 0000000..952a473 --- /dev/null +++ b/server/automatamp/structs/Server.go @@ -0,0 +1,18 @@ +package structs + +import ( + "time" + + "github.com/codecat/go-enet" +) + +type Server struct { + Host enet.Host + Connections map[enet.Peer]*Connection + Clients map[*Connection]*Client + Entities EntityList + ConnectionCount uint64 + HighestEntityGuid uint32 + Config map[string]interface{} + LastHeartbeat time.Time +} diff --git a/server/main.go b/server/main.go index d2ea926..c525a39 100644 --- a/server/main.go +++ b/server/main.go @@ -11,8 +11,7 @@ func main() { flag.Parse() if *mode == "server" { - server := automatamp.CreateServer() - server.Run() + automatamp.CreateServer() } else if *mode == "masterserver" { server := automatamp.CreateMasterServer() server.Run() diff --git a/tools/steamtwice/build.bat b/tools/steamtwice/build.bat new file mode 100644 index 0000000..ffc4893 --- /dev/null +++ b/tools/steamtwice/build.bat @@ -0,0 +1 @@ +cmake . -G "Visual Studio 17 2022" -A Win32 -B build \ No newline at end of file diff --git a/tools/steamtwice/release.bat b/tools/steamtwice/release.bat new file mode 100644 index 0000000..e2673be --- /dev/null +++ b/tools/steamtwice/release.bat @@ -0,0 +1 @@ +cmake --build ./build --config Release \ No newline at end of file