1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-15 20:15:24 +00:00
OpenDiablo2/d2networking/d2server/game_server.go
presiyan-ivanov 88326b5278
Initial cast overlay implementation. Fix HeroSkill deserialization & map entities processing crashing for remote client. (#766)
* Casting a skill now plays the corresponding overlay(if any).

* Prevent a crash caused by nil pointer in HeroSkill deserialization, happening when
unmarshalling HeroSkill from packets as a remote client.

* Add PlayerAnimationModeNone to handle some of the Skills(e.g.
Paladin auras) having "" as animation mode.

* Joining a game as remote client now waits for map generation to finish
before rendering map or processing map entities. This is temporary hack to prevent the game from
crashing due to concurrent map read & write exception.

* Send CastSkill packet to other clients.

Co-authored-by: Presiyan Ivanov <presiyan-ivanov@users.noreply.github.com>
2020-10-10 18:47:51 -04:00

450 lines
14 KiB
Go

package d2server
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net"
"sync"
"time"
"github.com/robertkrimen/otto"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"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/d2netpacket"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2server/d2tcpclientconnection"
"github.com/OpenDiablo2/OpenDiablo2/d2script"
)
const (
port = "6669"
chunkSize int = 4096
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 []byte
}
//nolint:gochecknoglobals // currently singleton by design
var singletonServer *GameServer
// 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,
maxConnections ...int) (*GameServer,
error) {
if len(maxConnections) == 0 {
maxConnections = []int{8}
}
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 []byte),
mapEngines: make([]*d2mapengine.MapEngine, 0),
scriptEngine: d2script.CreateScriptEngine(),
seed: time.Now().UnixNano(),
}
// 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(asset)
mapEngine.SetSeed(gameServer.seed)
mapEngine.ResetMap(d2enum.RegionAct1Town, 100, 100) // TODO: Mapgen - Needs levels.txt stuff
mapGen, err := d2mapgen.NewMapGenerator(asset, 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(singletonServer.mapEngines)
if err != nil {
fmt.Print(err.Error())
}
return val
})
// 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)
return
}
go g.handleConnection(c)
}
}()
return nil
}
// Stop stops the game server
func (g *GameServer) Stop() {
g.Lock()
g.cancel()
if err := g.listener.Close(); err != nil {
log.Printf("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:
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.CastSkill:
castSkill, err := d2netpacket.UnmarshalNetPacket(p)
if err != nil {
log.Println(err)
continue
}
g.sendPacketToClients(castSkill)
case d2netpackettype.SpawnItem:
item, err := d2netpacket.UnmarshalNetPacket(p)
if err != nil {
log.Println(err)
continue
}
g.sendPacketToClients(item)
case d2netpackettype.ServerClosed:
g.Stop()
}
}
}
}
func (g *GameServer) sendPacketToClients(packet d2netpacket.NetPacket) {
for _, c := range g.connections {
if err := c.SendPacketToClient(packet); err != nil {
log.Printf("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
var packet d2netpacket.NetPacket
log.Printf("Accepting connection: %s\n", conn.RemoteAddr().String())
defer func() {
if err := conn.Close(); err != nil {
log.Printf("failed to close the connection: %s\n", conn.RemoteAddr())
}
}()
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()
handleClientConnection(g, client, sx, sy)
return 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 OnClientConnected(client ClientConnection) {
// Temporary position hack --------------------------------------------
sx, sy := singletonServer.mapEngines[0].GetStartPosition() // TODO: Another temporary hack
clientPlayerState := client.GetPlayerState()
clientPlayerState.X = sx
clientPlayerState.Y = sy
// --------------------------------------------------------------------
log.Printf("Client connected with an id of %s", client.GetUniqueID())
singletonServer.connections[client.GetUniqueID()] = client
handleClientConnection(singletonServer, client, sx, sy)
}
func handleClientConnection(gameServer *GameServer, client ClientConnection, x, y float64) {
err := client.SendPacketToClient(d2netpacket.CreateUpdateServerInfoPacket(gameServer.seed, client.GetUniqueID()))
if err != nil {
log.Printf("GameServer: error sending UpdateServerInfoPacket to client %s: %s", client.GetUniqueID(), err)
}
err = client.SendPacketToClient(d2netpacket.CreateGenerateMapPacket(d2enum.RegionAct1Town))
if err != nil {
log.Printf("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, gameServer.asset)
createPlayerPacket := d2netpacket.CreateAddPlayerPacket(
client.GetUniqueID(),
playerState.HeroName,
playerX,
playerY,
playerState.HeroType,
playerState.Stats,
playerState.Skills,
playerState.Equipment,
)
for _, connection := range gameServer.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
}
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.Skills,
conPlayerState.Equipment,
),
)
if err != nil {
log.Printf("GameServer: error sending CreateAddPlayerPacket to client %s: %s", connection.GetUniqueID(), err)
}
}
}
// OnClientDisconnected removes the given client from the list
// of client connections.
func OnClientDisconnected(client ClientConnection) {
log.Printf("Client disconnected with an id of %s", 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, err := d2netpacket.UnmarshalMovePlayer(packet.PacketData)
if err != nil {
return err
}
// 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.connections[client.GetUniqueID()].GetPlayerState()
playerState.X = movePacket.DestX
playerState.Y = movePacket.DestY
// ----------------------------------------------------------------
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.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.connections {
err := player.SendPacketToClient(packet)
if err != nil {
log.Printf("GameServer: error sending %T to client %s: %s", packet, player.GetUniqueID(), err)
}
}
default:
log.Printf("GameServer: received unknown packet %T", packet)
}
return nil
}