mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-05 08:07:51 -05:00
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.
This commit is contained in:
parent
0ea6bd5b92
commit
2254e4b2a6
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user