1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-13 03:00:42 +00:00
OpenDiablo2/d2networking/d2server/game_server.go
Ian Ling 3936e01afb Networking bugfixes and cleanup
Make sure connections close properly, without weird error messages
Remove player map entity when a player disconnects from a multiplayer game
Close server properly when host disconnects, handle ServerClose on remote clients
Don't mix JSON decoders and raw TCP writes
Actually handle incoming packets from remote clients
General code cleanup for simplicity and consistency
2020-12-28 13:33:17 -08:00

497 lines
14 KiB
Go

package d2server
import (
"context"
"encoding/json"
"errors"
"io"
"net"
"sync"
"time"
"github.com/robertkrimen/otto"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2server/d2tcpclientconnection"
"github.com/OpenDiablo2/OpenDiablo2/d2script"
)
const logPrefix = "Game Server"
const (
port = "6669"
chunkSize int = 4096 // nolint:deadcode,unused,varcheck // WIP
subtilesPerTile = 5
middleOfTileOffset = 3
)
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
connections map[string]ClientConnection
listener net.Listener
networkServer bool
ctx context.Context
cancel context.CancelFunc
asset *d2asset.AssetManager
mapEngines []*d2mapengine.MapEngine
scriptEngine *d2script.ScriptEngine
seed int64
maxConnections int
packetManagerChan chan ReceivedPacket
heroStateFactory *d2hero.HeroStateFactory
*d2util.Logger
}
// ReceivedPacket encapsulates the data necessary for the packet manager goroutine to process data from clients.
// The packet manager needs to know who sent the data, in addition to the data itself.
type ReceivedPacket struct {
Client ClientConnection
Packet d2netpacket.NetPacket
}
// NewGameServer builds a new GameServer that can be started
//
// 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(asset *d2asset.AssetManager,
networkServer bool,
l d2util.LogLevel,
maxConnections ...int) (*GameServer,
error) {
if len(maxConnections) == 0 {
maxConnections = []int{8}
}
heroStateFactory, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
gameServer := &GameServer{
ctx: ctx,
cancel: cancel,
asset: asset,
connections: make(map[string]ClientConnection),
networkServer: networkServer,
maxConnections: maxConnections[0],
packetManagerChan: make(chan ReceivedPacket),
mapEngines: make([]*d2mapengine.MapEngine, 0),
scriptEngine: d2script.CreateScriptEngine(),
seed: time.Now().UnixNano(),
heroStateFactory: heroStateFactory,
}
gameServer.Logger = d2util.NewLogger()
gameServer.Logger.SetPrefix(logPrefix)
gameServer.Logger.SetLevel(l)
mapEngine := d2mapengine.CreateMapEngine(l, asset)
mapEngine.SetSeed(gameServer.seed)
mapEngine.ResetMap(d2enum.RegionAct1Town, 100, 100)
mapGen, err := d2mapgen.NewMapGenerator(asset, l, mapEngine)
if err != nil {
return nil, err
}
mapGen.GenerateAct1Overworld()
gameServer.mapEngines = append(gameServer.mapEngines, mapEngine)
gameServer.scriptEngine.AddFunction("getMapEngines", func(call otto.FunctionCall) otto.Value {
val, err := gameServer.scriptEngine.ToValue(gameServer.mapEngines)
if err != nil {
gameServer.Error(err.Error())
}
return val
})
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
}
g.Infof("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 {
select {
case <-g.ctx.Done():
// this error was just a result of the server closing, don't worry about it
default:
g.Errorf("Unable to accept connection: %s", err)
}
return
}
go g.handleConnection(c)
}
}()
return nil
}
// Stop stops the game server
func (g *GameServer) Stop() {
g.Lock()
g.cancel()
g.connections = make(map[string]ClientConnection)
if err := g.listener.Close(); err != nil {
g.Errorf("failed to close the listener %s, err: %v\n", g.listener.Addr(), err)
}
}
// 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:
err := g.OnPacketReceived(p.Client, p.Packet)
if err != nil {
g.Errorf("failed to handle packet received from client %s: %v", p.Client.GetUniqueID(), err)
}
}
}
}
func (g *GameServer) sendPacketToClients(packet d2netpacket.NetPacket) {
for _, c := range g.connections {
if err := c.SendPacketToClient(packet); err != nil {
g.Errorf("GameServer: error sending packet: %s to client %s: %s", packet.PacketType, c.GetUniqueID(), err)
}
}
}
// 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
client ClientConnection
)
g.Infof("Accepting connection: %s\n", conn.RemoteAddr().String())
defer func() {
if err := conn.Close(); err != nil {
g.Errorf("failed to close the connection: %s\n", conn.RemoteAddr())
}
}()
decoder := json.NewDecoder(conn)
for {
var packet d2netpacket.NetPacket
err := decoder.Decode(&packet)
if err != nil {
switch err {
case io.EOF:
break // the other side closed the connection
default:
g.Error(err.Error())
}
return // allow the connection to close
}
// 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 {
g.Infof("Closing connection with %s: did not receive new player connection request...", conn.RemoteAddr().String())
}
if client, err = g.registerConnection(packet.PacketData, conn); err != nil {
return
}
connected = 1
}
select {
case <-g.ctx.Done():
return
default:
g.packetManagerChan <- ReceivedPacket{
Client: client,
Packet: packet,
}
}
}
}
// registerConnection accepts a PlayerConnectionRequestPacket and thread safely updates the connection pool
//
// Errors:
// - errServerFull
// - errPlayerAlreadyExists
func (g *GameServer) registerConnection(b []byte, conn net.Conn) (ClientConnection, error) {
var client ClientConnection
g.Lock()
defer g.Unlock()
// check to see if the server is full
if len(g.connections) >= g.maxConnections {
sf, serverFullErr := d2netpacket.CreateServerFullPacket()
if serverFullErr != nil {
g.Errorf("ServerFullPacket: %v", serverFullErr)
}
msf, marshalServerFullErr := d2netpacket.MarshalPacket(sf)
if marshalServerFullErr != nil {
g.Errorf("MarshalPacket: %v", marshalServerFullErr)
}
_, errServerFullPacket := conn.Write(msf)
g.Warningf("%v", errServerFullPacket)
return client, errServerFull
}
// if it is not full, unmarshal the playerConnectionRequest
packet, err := d2netpacket.UnmarshalPlayerConnectionRequest(b)
if err != nil {
g.Errorf("Failed to unmarshal PlayerConnectionRequest: %s\n", err)
}
// check to see if the player is already registered
if _, ok := g.connections[packet.ID]; ok {
g.Errorf("%v", errPlayerAlreadyExists)
return client, errPlayerAlreadyExists
}
// Client a new TCP Client Connection and add it to the connections map
client = d2tcpclientconnection.CreateTCPClientConnection(conn, packet.ID)
client.SetPlayerState(packet.PlayerState)
g.OnClientConnected(client)
return client, nil
}
// OnClientConnected initializes the given ClientConnection. It sends the
// following packets to the newly connected client: UpdateServerInfoPacket,
// GenerateMapPacket, AddPlayerPacket.
//
// It also sends AddPlayerPackets for each other player entity to the new
// player and vice versa, so all player entities exist on all clients.
//
// For more information, see d2networking.d2netpacket.
func (g *GameServer) OnClientConnected(client ClientConnection) {
// Temporary position hack --------------------------------------------
// https://github.com/OpenDiablo2/OpenDiablo2/issues/829
sx, sy := g.mapEngines[0].GetStartPosition()
clientPlayerState := client.GetPlayerState()
clientPlayerState.X = sx
clientPlayerState.Y = sy
// --------------------------------------------------------------------
g.Infof("Client connected with an id of %s", client.GetUniqueID())
g.connections[client.GetUniqueID()] = client
g.handleClientConnection(client, sx, sy)
}
func (g *GameServer) handleClientConnection(client ClientConnection, x, y float64) {
usi, err := d2netpacket.CreateUpdateServerInfoPacket(g.seed, client.GetUniqueID())
if err != nil {
g.Errorf("UpdateServerInfoPacket: %v", err)
}
err = client.SendPacketToClient(usi)
if err != nil {
g.Errorf("GameServer: error sending UpdateServerInfoPacket to client %s: %s", client.GetUniqueID(), err)
}
gmp, err := d2netpacket.CreateGenerateMapPacket(d2enum.RegionAct1Town)
if err != nil {
g.Errorf("GenerateMapPacket: %v", err)
}
err = client.SendPacketToClient(gmp)
if err != nil {
g.Errorf("GameServer: error sending GenerateMapPacket to client %s: %s", client.GetUniqueID(), err)
}
playerState := client.GetPlayerState()
// these are in subtiles
playerX := int(x*subtilesPerTile) + middleOfTileOffset
playerY := int(y*subtilesPerTile) + middleOfTileOffset
d2hero.HydrateSkills(playerState.Skills, g.asset)
createPlayerPacket, err := d2netpacket.CreateAddPlayerPacket(
client.GetUniqueID(),
playerState.HeroName,
playerX,
playerY,
playerState.HeroType,
playerState.Stats,
playerState.Skills,
playerState.Equipment,
playerState.LeftSkill,
playerState.RightSkill,
playerState.Gold,
)
if err != nil {
g.Errorf("AddPlayerPacket: %v", err)
}
for _, connection := range g.connections {
err := connection.SendPacketToClient(createPlayerPacket)
if err != nil {
g.Errorf("GameServer: error sending %T to client %s: %s", createPlayerPacket, connection.GetUniqueID(), err)
}
if connection.GetUniqueID() == client.GetUniqueID() {
continue
}
conPlayerState := connection.GetPlayerState()
playerX := int(conPlayerState.X*subtilesPerTile) + middleOfTileOffset
playerY := int(conPlayerState.Y*subtilesPerTile) + middleOfTileOffset
app, err := d2netpacket.CreateAddPlayerPacket(
connection.GetUniqueID(),
conPlayerState.HeroName,
playerX,
playerY,
conPlayerState.HeroType,
conPlayerState.Stats,
conPlayerState.Skills,
conPlayerState.Equipment,
conPlayerState.LeftSkill,
conPlayerState.RightSkill,
conPlayerState.Gold,
)
if err != nil {
g.Errorf("AddPlayerPacket: %v", err)
}
err = client.SendPacketToClient(app)
if err != nil {
g.Errorf("GameServer: error sending CreateAddPlayerPacket to client %s: %s", connection.GetUniqueID(), err)
}
}
}
// OnClientDisconnected removes the given client from the list
// of client connections.
// If this client was the host, disconnects all clients and kills GameServer.
func (g *GameServer) OnClientDisconnected(client ClientConnection) {
g.Infof("Client disconnected with an id of %s", client.GetUniqueID())
delete(g.connections, client.GetUniqueID())
if client.GetConnectionType() == d2clientconnectiontype.Local {
g.Info("Host disconnected, game server shuting down")
serverClosed, err := d2netpacket.CreateServerClosedPacket()
if err != nil {
g.Errorf("failed to generate ServerClosed packet after host disconnected: %s", err)
} else {
g.sendPacketToClients(serverClosed)
}
g.Stop()
}
}
// OnPacketReceived is called when a packet has been received from a remote client,
// and by the local client to 'send' a packet to the server,
// nolint:gocyclo // switch statement on packet type makes sense, no need to change
func (g *GameServer) OnPacketReceived(client ClientConnection, packet d2netpacket.NetPacket) error {
if g == nil {
return errors.New("game server is nil")
}
switch packet.PacketType {
case d2netpackettype.MovePlayer:
movePacket, err := d2netpacket.UnmarshalMovePlayer(packet.PacketData)
if err != nil {
return err
}
playerState := g.connections[client.GetUniqueID()].GetPlayerState()
playerState.X = movePacket.DestX
playerState.Y = movePacket.DestY
g.sendPacketToClients(packet)
case d2netpackettype.CastSkill, d2netpackettype.SpawnItem:
g.sendPacketToClients(packet)
case d2netpackettype.SavePlayer:
savePacket, err := d2netpacket.UnmarshalSavePlayer(packet.PacketData)
if err != nil {
return err
}
playerState := g.connections[client.GetUniqueID()].GetPlayerState()
playerState.LeftSkill = savePacket.Player.LeftSkill.Shallow.SkillID
playerState.RightSkill = savePacket.Player.RightSkill.Shallow.SkillID
playerState.Stats = savePacket.Player.Stats
playerState.Act = savePacket.Player.Act
playerState.Difficulty = savePacket.Difficulty
err = g.heroStateFactory.Save(playerState)
if err != nil {
g.Errorf("GameServer: error saving saving Player: %s", err)
}
case d2netpackettype.PlayerConnectionRequest:
break // prevent log message. these are handled by handleConnection
case d2netpackettype.PlayerDisconnectionNotification:
g.sendPacketToClients(packet)
g.OnClientDisconnected(client)
default:
g.Warningf("GameServer: received unknown packet %s", packet.PacketType)
}
return nil
}