mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-09-07 03:54:15 -04:00
* 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>
450 lines
14 KiB
Go
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
|
|
}
|