From 2254e4b2a645aa20372d00d6c6e335fe57897537 Mon Sep 17 00:00:00 2001 From: Stephen Horan <31539570+stephenhoran@users.noreply.github.com> Date: Sun, 9 Aug 2020 20:32:47 -0400 Subject: [PATCH] Networking Refactor (#698) * Networking refactor * Networking refactor * Networking refactor * Networking refactor * Refactor netpacket for json.Rawmessages as the data type and client side JSON decoder. * Move game server connection handler to json decoder. * Move game server connection handler to json decoder. --- .../d2localclient/local_client_connection.go | 17 +- .../remote_client_connection.go | 111 +--- d2networking/d2client/game_client.go | 35 +- .../d2netpackettype/message_type.go | 10 + d2networking/d2netpacket/net_packet.go | 34 +- d2networking/d2netpacket/packet_add_player.go | 31 +- .../d2netpacket/packet_generate_map.go | 19 +- d2networking/d2netpacket/packet_item_spawn.go | 23 +- .../d2netpacket/packet_move_player.go | 31 +- d2networking/d2netpacket/packet_ping.go | 10 +- .../d2netpacket/packet_player_cast.go | 31 +- .../packet_player_connection_request.go | 21 +- .../packet_player_disconnect_request.go | 19 +- d2networking/d2netpacket/packet_pong.go | 21 +- .../d2netpacket/packet_server_closed.go | 19 +- .../d2netpacket/packet_update_server_info.go | 25 +- d2networking/d2server/connection_manager.go | 106 ---- .../tcp_client_connection.go | 55 ++ d2networking/d2server/game_server.go | 516 +++++++++--------- 19 files changed, 625 insertions(+), 509 deletions(-) delete mode 100644 d2networking/d2server/connection_manager.go create mode 100644 d2networking/d2server/d2tcpclientconnection/tcp_client_connection.go diff --git a/d2networking/d2client/d2localclient/local_client_connection.go b/d2networking/d2client/d2localclient/local_client_connection.go index 952a170b..21cb4269 100644 --- a/d2networking/d2client/d2localclient/local_client_connection.go +++ b/d2networking/d2client/d2localclient/local_client_connection.go @@ -17,6 +17,7 @@ type LocalClientConnection struct { uniqueID string // Unique ID generated on construction openNetworkServer bool // True if this is a server playerState *d2player.PlayerState // Local player state + gameServer *d2server.GameServer // Game Server } // GetUniqueID returns LocalClientConnection.uniqueID. @@ -48,10 +49,18 @@ func Create(openNetworkServer bool) *LocalClientConnection { // Open creates a new GameServer, runs the server and connects this client to it. func (l *LocalClientConnection) Open(_, saveFilePath string) error { - l.SetPlayerState(d2player.LoadPlayerState(saveFilePath)) - d2server.Create(l.openNetworkServer) + var err error + + l.SetPlayerState(d2player.LoadPlayerState(saveFilePath)) + l.gameServer, err = d2server.NewGameServer(l.openNetworkServer, 30) + if err != nil { + return err + } + + if err := l.gameServer.Start(); err != nil { + return err + } - go d2server.Run() d2server.OnClientConnected(l) return nil @@ -65,7 +74,7 @@ func (l *LocalClientConnection) Close() error { } d2server.OnClientDisconnected(l) - d2server.Destroy() + l.gameServer.Stop() return nil } diff --git a/d2networking/d2client/d2remoteclient/remote_client_connection.go b/d2networking/d2client/d2remoteclient/remote_client_connection.go index 87622f92..ecfc05e5 100644 --- a/d2networking/d2client/d2remoteclient/remote_client_connection.go +++ b/d2networking/d2client/d2remoteclient/remote_client_connection.go @@ -1,11 +1,8 @@ package d2remoteclient import ( - "bytes" - "compress/gzip" "encoding/json" "fmt" - "io" "log" "net" "strings" @@ -24,7 +21,7 @@ import ( type RemoteClientConnection struct { clientListener d2networking.ClientListener // The GameClient uniqueID string // Unique ID generated on construction - udpConnection *net.UDPConn // UDP connection to the server + tcpConnection *net.TCPConn // UDP connection to the server active bool // The connection is currently open } @@ -46,14 +43,14 @@ func (r *RemoteClientConnection) Open(connectionString, saveFilePath string) err } // TODO: Connect to the server - udpAddress, err := net.ResolveUDPAddr("udp", connectionString) + tcpAddress, err := net.ResolveTCPAddr("tcp", connectionString) // TODO: Show connection error screen if connection fails if err != nil { return err } - r.udpConnection, err = net.DialUDP("udp", nil, udpAddress) + r.tcpConnection, err = net.DialTCP("tcp", nil, tcpAddress) // TODO: Show connection error screen if connection fails if err != nil { return err @@ -62,10 +59,11 @@ func (r *RemoteClientConnection) Open(connectionString, saveFilePath string) err r.active = true go r.serverListener() - log.Printf("Connected to server at %s", r.udpConnection.RemoteAddr().String()) + log.Printf("Connected to server at %s", r.tcpConnection.RemoteAddr().String()) gameState := d2player.LoadPlayerState(saveFilePath) - err = r.SendPacketToServer(d2netpacket.CreatePlayerConnectionRequestPacket(r.GetUniqueID(), gameState)) + packet := d2netpacket.CreatePlayerConnectionRequestPacket(r.GetUniqueID(), gameState) + err = r.SendPacketToServer(packet) if err != nil { log.Print("RemoteClientConnection: error sending PlayerConnectionRequestPacket to server.") @@ -107,102 +105,47 @@ func (r *RemoteClientConnection) SetClientListener(listener d2networking.ClientL // SendPacketToServer compresses the JSON encoding of a NetPacket and // sends it to the server. func (r *RemoteClientConnection) SendPacketToServer(packet d2netpacket.NetPacket) error { - data, err := json.Marshal(packet.PacketData) + data, err := json.Marshal(packet) if err != nil { return err } - var buff bytes.Buffer - - buff.WriteByte(byte(packet.PacketType)) - writer, _ := gzip.NewWriterLevel(&buff, gzip.BestCompression) - - var written int - - if written, err = writer.Write(data); err != nil { - return err - } else if written == 0 { - return fmt.Errorf("remoteClientConnection: attempted to send empty %v packet body", packet.PacketType) - } - - if writeErr := writer.Close(); writeErr != nil { - return writeErr - } - - if _, err = r.udpConnection.Write(buff.Bytes()); err != nil { + if _, err = r.tcpConnection.Write(data); err != nil { return err } return nil } -// serverListener runs a while loop, reading from the GameServer's UDP +// serverListener runs a while loop, reading from the GameServer's TCP // connection. func (r *RemoteClientConnection) serverListener() { - buffer := make([]byte, 4096) + var packet d2netpacket.NetPacket + decoder := json.NewDecoder(r.tcpConnection) - for r.active { - n, _, err := r.udpConnection.ReadFromUDP(buffer) + for { + err := decoder.Decode(&packet) + + p, err := r.decodeToPacket(packet.PacketType, string(packet.PacketData)) if err != nil { - fmt.Printf("Socket error: %s\n", err) - continue + log.Println(packet.PacketType, err) } - if n <= 0 { - continue - } - - data, packetType, err := r.bytesToJSON(buffer) + err = r.clientListener.OnPacketReceived(p) if err != nil { - log.Println(packetType, err) - } - - packet, err := r.decodeToPacket(packetType, data) - if err != nil { - log.Println(packetType, err) - } - - err = r.clientListener.OnPacketReceived(packet) - if err != nil { - log.Println(packetType, err) + log.Println(packet.PacketType, err) } } } // bytesToJSON reads the packet type, decompresses the packet and returns a JSON string. func (r *RemoteClientConnection) bytesToJSON(buffer []byte) (string, d2netpackettype.NetPacketType, error) { - buff := bytes.NewBuffer(buffer) - - packetTypeID, err := buff.ReadByte() + packet, err := d2netpacket.UnmarshalNetPacket(buffer) if err != nil { - // The packet type here will be UpdateServerInfo. That shouldn't matter - // but perhaps we should have a 'None' packet type anyway. - return "", d2netpackettype.NetPacketType(0), fmt.Errorf("error reading packet type: %s", err) + return "", 0, err } - packetType := d2netpackettype.NetPacketType(packetTypeID) - reader, err := gzip.NewReader(buff) - - if err != nil { - return "", packetType, fmt.Errorf("error creating reader for %v packet: %s", packetType, err) - } - - sb := new(strings.Builder) - - // This will throw errors where packets are not compressed. This doesn't - // break anything, so the gzip.ErrHeader error is currently ignored to - // avoid noisy logging. - written, err := io.Copy(sb, reader) - - if err != nil && err != gzip.ErrHeader { - return "", packetType, fmt.Errorf("error copying bytes from %v packet: %s", packetType, err) - } - - if written == 0 { - return "", packetType, fmt.Errorf("empty %v packet received", packetType) - } - - return sb.String(), packetType, nil + return string(packet.PacketData), packet.PacketType, nil } // decodeToPacket unmarshals the JSON string into the correct struct @@ -219,7 +162,7 @@ func (r *RemoteClientConnection) decodeToPacket(t d2netpackettype.NetPacketType, break } - np = d2netpacket.NetPacket{PacketType: t, PacketData: p} + np = d2netpacket.NetPacket{PacketType: t, PacketData: d2netpacket.MarshalPacket(p)} case d2netpackettype.MovePlayer: var p d2netpacket.MovePlayerPacket @@ -227,7 +170,7 @@ func (r *RemoteClientConnection) decodeToPacket(t d2netpackettype.NetPacketType, break } - np = d2netpacket.NetPacket{PacketType: t, PacketData: p} + np = d2netpacket.NetPacket{PacketType: t, PacketData: d2netpacket.MarshalPacket(p)} case d2netpackettype.UpdateServerInfo: var p d2netpacket.UpdateServerInfoPacket @@ -235,7 +178,7 @@ func (r *RemoteClientConnection) decodeToPacket(t d2netpackettype.NetPacketType, break } - np = d2netpacket.NetPacket{PacketType: t, PacketData: p} + np = d2netpacket.NetPacket{PacketType: t, PacketData: d2netpacket.MarshalPacket(p)} case d2netpackettype.AddPlayer: var p d2netpacket.AddPlayerPacket @@ -243,7 +186,7 @@ func (r *RemoteClientConnection) decodeToPacket(t d2netpackettype.NetPacketType, break } - np = d2netpacket.NetPacket{PacketType: t, PacketData: p} + np = d2netpacket.NetPacket{PacketType: t, PacketData: d2netpacket.MarshalPacket(p)} case d2netpackettype.Ping: var p d2netpacket.PingPacket @@ -251,7 +194,7 @@ func (r *RemoteClientConnection) decodeToPacket(t d2netpackettype.NetPacketType, break } - np = d2netpacket.NetPacket{PacketType: t, PacketData: p} + np = d2netpacket.NetPacket{PacketType: t, PacketData: d2netpacket.MarshalPacket(p)} case d2netpackettype.PlayerDisconnectionNotification: var p d2netpacket.PlayerDisconnectRequestPacket @@ -259,7 +202,7 @@ func (r *RemoteClientConnection) decodeToPacket(t d2netpackettype.NetPacketType, break } - np = d2netpacket.NetPacket{PacketType: t, PacketData: p} + np = d2netpacket.NetPacket{PacketType: t, PacketData: d2netpacket.MarshalPacket(p)} default: err = fmt.Errorf("RemoteClientConnection: unrecognized packet type: %v", t) diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index 7478c4bb..0f0b9782 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -145,7 +145,10 @@ func (g *GameClient) SendPacketToServer(packet d2netpacket.NetPacket) error { } func (g *GameClient) handleGenerateMapPacket(packet d2netpacket.NetPacket) error { - mapData := packet.PacketData.(d2netpacket.GenerateMapPacket) + mapData, err := d2netpacket.UnmarshalGenerateMap(packet.PacketData) + if err != nil { + return err + } if mapData.RegionType == d2enum.RegionAct1Town { d2mapgen.GenerateAct1Overworld(g.MapEngine) @@ -157,7 +160,11 @@ func (g *GameClient) handleGenerateMapPacket(packet d2netpacket.NetPacket) error } func (g *GameClient) handleUpdateServerInfoPacket(packet d2netpacket.NetPacket) error { - serverInfo := packet.PacketData.(d2netpacket.UpdateServerInfoPacket) + serverInfo, err := d2netpacket.UnmarshalUpdateServerInfo(packet.PacketData) + if err != nil { + return err + } + g.MapEngine.SetSeed(serverInfo.Seed) g.PlayerID = serverInfo.PlayerID g.Seed = serverInfo.Seed @@ -167,7 +174,11 @@ func (g *GameClient) handleUpdateServerInfoPacket(packet d2netpacket.NetPacket) } func (g *GameClient) handleAddPlayerPacket(packet d2netpacket.NetPacket) error { - player := packet.PacketData.(d2netpacket.AddPlayerPacket) + player, err := d2netpacket.UnmarshalAddPlayer(packet.PacketData) + if err != nil { + return err + } + newPlayer := d2mapentity.NewPlayer(player.ID, player.Name, player.X, player.Y, 0, player.HeroType, player.Stats, &player.Equipment) @@ -178,7 +189,11 @@ func (g *GameClient) handleAddPlayerPacket(packet d2netpacket.NetPacket) error { } func (g *GameClient) handleSpawnItemPacket(packet d2netpacket.NetPacket) error { - item := packet.PacketData.(d2netpacket.SpawnItemPacket) + item, err := d2netpacket.UnmarshalSpawnItem(packet.PacketData) + if err != nil { + return err + } + itemEntity, err := d2mapentity.NewItem(item.X, item.Y, item.Codes...) if err == nil { @@ -189,7 +204,11 @@ func (g *GameClient) handleSpawnItemPacket(packet d2netpacket.NetPacket) error { } func (g *GameClient) handleMovePlayerPacket(packet d2netpacket.NetPacket) error { - movePlayer := packet.PacketData.(d2netpacket.MovePlayerPacket) + movePlayer, err := d2netpacket.UnmarshalMovePlayer(packet.PacketData) + if err != nil { + return err + } + player := g.Players[movePlayer.PlayerID] start := d2vector.NewPositionTile(movePlayer.StartX, movePlayer.StartY) dest := d2vector.NewPositionTile(movePlayer.DestX, movePlayer.DestY) @@ -224,7 +243,11 @@ func (g *GameClient) handleMovePlayerPacket(packet d2netpacket.NetPacket) error } func (g *GameClient) handleCastSkillPacket(packet d2netpacket.NetPacket) error { - playerCast := packet.PacketData.(d2netpacket.CastPacket) + playerCast, err := d2netpacket.UnmarshalCast(packet.PacketData) + if err != nil { + return err + } + player := g.Players[playerCast.SourceEntityID] player.SetCasting() diff --git a/d2networking/d2netpacket/d2netpackettype/message_type.go b/d2networking/d2netpacket/d2netpackettype/message_type.go index 3e48bd86..266c6c0c 100644 --- a/d2networking/d2netpacket/d2netpackettype/message_type.go +++ b/d2networking/d2netpacket/d2netpackettype/message_type.go @@ -1,5 +1,7 @@ package d2netpackettype +import "encoding/json" + // NetPacketType is an enum referring to all packet types in package // d2netpacket. type NetPacketType uint32 @@ -25,6 +27,8 @@ const ( ServerClosed // Sent by the local host when it has closed the server CastSkill // Sent by client or server, indicates entity casting skill SpawnItem // Sent by server + + UnknownPacketType = 666 ) func (n NetPacketType) String() string { @@ -44,3 +48,9 @@ func (n NetPacketType) String() string { return strings[n] } + +func (n NetPacketType) MarshalPacket() []byte { + p, _ := json.Marshal(n) + + return p +} diff --git a/d2networking/d2netpacket/net_packet.go b/d2networking/d2netpacket/net_packet.go index 4c19536a..37b9870d 100644 --- a/d2networking/d2netpacket/net_packet.go +++ b/d2networking/d2netpacket/net_packet.go @@ -1,6 +1,10 @@ package d2netpacket -import "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" +import ( + "encoding/json" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" + "log" +) // NetPacket is used to wrap and send all packet types under d2netpacket. // When decoding a packet: First the PacketType byte is read, then the @@ -8,5 +12,31 @@ import "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackett // PacketType. type NetPacket struct { PacketType d2netpackettype.NetPacketType `json:"packetType"` - PacketData interface{} `json:"packetData"` + PacketData json.RawMessage `json:"packetData"` +} + +func InspectPacketType(b []byte) d2netpackettype.NetPacketType { + var packet NetPacket + + if err := json.Unmarshal(b, &packet); err != nil { + log.Println(err) + } + + return packet.PacketType +} + +func UnmarshalNetPacket(packet []byte) (NetPacket, error) { + var p NetPacket + if err := json.Unmarshal(packet, &p); err != nil { + return p, err + } + + return p, nil +} + +// MarshalPacket is a quick helper function to Marshal very anything UNSAFELY, meaning the error is not checked before sending. +func MarshalPacket(packet interface{}) []byte { + b, _ := json.Marshal(packet) + + return b } diff --git a/d2networking/d2netpacket/packet_add_player.go b/d2networking/d2netpacket/packet_add_player.go index df723ac6..abbfd12d 100644 --- a/d2networking/d2netpacket/packet_add_player.go +++ b/d2networking/d2netpacket/packet_add_player.go @@ -1,6 +1,7 @@ package d2netpacket import ( + "encoding/json" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory" @@ -24,16 +25,28 @@ type AddPlayerPacket struct { // AddPlayerPacket with the data in given parameters. func CreateAddPlayerPacket(id, name string, x, y int, heroType d2enum.Hero, stats *d2hero.HeroStatsState, equipment d2inventory.CharacterEquipment) NetPacket { + addPlayerPacket := AddPlayerPacket{ + ID: id, + Name: name, + X: x, + Y: y, + HeroType: heroType, + Equipment: equipment, + Stats: stats, + } + b, _ := json.Marshal(addPlayerPacket) + return NetPacket{ PacketType: d2netpackettype.AddPlayer, - PacketData: AddPlayerPacket{ - ID: id, - Name: name, - X: x, - Y: y, - HeroType: heroType, - Equipment: equipment, - Stats: stats, - }, + PacketData: b, } } + +func UnmarshalAddPlayer(packet []byte) (AddPlayerPacket, error) { + var p AddPlayerPacket + if err := json.Unmarshal(packet, &p); err != nil { + return p, err + } + + return p, nil +} diff --git a/d2networking/d2netpacket/packet_generate_map.go b/d2networking/d2netpacket/packet_generate_map.go index 8f0e3344..2d80ff28 100644 --- a/d2networking/d2netpacket/packet_generate_map.go +++ b/d2networking/d2netpacket/packet_generate_map.go @@ -1,6 +1,7 @@ package d2netpacket import ( + "encoding/json" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" ) @@ -15,10 +16,22 @@ type GenerateMapPacket struct { // CreateGenerateMapPacket returns a NetPacket which declares a // GenerateMapPacket with the given regionType. func CreateGenerateMapPacket(regionType d2enum.RegionIdType) NetPacket { + generateMapPacket := GenerateMapPacket{ + RegionType: regionType, + } + b, _ := json.Marshal(generateMapPacket) + return NetPacket{ PacketType: d2netpackettype.GenerateMap, - PacketData: GenerateMapPacket{ - RegionType: regionType, - }, + PacketData: b, } } + +func UnmarshalGenerateMap(packet []byte) (GenerateMapPacket, error) { + var p GenerateMapPacket + if err := json.Unmarshal(packet, &p); err != nil { + return p, err + } + + return p, nil +} diff --git a/d2networking/d2netpacket/packet_item_spawn.go b/d2networking/d2netpacket/packet_item_spawn.go index ff0894a9..414cc7da 100644 --- a/d2networking/d2netpacket/packet_item_spawn.go +++ b/d2networking/d2netpacket/packet_item_spawn.go @@ -1,6 +1,7 @@ package d2netpacket import ( + "encoding/json" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" ) @@ -14,12 +15,24 @@ type SpawnItemPacket struct { // CreateSpawnItemPacket returns a NetPacket which declares a // SpawnItemPacket with the data in given parameters. func CreateSpawnItemPacket(x, y int, codes ...string) NetPacket { + spawnItemPacket := SpawnItemPacket{ + X: x, + Y: y, + Codes: codes, + } + b, _ := json.Marshal(spawnItemPacket) + return NetPacket{ PacketType: d2netpackettype.SpawnItem, - PacketData: SpawnItemPacket{ - X: x, - Y: y, - Codes: codes, - }, + PacketData: b, } } + +func UnmarshalSpawnItem(packet []byte) (SpawnItemPacket, error) { + var p SpawnItemPacket + if err := json.Unmarshal(packet, &p); err != nil { + return p, err + } + + return p, nil +} diff --git a/d2networking/d2netpacket/packet_move_player.go b/d2networking/d2netpacket/packet_move_player.go index d7ce2e8a..0bc64042 100644 --- a/d2networking/d2netpacket/packet_move_player.go +++ b/d2networking/d2netpacket/packet_move_player.go @@ -1,6 +1,9 @@ package d2netpacket -import "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" +import ( + "encoding/json" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" +) // MovePlayerPacket contains a movement command for a specific player entity. // It is sent by the server to move a player entity on a client. @@ -16,14 +19,26 @@ type MovePlayerPacket struct { // CreateMovePlayerPacket returns a NetPacket which declares a MovePlayerPacket // with the given ID and movement command. func CreateMovePlayerPacket(playerID string, startX, startY, destX, destY float64) NetPacket { + movePlayerPacket := MovePlayerPacket{ + PlayerID: playerID, + StartX: startX, + StartY: startY, + DestX: destX, + DestY: destY, + } + b, _ := json.Marshal(movePlayerPacket) + return NetPacket{ PacketType: d2netpackettype.MovePlayer, - PacketData: MovePlayerPacket{ - PlayerID: playerID, - StartX: startX, - StartY: startY, - DestX: destX, - DestY: destY, - }, + PacketData: b, } } + +func UnmarshalMovePlayer(packet []byte) (MovePlayerPacket, error) { + var p MovePlayerPacket + if err := json.Unmarshal(packet, &p); err != nil { + return p, err + } + + return p, nil +} diff --git a/d2networking/d2netpacket/packet_ping.go b/d2networking/d2netpacket/packet_ping.go index 123db068..463f784e 100644 --- a/d2networking/d2netpacket/packet_ping.go +++ b/d2networking/d2netpacket/packet_ping.go @@ -1,6 +1,7 @@ package d2netpacket import ( + "encoding/json" "time" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" @@ -15,10 +16,13 @@ type PingPacket struct { // CreatePingPacket returns a NetPacket which declares a GenerateMapPacket // with the the current time. func CreatePingPacket() NetPacket { + ping := PingPacket{ + TS: time.Now(), + } + b, _ := json.Marshal(ping) + return NetPacket{ PacketType: d2netpackettype.Ping, - PacketData: PingPacket{ - TS: time.Now(), - }, + PacketData: b, } } diff --git a/d2networking/d2netpacket/packet_player_cast.go b/d2networking/d2netpacket/packet_player_cast.go index 50eb4909..22e54fd7 100644 --- a/d2networking/d2netpacket/packet_player_cast.go +++ b/d2networking/d2netpacket/packet_player_cast.go @@ -1,6 +1,9 @@ package d2netpacket -import "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" +import ( + "encoding/json" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" +) // CastPacket contains a cast command for an entity. It is sent by the server // and instructs the client to trigger the use of the given skill on the given @@ -17,14 +20,26 @@ type CastPacket struct { // CreateCastPacket returns a NetPacket which declares a CastPacket with the // given skill command. func CreateCastPacket(entityID string, skillID int, targetX, targetY float64) NetPacket { + castPacket := CastPacket{ + SourceEntityID: entityID, + SkillID: skillID, + TargetX: targetX, + TargetY: targetY, + TargetEntityID: "", // TODO implement targeting entities + } + b, _ := json.Marshal(castPacket) + return NetPacket{ PacketType: d2netpackettype.CastSkill, - PacketData: CastPacket{ - SourceEntityID: entityID, - SkillID: skillID, - TargetX: targetX, - TargetY: targetY, - TargetEntityID: "", // TODO implement targeting entities - }, + PacketData: b, } } + +func UnmarshalCast(packet []byte) (CastPacket, error) { + var p CastPacket + if err := json.Unmarshal(packet, &p); err != nil { + return p, err + } + + return p, nil +} diff --git a/d2networking/d2netpacket/packet_player_connection_request.go b/d2networking/d2netpacket/packet_player_connection_request.go index 646844d8..2c896343 100644 --- a/d2networking/d2netpacket/packet_player_connection_request.go +++ b/d2networking/d2netpacket/packet_player_connection_request.go @@ -1,6 +1,7 @@ package d2netpacket import ( + "encoding/json" "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" ) @@ -15,11 +16,23 @@ type PlayerConnectionRequestPacket struct { // CreatePlayerConnectionRequestPacket returns a NetPacket which defines a // PlayerConnectionRequestPacket with the given ID and game state. func CreatePlayerConnectionRequestPacket(id string, playerState *d2player.PlayerState) NetPacket { + playerConnectionRequest := PlayerConnectionRequestPacket{ + ID: id, + PlayerState: playerState, + } + b, _ := json.Marshal(playerConnectionRequest) + return NetPacket{ PacketType: d2netpackettype.PlayerConnectionRequest, - PacketData: PlayerConnectionRequestPacket{ - ID: id, - PlayerState: playerState, - }, + PacketData: b, } } + +func UnmarshalPlayerConnectionRequest(packet []byte) (PlayerConnectionRequestPacket, error) { + var resp PlayerConnectionRequestPacket + + if err := json.Unmarshal(packet, &resp); err != nil { + return PlayerConnectionRequestPacket{}, err + } + return resp, nil +} diff --git a/d2networking/d2netpacket/packet_player_disconnect_request.go b/d2networking/d2netpacket/packet_player_disconnect_request.go index 0c063d4a..64bff8d0 100644 --- a/d2networking/d2netpacket/packet_player_disconnect_request.go +++ b/d2networking/d2netpacket/packet_player_disconnect_request.go @@ -1,6 +1,7 @@ package d2netpacket import ( + "encoding/json" "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" ) @@ -15,10 +16,22 @@ type PlayerDisconnectRequestPacket struct { // CreatePlayerDisconnectRequestPacket returns a NetPacket which defines a // PlayerDisconnectRequestPacket with the given ID. func CreatePlayerDisconnectRequestPacket(id string) NetPacket { + playerDisconnectRequest := PlayerDisconnectRequestPacket{ + ID: id, + } + b, _ := json.Marshal(playerDisconnectRequest) + return NetPacket{ PacketType: d2netpackettype.PlayerDisconnectionNotification, - PacketData: PlayerDisconnectRequestPacket{ - ID: id, - }, + PacketData: b, } } + +func UnmarshalPlayerDisconnectionRequest(packet []byte) (PlayerDisconnectRequestPacket, error) { + var resp PlayerDisconnectRequestPacket + + if err := json.Unmarshal(packet, &resp); err != nil { + return resp, err + } + return resp, nil +} diff --git a/d2networking/d2netpacket/packet_pong.go b/d2networking/d2netpacket/packet_pong.go index 1e2ef4cc..b80a02b5 100644 --- a/d2networking/d2netpacket/packet_pong.go +++ b/d2networking/d2netpacket/packet_pong.go @@ -1,6 +1,7 @@ package d2netpacket import ( + "encoding/json" "time" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" @@ -16,11 +17,23 @@ type PongPacket struct { // CreatePongPacket returns a NetPacket which declares a PongPacket with // the current time and given ID. func CreatePongPacket(id string) NetPacket { + pong := PongPacket{ + ID: id, + TS: time.Now(), + } + b, _ := json.Marshal(pong) + return NetPacket{ PacketType: d2netpackettype.Pong, - PacketData: PongPacket{ - ID: id, - TS: time.Now(), - }, + PacketData: b, } } + +func UnmarshalPong(packet []byte) (PongPacket, error) { + var resp PongPacket + + if err := json.Unmarshal(packet, &resp); err != nil { + return resp, err + } + return resp, nil +} diff --git a/d2networking/d2netpacket/packet_server_closed.go b/d2networking/d2netpacket/packet_server_closed.go index 571aae76..fdeb175a 100644 --- a/d2networking/d2netpacket/packet_server_closed.go +++ b/d2networking/d2netpacket/packet_server_closed.go @@ -1,6 +1,7 @@ package d2netpacket import ( + "encoding/json" "time" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" @@ -15,10 +16,22 @@ type ServerClosedPacket struct { // CreateServerClosedPacket returns a NetPacket which declares a // ServerClosedPacket with the current time. func CreateServerClosedPacket() NetPacket { + serverClosed := ServerClosedPacket{ + TS: time.Now(), + } + b, _ := json.Marshal(serverClosed) + return NetPacket{ PacketType: d2netpackettype.ServerClosed, - PacketData: ServerClosedPacket{ - TS: time.Now(), - }, + PacketData: b, } } + +func UnmarshalServerClosed(packet []byte) (ServerClosedPacket, error) { + var resp ServerClosedPacket + + if err := json.Unmarshal(packet, &resp); err != nil { + return resp, err + } + return resp, nil +} diff --git a/d2networking/d2netpacket/packet_update_server_info.go b/d2networking/d2netpacket/packet_update_server_info.go index 5cd5eb2f..4173b69b 100644 --- a/d2networking/d2netpacket/packet_update_server_info.go +++ b/d2networking/d2netpacket/packet_update_server_info.go @@ -1,6 +1,9 @@ package d2netpacket -import "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" +import ( + "encoding/json" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" +) // UpdateServerInfoPacket contains the ID for a player and the map seed. // It is sent by the server to synchronize these values on the client. @@ -12,11 +15,23 @@ type UpdateServerInfoPacket struct { // CreateUpdateServerInfoPacket returns a NetPacket which declares an // UpdateServerInfoPacket with the given player ID and map seed. func CreateUpdateServerInfoPacket(seed int64, playerID string) NetPacket { + updateServerInfo := UpdateServerInfoPacket{ + Seed: seed, + PlayerID: playerID, + } + b, _ := json.Marshal(updateServerInfo) + return NetPacket{ PacketType: d2netpackettype.UpdateServerInfo, - PacketData: UpdateServerInfoPacket{ - Seed: seed, - PlayerID: playerID, - }, + PacketData: b, } } + +func UnmarshalUpdateServerInfo(packet []byte) (UpdateServerInfoPacket, error) { + var resp UpdateServerInfoPacket + + if err := json.Unmarshal(packet, &resp); err != nil { + return resp, err + } + return resp, nil +} diff --git a/d2networking/d2server/connection_manager.go b/d2networking/d2server/connection_manager.go deleted file mode 100644 index 484f70a4..00000000 --- a/d2networking/d2server/connection_manager.go +++ /dev/null @@ -1,106 +0,0 @@ -package d2server - -import ( - "log" - "sync" - "time" - - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" -) - -const ( - numRetries = 3 - second = 1000 -) - -// ConnectionManager is responsible for cleanup up connections accepted by the game server. -// As the server communicates over UDP and is stateless we need to implement some loose state -// management via a ping/pong system. ConnectionManager also handles communication for -// graceful shutdowns. -type ConnectionManager struct { - sync.RWMutex - retries int // Number of attempts before the dropping the client - interval time.Duration // How long to wait before each ping/pong test - gameServer *GameServer // The GameServer with the connections being managed - status map[string]int // Map of inflight ping/pong requests -} - -// CreateConnectionManager constructs a new ConnectionManager and calls -// ConnectionManager.Run() in a goroutine before retuning a pointer to -// the new ConnectionManager. -func CreateConnectionManager(gameServer *GameServer) *ConnectionManager { - manager := &ConnectionManager{ - retries: numRetries, - interval: time.Millisecond * second, - gameServer: gameServer, - status: make(map[string]int), - } - - go manager.Run() - - return manager -} - -// Run starts up any watchers for for the connection manager -func (c *ConnectionManager) Run() { - log.Print("Starting connection manager...") - - for { - c.checkPeers() - time.Sleep(c.interval) - } -} - -// checkPeers manages connection validation and cleanup for all peers. -func (c *ConnectionManager) checkPeers() { - for id, connection := range c.gameServer.clientConnections { - if connection.GetConnectionType() == d2clientconnectiontype.Local { - continue - } - - if err := connection.SendPacketToClient(d2netpacket.CreatePingPacket()); err != nil { - log.Printf("Cannot ping client id: %s", id) - } - - c.RWMutex.Lock() - - c.status[id]++ - - if c.status[id] >= c.retries { - delete(c.status, id) - c.Drop(id) - } - - c.RWMutex.Unlock() - } -} - -// Recv simply resets the counter, acknowledging we have received a pong from the client. -func (c *ConnectionManager) Recv(id string) { - c.status[id] = 0 -} - -// Drop removes the client id from the connection pool of the game server. -func (c *ConnectionManager) Drop(id string) { - c.gameServer.RWMutex.Lock() - defer c.gameServer.RWMutex.Unlock() - delete(c.gameServer.clientConnections, id) - log.Printf("%s has been disconnected...", id) -} - -// Shutdown will notify all of the clients that the server has been shutdown. -func (c *ConnectionManager) Shutdown() { - // TODO: Currently this will never actually get called as the go routines are never signaled about the application termination. - // Things can be done more cleanly once we have graceful exits however we still need to account for other OS Signals - log.Print("Notifying clients server is shutting down...") - - for _, connection := range c.gameServer.clientConnections { - err := connection.SendPacketToClient(d2netpacket.CreateServerClosedPacket()) - if err != nil { - log.Printf("ConnectionManager: error sending ServerClosedPacket to client ID %s: %s", connection.GetUniqueID(), err) - } - } - - Stop() -} diff --git a/d2networking/d2server/d2tcpclientconnection/tcp_client_connection.go b/d2networking/d2server/d2tcpclientconnection/tcp_client_connection.go new file mode 100644 index 00000000..0f106d6e --- /dev/null +++ b/d2networking/d2server/d2tcpclientconnection/tcp_client_connection.go @@ -0,0 +1,55 @@ +package d2tcpclientconnection + +import ( + "encoding/json" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" + "net" + + "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" +) + +type TCPClientConnection struct { + id string + tcpConnection net.Conn + playerState *d2player.PlayerState +} + +func CreateTCPClientConnection(tcpConnection net.Conn, id string) *TCPClientConnection { + return &TCPClientConnection{ + tcpConnection: tcpConnection, + id: id, + } +} + +func (t TCPClientConnection) GetUniqueID() string { + return t.id +} + +func (t *TCPClientConnection) SendPacketToClient(p d2netpacket.NetPacket) error { + packet, err := json.Marshal(p) + if err != nil { + return err + } + + _, err = t.tcpConnection.Write(packet) + if err != nil { + return err + } + + return nil +} + +func (t *TCPClientConnection) SetPlayerState(playerState *d2player.PlayerState) { + t.playerState = playerState +} + +func (t *TCPClientConnection) GetPlayerState() *d2player.PlayerState { + return t.playerState +} + +// GetConnectionType returns an enum representing the connection type. +// See: d2clientconnectiontype. +func (t TCPClientConnection) GetConnectionType() d2clientconnectiontype.ClientConnectionType { + return d2clientconnectiontype.LANClient +} diff --git a/d2networking/d2server/game_server.go b/d2networking/d2server/game_server.go index 9ea5fb3f..790a75e6 100644 --- a/d2networking/d2server/game_server.go +++ b/d2networking/d2server/game_server.go @@ -1,14 +1,13 @@ package d2server import ( - "bytes" - "compress/gzip" + "context" "encoding/json" + "errors" "fmt" - "io" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2server/d2tcpclientconnection" "log" "net" - "strings" "sync" "time" @@ -17,296 +16,309 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2server/d2udpclientconnection" "github.com/OpenDiablo2/OpenDiablo2/d2script" "github.com/robertkrimen/otto" ) const ( - udpBufferSize = 4096 - subtilesPerTile = 5 - middleOfTileOffset = 3 + Port = "6669" + ChunkSize int = 4096 + subtilesPerTile = 5 + middleOfTileOffset = 3 ) -// GameServer owns the authoritative copy of the map and entities -// It accepts incoming connections from local (host) and remote -// clients. +var ( + ErrPlayerAlreadyExists = errors.New("player already exists") + ErrServerFull = errors.New("server full") // Server currently at maximum TCP connections +) + +// GameServer manages a copy of the map and entities as well as manages packet routing and connections. +// It can accept connections from localhost as well remote clients. It can also be started in a standalone mode. type GameServer struct { sync.RWMutex - clientConnections map[string]ClientConnection - manager *ConnectionManager + connections map[string]ClientConnection + listener net.Listener + networkServer bool + ctx context.Context + cancel context.CancelFunc mapEngines []*d2mapengine.MapEngine scriptEngine *d2script.ScriptEngine - udpConnection *net.UDPConn seed int64 - running bool + maxConnections int + packetManagerChan chan []byte } //nolint:gochecknoglobals // currently singleton by design var singletonServer *GameServer -// Create constructs a new GameServer and assigns it as a singleton. It -// also generates the initial map and entities for the server. +// NewGameServer builds a new GameServer that can be started // -// If openNetworkServer is true, the GameServer starts listening for UDP -// packets. -func Create(openNetworkServer bool) { - log.Print("Creating GameServer") - - if singletonServer != nil { - return +// ctx: required context item +// networkServer: true = 0.0.0.0 | false = 127.0.0.1 +// maxConnections (default: 8): maximum number of TCP connections allowed open +func NewGameServer(networkServer bool, maxConnections ...int) (*GameServer, error) { + if len(maxConnections) == 0 { + maxConnections = []int{8} } - singletonServer = &GameServer{ - clientConnections: make(map[string]ClientConnection), + ctx, cancel := context.WithCancel(context.Background()) + + gameServer := &GameServer{ + ctx: ctx, + cancel: cancel, + connections: make(map[string]ClientConnection), + networkServer: networkServer, + maxConnections: maxConnections[0], + packetManagerChan: make(chan []byte), mapEngines: make([]*d2mapengine.MapEngine, 0), scriptEngine: d2script.CreateScriptEngine(), seed: time.Now().UnixNano(), } - singletonServer.manager = CreateConnectionManager(singletonServer) - + // TODO: In order to support dedicated mode we need to load the levels txt and files. Revisit this once this we can + // load files independent of the app. mapEngine := d2mapengine.CreateMapEngine() - mapEngine.SetSeed(singletonServer.seed) - - // TODO: Mapgen - Needs levels.txt stuff - mapEngine.ResetMap(d2enum.RegionAct1Town, 100, 100) - + mapEngine.SetSeed(gameServer.seed) + mapEngine.ResetMap(d2enum.RegionAct1Town, 100, 100) // TODO: Mapgen - Needs levels.txt stuff d2mapgen.GenerateAct1Overworld(mapEngine) - singletonServer.mapEngines = append(singletonServer.mapEngines, mapEngine) - singletonServer.scriptEngine.AddFunction("getMapEngines", func(call otto.FunctionCall) otto.Value { - val, err := singletonServer.scriptEngine.ToValue(singletonServer.mapEngines) + gameServer.mapEngines = append(gameServer.mapEngines, mapEngine) + + gameServer.scriptEngine.AddFunction("getMapEngines", func(call otto.FunctionCall) otto.Value { + val, err := gameServer.scriptEngine.ToValue(singletonServer.mapEngines) if err != nil { fmt.Print(err.Error()) } return val }) - if openNetworkServer { - createNetworkServer() + // TODO: Temporary hack to work around local connections. Possible that we can move away from the singleton pattern here + // but for now this will work. + singletonServer = gameServer + + return gameServer, nil +} + +// Start essentially starts all of the game server go routines as well as begins listening for connection. This will +// return an error if it is unable to bind to a socket. +func (g *GameServer) Start() error { + listenerAddress := "127.0.0.1:" + Port + if g.networkServer { + listenerAddress = "0.0.0.0:" + Port + } + + log.Printf("Starting Game Server @ %s\n", listenerAddress) + l, err := net.Listen("tcp4", listenerAddress) + if err != nil { + return err + } + + g.listener = l + + go g.packetManager() + + go func() { + for { + c, err := g.listener.Accept() + if err != nil { + log.Printf("Unable to accept connection: %s\n", err) + } + + go g.handleConnection(c) + } + }() + + return nil +} + +func (g *GameServer) Stop() { + g.Lock() + g.cancel() + + g.listener.Close() +} + +// packetManager is meant to be started as a Goroutine and is used to manage routing of packets to clients. +func (g *GameServer) packetManager() { + defer close(g.packetManagerChan) + + for { + select { + // If the server is stopped we need to clean up the packet manager goroutine + case <-g.ctx.Done(): + return + case p := <-g.packetManagerChan: + switch d2netpacket.InspectPacketType(p) { + case d2netpackettype.PlayerConnectionRequest: + player, err := d2netpacket.UnmarshalNetPacket(p) + if err != nil { + log.Printf("Unable to unmarshal PlayerConnectionRequestPacket: %s\n", err) + } + g.sendPacketToClients(player) + case d2netpackettype.MovePlayer: + move, err := d2netpacket.UnmarshalNetPacket(p) + if err != nil { + log.Println(err) + continue + } + g.sendPacketToClients(move) + case d2netpackettype.SpawnItem: + item, err := d2netpacket.UnmarshalNetPacket(p) + if err != nil { + log.Println(err) + continue + } + g.sendPacketToClients(item) + case d2netpackettype.ServerClosed: + g.Stop() + } + } } } -func createNetworkServer() { - s, err := net.ResolveUDPAddr("udp4", "0.0.0.0:6669") - if err != nil { - panic(err) - } - - singletonServer.udpConnection, err = net.ListenUDP("udp4", s) - if err != nil { - panic(err) - } - - err = singletonServer.udpConnection.SetReadBuffer(udpBufferSize) - - if err != nil { - log.Print("GameServer: error setting UDP read buffer:", err) +func (g *GameServer) sendPacketToClients(packet d2netpacket.NetPacket) { + for _, c := range g.connections { + c.SendPacketToClient(packet) } } -// runNetworkServer runs a while loop, reading from the GameServer's UDP -// connection. -func runNetworkServer() { - buffer := make([]byte, 4096) +// handleConnection accepts an individual connection and starts pooling for new packets. It is recommended this is called +// via Go Routine. Context should be a property of the GameServer Struct. +func (g *GameServer) handleConnection(conn net.Conn) { + var connected int + var packet d2netpacket.NetPacket - for singletonServer.running { - _, addr, udpReadErr := singletonServer.udpConnection.ReadFromUDP(buffer) - if udpReadErr != nil { - fmt.Printf("Socket error: %s\n", udpReadErr) + log.Printf("Accepting connection: %s", conn.RemoteAddr().String()) + defer conn.Close() + + decoder := json.NewDecoder(conn) + + for { + err := decoder.Decode(&packet) + if err != nil { + log.Println(err) + return // exit this connection as we could not read the first packet + } + + // If this is the first packet we are seeing from this specific connection we first need to see if the client + // is sending a valid request. If this is a valid request, we will register it and flip the connected switch + // to. + if connected == 0 { + if packet.PacketType != d2netpackettype.PlayerConnectionRequest { + log.Printf("Closing connection with %s: did not receive new player connection request...\n", conn.RemoteAddr().String()) + } + + // TODO: I do not think this error check actually works. Need to retrofit with Errors.Is(). + if err := g.registerConnection(packet.PacketData, conn); err != nil { + switch err { + case ErrServerFull: // Server is currently full and not accepting new connections. + // TODO: Need to create a new Server Full packet to return to clients. + log.Println(err) + return + case ErrPlayerAlreadyExists: // Player is already registered and did not disconnection correctly. + log.Println(err) + return + } + } + + connected = 1 + } + + select { + case <-g.ctx.Done(): + return + default: + g.packetManagerChan <- packet.PacketData + } + } +} + +// registerConnection accepts a PlayerConnectionRequestPacket and thread safely updates the connection pool +// +// Errors: +// - ErrServerFull +// - ErrPlayerAlreadyExists +func (g *GameServer) registerConnection(b []byte, conn net.Conn) error { + g.Lock() + + // check to see if the server is full + if len(g.connections) >= g.maxConnections { + return ErrServerFull + } + + // if it is not full, unmarshal the playerConnectionRequest + packet, err := d2netpacket.UnmarshalPlayerConnectionRequest(b) + if err != nil { + log.Printf("Failed to unmarshal PlayerConnectionRequest: %s\n", err) + } + + // check to see if the player is already registered + if _, ok := g.connections[packet.ID]; ok { + return ErrPlayerAlreadyExists + } + + // Client a new TCP Client Connection and add it to the connections map + client := d2tcpclientconnection.CreateTCPClientConnection(conn, packet.ID) + client.SetPlayerState(packet.PlayerState) + log.Printf("Client connected with an id of %s", client.GetUniqueID()) + g.connections[client.GetUniqueID()] = client + + // Temporary position hack -------------------------------------------- + sx, sy := g.mapEngines[0].GetStartPosition() // TODO: Another temporary hack + clientPlayerState := client.GetPlayerState() + clientPlayerState.X = sx + clientPlayerState.Y = sy + // --------- + + // This really should be deferred however to much time will be spend holding a lock when we attempt to send a packet + g.Unlock() + + if err := client.SendPacketToClient(d2netpacket.CreateUpdateServerInfoPacket(g.seed, client.GetUniqueID())); err != nil { + log.Printf("GameServer: error sending UpdateServerInfoPacket to client %s: %s", client.GetUniqueID(), err) + } + if err := client.SendPacketToClient(d2netpacket.CreateGenerateMapPacket(d2enum.RegionAct1Town)); err != nil { + log.Printf("GameServer: error sending GenerateMapPacket to client %s: %s", client.GetUniqueID(), err) + } + + playerState := client.GetPlayerState() + + // these are in subtiles + playerX := int(sx*subtilesPerTile) + middleOfTileOffset + playerY := int(sy*subtilesPerTile) + middleOfTileOffset + + createPlayerPacket := d2netpacket.CreateAddPlayerPacket(client.GetUniqueID(), + playerState.HeroName, playerX, playerY, + playerState.HeroType, playerState.Stats, playerState.Equipment) + + for _, connection := range g.connections { + err := connection.SendPacketToClient(createPlayerPacket) + if err != nil { + log.Printf("GameServer: error sending %T to client %s: %s", createPlayerPacket, connection.GetUniqueID(), err) + } + + if connection.GetUniqueID() == client.GetUniqueID() { continue } - buff := bytes.NewBuffer(buffer) + conPlayerState := connection.GetPlayerState() + playerX := int(conPlayerState.X*subtilesPerTile) + middleOfTileOffset + playerY := int(conPlayerState.Y*subtilesPerTile) + middleOfTileOffset + err = client.SendPacketToClient(d2netpacket.CreateAddPlayerPacket( + connection.GetUniqueID(), + conPlayerState.HeroName, + playerX, playerY, + conPlayerState.HeroType, + conPlayerState.Stats, conPlayerState.Equipment), + ) - packetTypeID, _ := buff.ReadByte() - packetType := d2netpackettype.NetPacketType(packetTypeID) - - reader, _ := gzip.NewReader(buff) - sb := new(strings.Builder) - - // This will throw errors where packets are not compressed. This doesn't - // break anything, so the gzip.ErrHeader error, is currently ignored to - // avoid noisy logging. - written, copyErr := io.Copy(sb, reader) - - if copyErr != nil && copyErr != gzip.ErrHeader { - log.Printf("GameServer: error copying bytes from %v packet: %s", packetType, copyErr) - } - - if written == 0 { - log.Printf("GameServer: empty packet %v packet received", packetType) - continue - } - - stringData := sb.String() - - switch packetType { - case d2netpackettype.PlayerConnectionRequest: - if err := handlePlayerConnectionRequest(addr, stringData); err != nil { - log.Printf("GameServer error: %v", err) - } - case d2netpackettype.MovePlayer: - if err := handleMovePlayer(packetType, stringData); err != nil { - log.Printf("GameServer error: %v", err) - } - case d2netpackettype.SpawnItem: - if err := handleSpawnItem(packetType, stringData); err != nil { - log.Printf("GameServer error: %v", err) - } - case d2netpackettype.Pong: - if err := handlePingPong(stringData); err != nil { - log.Printf("GameServer error: %v", err) - } - case d2netpackettype.ServerClosed: - singletonServer.manager.Shutdown() - case d2netpackettype.PlayerDisconnectionNotification: - if err := handlePlayerDisconnectNotification(stringData); err != nil { - log.Printf("GameServer error: %v", err) - } - } - } -} - -func handlePlayerConnectionRequest(addr *net.UDPAddr, stringData string) error { - packetData := d2netpacket.PlayerConnectionRequestPacket{} - err := json.Unmarshal([]byte(stringData), &packetData) - - if err != nil { - log.Printf("GameServer: error unmarshalling packet of type %T: %s", packetData, err) - return err - } - - clientConnection := d2udpclientconnection.CreateUDPClientConnection(singletonServer.udpConnection, packetData.ID, addr) - - clientConnection.SetPlayerState(packetData.PlayerState) - OnClientConnected(clientConnection) - - return nil -} - -func handleMovePlayer(packetType d2netpackettype.NetPacketType, stringData string) error { - packetData := d2netpacket.MovePlayerPacket{} - err := json.Unmarshal([]byte(stringData), &packetData) - - if err != nil { - log.Printf("GameServer: error unmarshalling %T: %s", packetData, err) - return err - } - - netPacket := d2netpacket.NetPacket{ - PacketType: packetType, - PacketData: packetData, - } - - for _, player := range singletonServer.clientConnections { - err = player.SendPacketToClient(netPacket) if err != nil { - log.Printf("GameServer: error sending %T to client %s: %s", packetData, player.GetUniqueID(), err) + log.Printf("GameServer: error sending CreateAddPlayerPacket to client %s: %s", connection.GetUniqueID(), err) } } return nil } -func handleSpawnItem(packetType d2netpackettype.NetPacketType, stringData string) error { - packetData := d2netpacket.SpawnItemPacket{} - err := json.Unmarshal([]byte(stringData), &packetData) - - if err != nil { - log.Printf("GameServer: error unmarshalling %T: %s", packetData, err) - return err - } - - netPacket := d2netpacket.NetPacket{ - PacketType: packetType, - PacketData: packetData, - } - - for _, player := range singletonServer.clientConnections { - err = player.SendPacketToClient(netPacket) - if err != nil { - log.Printf("GameServer: error sending %T to client %s: %s", packetData, player.GetUniqueID(), err) - } - } - - return nil -} - -func handlePingPong(stringData string) error { - packetData := d2netpacket.PlayerConnectionRequestPacket{} - err := json.Unmarshal([]byte(stringData), &packetData) - - if err != nil { - log.Printf("GameServer: error unmarshalling packet of type %T: %s", packetData, err) - return err - } - - singletonServer.manager.Recv(packetData.ID) - - return nil -} - -func handlePlayerDisconnectNotification(stringData string) error { - var packet d2netpacket.PlayerDisconnectRequestPacket - err := json.Unmarshal([]byte(stringData), &packet) - - if err != nil { - log.Printf("GameServer: error unmarshalling packet of type %T: %s", packet, err) - return err - } - - log.Printf("Received disconnect: %s", packet.ID) - - return nil -} - -// Run sets GameServer.running to true and call runNetworkServer -// in a goroutine. -func Run() { - log.Print("Starting GameServer") - - singletonServer.running = true - _, err := singletonServer.scriptEngine.RunScript("scripts/server/server.js") - - if err != nil { - log.Printf("GameServer: error initializing debug script: %s", err) - } - - if singletonServer.udpConnection != nil { - go runNetworkServer() - } - - log.Print("Network server has been started") -} - -// Stop sets GameServer.running to false and closes the -// GameServer's UDP connection. -func Stop() { - log.Print("Stopping GameServer") - - singletonServer.running = false - - if singletonServer.udpConnection != nil { - err := singletonServer.udpConnection.Close() - if err != nil { - log.Printf("GameServer: error when trying to close UDP connection: %s", err) - } - } -} - -// Destroy calls Stop() if the server exists. -func Destroy() { - if singletonServer == nil { - return - } - - log.Print("Destroying GameServer") - - Stop() -} - // OnClientConnected initializes the given ClientConnection. It sends the // following packets to the newly connected client: UpdateServerInfoPacket, // GenerateMapPacket, AddPlayerPacket. @@ -324,7 +336,7 @@ func OnClientConnected(client ClientConnection) { // -------------------------------------------------------------------- log.Printf("Client connected with an id of %s", client.GetUniqueID()) - singletonServer.clientConnections[client.GetUniqueID()] = client + singletonServer.connections[client.GetUniqueID()] = client err := client.SendPacketToClient(d2netpacket.CreateUpdateServerInfoPacket(singletonServer.seed, client.GetUniqueID())) if err != nil { @@ -347,7 +359,7 @@ func OnClientConnected(client ClientConnection) { playerState.HeroName, playerX, playerY, playerState.HeroType, playerState.Stats, playerState.Equipment) - for _, connection := range singletonServer.clientConnections { + for _, connection := range singletonServer.connections { err := connection.SendPacketToClient(createPlayerPacket) if err != nil { log.Printf("GameServer: error sending %T to client %s: %s", createPlayerPacket, connection.GetUniqueID(), err) @@ -378,35 +390,36 @@ func OnClientConnected(client ClientConnection) { // of client connections. func OnClientDisconnected(client ClientConnection) { log.Printf("Client disconnected with an id of %s", client.GetUniqueID()) - delete(singletonServer.clientConnections, client.GetUniqueID()) + delete(singletonServer.connections, client.GetUniqueID()) } // OnPacketReceived is called by the local client to 'send' a packet to the server. func OnPacketReceived(client ClientConnection, packet d2netpacket.NetPacket) error { switch packet.PacketType { case d2netpackettype.MovePlayer: + movePacket, _ := d2netpacket.UnmarshalMovePlayer(packet.PacketData) // TODO: This needs to be verified on the server (here) before sending to other clients.... // TODO: Hacky, this should be updated in realtime ---------------- // TODO: Verify player id - playerState := singletonServer.clientConnections[client.GetUniqueID()].GetPlayerState() - playerState.X = packet.PacketData.(d2netpacket.MovePlayerPacket).DestX - playerState.Y = packet.PacketData.(d2netpacket.MovePlayerPacket).DestY + playerState := singletonServer.connections[client.GetUniqueID()].GetPlayerState() + playerState.X = movePacket.DestX + playerState.Y = movePacket.DestY // ---------------------------------------------------------------- - for _, player := range singletonServer.clientConnections { + for _, player := range singletonServer.connections { err := player.SendPacketToClient(packet) if err != nil { log.Printf("GameServer: error sending %T to client %s: %s", packet, player.GetUniqueID(), err) } } case d2netpackettype.CastSkill: - for _, player := range singletonServer.clientConnections { + for _, player := range singletonServer.connections { err := player.SendPacketToClient(packet) if err != nil { log.Printf("GameServer: error sending %T to client %s: %s", packet, player.GetUniqueID(), err) } } case d2netpackettype.SpawnItem: - for _, player := range singletonServer.clientConnections { + for _, player := range singletonServer.connections { err := player.SendPacketToClient(packet) if err != nil { log.Printf("GameServer: error sending %T to client %s: %s", packet, player.GetUniqueID(), err) @@ -414,6 +427,5 @@ func OnPacketReceived(client ClientConnection, packet d2netpacket.NetPacket) err } } - return nil }