1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-09 09:20:44 +00:00
OpenDiablo2/d2networking/d2client/d2remoteclient/remote_client_connection.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

234 lines
6.5 KiB
Go

package d2remoteclient
import (
"encoding/json"
"fmt"
"io"
"net"
"strings"
"github.com/google/uuid"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2networking"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype"
)
const logPrefix = "Remote Client"
// RemoteClientConnection is the implementation of ClientConnection
// for a remote client.
type RemoteClientConnection struct {
asset *d2asset.AssetManager
heroState *d2hero.HeroStateFactory
clientListener d2networking.ClientListener // The GameClient
uniqueID string // Unique ID generated on construction
tcpConnection *net.TCPConn // UDP connection to the server
active bool // The connection is currently open
*d2util.Logger
}
// Create constructs a new RemoteClientConnection
// and returns a pointer to it.
func Create(l d2util.LogLevel, asset *d2asset.AssetManager) (*RemoteClientConnection, error) {
heroStateFactory, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
result := &RemoteClientConnection{
asset: asset,
heroState: heroStateFactory,
uniqueID: uuid.New().String(),
}
result.Logger = d2util.NewLogger()
result.Logger.SetPrefix(logPrefix)
result.Logger.SetLevel(l)
return result, nil
}
// Open runs serverListener() in a goroutine to continuously read UDP packets.
// It also sends a PlayerConnectionRequestPacket packet to the server (see d2netpacket).
func (r *RemoteClientConnection) Open(connectionString, saveFilePath string) error {
if !strings.Contains(connectionString, ":") {
connectionString += ":6669"
}
tcpAddress, err := net.ResolveTCPAddr("tcp", connectionString)
if err != nil {
return err
}
r.tcpConnection, err = net.DialTCP("tcp", nil, tcpAddress)
if err != nil {
return err
}
r.active = true
go r.serverListener()
r.Infof("Connected to server at %s", r.tcpConnection.RemoteAddr().String())
gameState := r.heroState.LoadHeroState(saveFilePath)
packet, err := d2netpacket.CreatePlayerConnectionRequestPacket(r.GetUniqueID(), gameState)
if err != nil {
r.Errorf("PlayerConnectionRequestPacket: %v", err)
}
err = r.SendPacketToServer(packet)
if err != nil {
r.Errorf("RemoteClientConnection: error sending PlayerConnectionRequestPacket to server.")
return err
}
return nil
}
// Close informs the server that this client has disconnected and sets
// RemoteClientConnection.active to false.
func (r *RemoteClientConnection) Close() error {
r.active = false
pd, err := d2netpacket.CreatePlayerDisconnectRequestPacket(r.GetUniqueID())
if err != nil {
return fmt.Errorf("PlayerDisconnectRequestPacket: %v", err)
}
err = r.SendPacketToServer(pd)
if err != nil {
return err
}
return nil
}
// GetUniqueID returns RemoteClientConnection.uniqueID.
func (r RemoteClientConnection) GetUniqueID() string {
return r.uniqueID
}
// GetConnectionType returns an enum representing the connection type.
// See: d2clientconnectiontype
func (r RemoteClientConnection) GetConnectionType() d2clientconnectiontype.ClientConnectionType {
return d2clientconnectiontype.LANClient
}
// SetClientListener sets RemoteClientConnection.clientListener to the given value.
func (r *RemoteClientConnection) SetClientListener(listener d2networking.ClientListener) {
r.clientListener = listener
}
// SendPacketToServer compresses the JSON encoding of a NetPacket and
// sends it to the server.
func (r *RemoteClientConnection) SendPacketToServer(packet d2netpacket.NetPacket) error {
encoder := json.NewEncoder(r.tcpConnection)
err := encoder.Encode(packet)
if err != nil {
return err
}
return nil
}
// serverListener runs a while loop, reading from the GameServer's TCP
// connection.
func (r *RemoteClientConnection) serverListener() {
decoder := json.NewDecoder(r.tcpConnection)
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:
r.Errorf("failed to decode the packet, err: %v\n", err)
}
return // allow the connection to close
}
p, err := r.decodeToPacket(packet.PacketType, string(packet.PacketData))
if err != nil {
r.Errorf("%v %v", packet.PacketType, err)
}
err = r.clientListener.OnPacketReceived(p)
if err != nil {
r.Errorf("%v %v", packet.PacketType, err)
}
}
}
// bytesToJSON reads the packet type, decompresses the packet and returns a JSON string.
// nolint:unused // WIP
func (r *RemoteClientConnection) bytesToJSON(buffer []byte) (string, d2netpackettype.NetPacketType, error) {
packet, err := d2netpacket.UnmarshalNetPacket(buffer)
if err != nil {
return "", 0, err
}
return string(packet.PacketData), packet.PacketType, nil
}
// decodeToPacket unmarshals the JSON string into the correct struct
// and returns a NetPacket declaring that struct.
// nolint:gocyclo,funlen // switch statement on packet type makes sense, no need to change
func (r *RemoteClientConnection) decodeToPacket(
t d2netpackettype.NetPacketType,
data string) (d2netpacket.NetPacket, error) {
var (
np = d2netpacket.NetPacket{}
err error
p interface{}
)
switch t {
case d2netpackettype.GenerateMap:
p, err = d2netpacket.UnmarshalGenerateMap([]byte(data))
case d2netpackettype.MovePlayer:
p, err = d2netpacket.UnmarshalMovePlayer([]byte(data))
case d2netpackettype.UpdateServerInfo:
p, err = d2netpacket.UnmarshalUpdateServerInfo([]byte(data))
case d2netpackettype.AddPlayer:
p, err = d2netpacket.UnmarshalAddPlayer([]byte(data))
case d2netpackettype.CastSkill:
p, err = d2netpacket.UnmarshalCast([]byte(data))
case d2netpackettype.Ping:
p, err = d2netpacket.UnmarshalPing([]byte(data))
case d2netpackettype.PlayerDisconnectionNotification:
p, err = d2netpacket.UnmarshalPlayerDisconnectionRequest([]byte(data))
case d2netpackettype.ServerClosed:
p, err = d2netpacket.UnmarshalServerClosed([]byte(data))
default:
err = fmt.Errorf("RemoteClientConnection: unrecognized packet type: %v", t)
}
if err != nil {
return np, err
}
mp, marshalErr := d2netpacket.MarshalPacket(p)
if marshalErr != nil {
r.Errorf("MarshalPacket: %v", marshalErr)
}
np = d2netpacket.NetPacket{PacketType: t, PacketData: mp}
return np, nil
}