1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-11-17 18:06:03 -05:00
OpenDiablo2/d2networking/d2client/game_client.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

467 lines
13 KiB
Go

package d2client
import (
"fmt"
"os"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2localclient"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2remoteclient"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype"
"github.com/OpenDiablo2/OpenDiablo2/d2script"
)
const logPrefix = "Game Client"
const (
numSubtilesPerTile = 5
)
// GameClient manages a connection to d2server.GameServer
// and keeps a synchronized copy of the map and entities.
type GameClient struct {
clientConnection ServerConnection // Abstract local/remote connection
connectionType d2clientconnectiontype.ClientConnectionType // Type of connection (local or remote)
asset *d2asset.AssetManager
scriptEngine *d2script.ScriptEngine
GameState *d2hero.HeroState // local player state
MapEngine *d2mapengine.MapEngine // Map and entities
mapGen *d2mapgen.MapGenerator // map generator
PlayerID string // ID of the local player
Players map[string]*d2mapentity.Player // IDs of the other players
Seed int64 // Map seed
RegenMap bool // Regenerate tile cache on render (map has changed)
*d2util.Logger
}
// Create constructs a new GameClient and returns a pointer to it.
func Create(connectionType d2clientconnectiontype.ClientConnectionType,
asset *d2asset.AssetManager,
l d2util.LogLevel,
scriptEngine *d2script.ScriptEngine) (*GameClient, error) {
result := &GameClient{
asset: asset,
MapEngine: d2mapengine.CreateMapEngine(l, asset),
Players: make(map[string]*d2mapentity.Player),
connectionType: connectionType,
scriptEngine: scriptEngine,
}
result.Logger = d2util.NewLogger()
result.Logger.SetPrefix(logPrefix)
result.Logger.SetLevel(l)
// for a remote client connection, set loading to true - wait until we process the GenerateMapPacket
// before we start updating map entites
result.MapEngine.IsLoading = connectionType == d2clientconnectiontype.LANClient
mapGen, err := d2mapgen.NewMapGenerator(asset, l, result.MapEngine)
if err != nil {
return nil, err
}
result.mapGen = mapGen
switch connectionType {
case d2clientconnectiontype.LANClient:
result.clientConnection, err = d2remoteclient.Create(l, asset)
case d2clientconnectiontype.LANServer:
result.clientConnection, err = d2localclient.Create(asset, l, true)
case d2clientconnectiontype.Local:
result.clientConnection, err = d2localclient.Create(asset, l, false)
default:
err = fmt.Errorf("unknown client connection type specified: %d", connectionType)
}
if err != nil {
return nil, err
}
result.clientConnection.SetClientListener(result)
return result, nil
}
// Open creates the server and connects to it if the client is local.
// If the client is remote it sends a PlayerConnectionRequestPacket to the
// server (see d2netpacket).
func (g *GameClient) Open(connectionString, saveFilePath string) error {
switch g.connectionType {
case d2clientconnectiontype.LANServer, d2clientconnectiontype.Local:
g.scriptEngine.AllowEval()
}
return g.clientConnection.Open(connectionString, saveFilePath)
}
// Close destroys the server if the client is local. For remote clients
// it sends a DisconnectRequestPacket (see d2netpacket).
func (g *GameClient) Close() error {
switch g.connectionType {
case d2clientconnectiontype.LANServer, d2clientconnectiontype.Local:
g.scriptEngine.DisallowEval()
}
return g.clientConnection.Close()
}
// Destroy does the same thing as Close.
func (g *GameClient) Destroy() error {
return g.Close()
}
// OnPacketReceived is called by the ClientConection and processes incoming
// packets.
// nolint:gocyclo // switch statement on packet type makes sense, no need to change
func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error {
switch packet.PacketType {
case d2netpackettype.GenerateMap:
if err := g.handleGenerateMapPacket(packet); err != nil {
return err
}
case d2netpackettype.UpdateServerInfo:
if err := g.handleUpdateServerInfoPacket(packet); err != nil {
return err
}
case d2netpackettype.AddPlayer:
if err := g.handleAddPlayerPacket(packet); err != nil {
return err
}
case d2netpackettype.MovePlayer:
if err := g.handleMovePlayerPacket(packet); err != nil {
return err
}
case d2netpackettype.CastSkill:
if err := g.handleCastSkillPacket(packet); err != nil {
return err
}
case d2netpackettype.SpawnItem:
if err := g.handleSpawnItemPacket(packet); err != nil {
return err
}
case d2netpackettype.Ping:
if err := g.handlePingPacket(); err != nil {
g.Errorf("GameClient: error responding to server ping: %s", err)
}
case d2netpackettype.PlayerDisconnectionNotification:
if err := g.handlePlayerDisconnectionPacket(packet); err != nil {
return err
}
case d2netpackettype.ServerClosed:
// https://github.com/OpenDiablo2/OpenDiablo2/issues/802
g.Infof("Server has been closed")
os.Exit(0)
case d2netpackettype.ServerFull:
g.Infof("Server is full") // need to be verified
os.Exit(0)
default:
g.Fatalf("Invalid packet type: %d", packet.PacketType)
}
return nil
}
// SendPacketToServer calls server.OnPacketReceived if the client is local.
// If it is remote the NetPacket sent over a UDP connection to the server.
func (g *GameClient) SendPacketToServer(packet d2netpacket.NetPacket) error {
return g.clientConnection.SendPacketToServer(packet)
}
func (g *GameClient) handleGenerateMapPacket(packet d2netpacket.NetPacket) error {
mapData, err := d2netpacket.UnmarshalGenerateMap(packet.PacketData)
if err != nil {
return err
}
if mapData.RegionType == d2enum.RegionAct1Town {
g.mapGen.GenerateAct1Overworld()
}
g.RegenMap = true
return nil
}
func (g *GameClient) handleUpdateServerInfoPacket(packet d2netpacket.NetPacket) error {
serverInfo, err := d2netpacket.UnmarshalUpdateServerInfo(packet.PacketData)
if err != nil {
return err
}
g.MapEngine.SetSeed(serverInfo.Seed)
g.PlayerID = serverInfo.PlayerID
g.Seed = serverInfo.Seed
g.Infof("Player id set to %s", serverInfo.PlayerID)
return nil
}
func (g *GameClient) handleAddPlayerPacket(packet d2netpacket.NetPacket) error {
player, err := d2netpacket.UnmarshalAddPlayer(packet.PacketData)
if err != nil {
return err
}
d2hero.HydrateSkills(player.Skills, g.asset)
newPlayer := g.MapEngine.NewPlayer(player.ID, player.Name, player.X, player.Y, 0,
player.HeroType, player.Stats, player.Skills, &player.Equipment, player.LeftSkill, player.RightSkill, player.Gold)
g.Players[newPlayer.ID()] = newPlayer
g.MapEngine.AddEntity(newPlayer)
return nil
}
func (g *GameClient) handleSpawnItemPacket(packet d2netpacket.NetPacket) error {
item, err := d2netpacket.UnmarshalSpawnItem(packet.PacketData)
if err != nil {
return err
}
itemEntity, err := g.MapEngine.NewItem(item.X, item.Y, item.Codes...)
if err == nil {
g.MapEngine.AddEntity(itemEntity)
}
return err
}
func (g *GameClient) handleMovePlayerPacket(packet d2netpacket.NetPacket) error {
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)
path := g.MapEngine.PathFind(start, dest)
if len(path) > 0 {
player.SetPath(path, func() {
tilePosition := player.Position.Tile()
tile := g.MapEngine.TileAt(int(tilePosition.X()), int(tilePosition.Y()))
if tile == nil {
return
}
player.SetIsInTown(tile.RegionType == d2enum.RegionAct1Town)
err := player.SetAnimationMode(player.GetAnimationMode())
if err != nil {
fmtStr := "GameClient: error setting animation mode for player %s: %s"
g.Errorf(fmtStr, player.ID(), err)
}
})
}
return nil
}
func (g *GameClient) handleCastSkillPacket(packet d2netpacket.NetPacket) error {
playerCast, err := d2netpacket.UnmarshalCast(packet.PacketData)
if err != nil {
return err
}
player := g.Players[playerCast.SourceEntityID]
player.StopMoving()
castX := playerCast.TargetX * numSubtilesPerTile
castY := playerCast.TargetY * numSubtilesPerTile
direction := player.Position.DirectionTo(*d2vector.NewVector(castX, castY))
player.SetDirection(direction)
skillRecord := g.asset.Records.Skill.Details[playerCast.SkillID]
missileEntities, err := g.createMissileEntities(skillRecord, player, castX, castY)
if err != nil {
return err
}
var summonedNpcEntity *d2mapentity.NPC
if skillRecord.Summon != "" {
summonedNpcEntity, err = g.createSummonedNpcEntity(skillRecord, int(castX), int(castY))
if err != nil {
return err
}
}
player.StartCasting(skillRecord.Anim, func() {
if len(missileEntities) > 0 {
// shoot the missiles of the skill after the player has finished casting
for _, missileEntity := range missileEntities {
g.MapEngine.AddEntity(missileEntity)
}
}
if summonedNpcEntity != nil {
// summon the referenced NPC after the player has finished casting
g.MapEngine.AddEntity(summonedNpcEntity)
}
})
overlayRecord := g.asset.Records.Layout.Overlays[skillRecord.Castoverlay]
return g.playCastOverlay(overlayRecord, int(player.Position.X()), int(player.Position.Y()))
}
func (g *GameClient) createSummonedNpcEntity(skillRecord *d2records.SkillRecord, x, y int) (*d2mapentity.NPC, error) {
monsterStatsRecord := g.asset.Records.Monster.Stats[skillRecord.Summon]
if monsterStatsRecord == nil {
fmtErr := "cannot cast skill - No monstat entry for \"%s\""
return nil, fmt.Errorf(fmtErr, skillRecord.Summon)
}
// https://github.com/OpenDiablo2/OpenDiablo2/issues/803
summonedNpcEntity, err := g.MapEngine.NewNPC(x, y, monsterStatsRecord, 0)
if err != nil {
return nil, err
}
return summonedNpcEntity, nil
}
func (g *GameClient) createMissileEntities(
skillRecord *d2records.SkillRecord,
player *d2mapentity.Player,
castX, castY float64,
) ([]*d2mapentity.Missile, error) {
missileRecords := []*d2records.MissileRecord{
g.asset.Records.GetMissileByName(skillRecord.Cltmissile),
g.asset.Records.GetMissileByName(skillRecord.Cltmissilea),
g.asset.Records.GetMissileByName(skillRecord.Cltmissileb),
g.asset.Records.GetMissileByName(skillRecord.Cltmissilec),
g.asset.Records.GetMissileByName(skillRecord.Cltmissiled),
}
missileEntities := make([]*d2mapentity.Missile, 0)
for _, missileRecord := range missileRecords {
if missileRecord == nil {
continue
}
missileEntity, err := g.createMissileEntity(missileRecord, player, castX, castY)
if err != nil {
return nil, err
}
missileEntities = append(missileEntities, missileEntity)
}
return missileEntities, nil
}
func (g *GameClient) createMissileEntity(
missileRecord *d2records.MissileRecord,
player *d2mapentity.Player,
castX, castY float64,
) (*d2mapentity.Missile, error) {
if missileRecord == nil {
return nil, nil
}
radians := d2math.GetRadiansBetween(
player.Position.X(),
player.Position.Y(),
castX,
castY,
)
missileEntity, err := g.MapEngine.NewMissile(
int(player.Position.X()),
int(player.Position.Y()),
g.asset.Records.Missiles[missileRecord.Id],
)
if err != nil {
return nil, err
}
missileEntity.SetRadians(radians, func() {
g.MapEngine.RemoveEntity(missileEntity)
})
return missileEntity, nil
}
func (g *GameClient) playCastOverlay(overlayRecord *d2records.OverlayRecord, x, y int) error {
if overlayRecord == nil {
return nil
}
overlayEntity, err := g.MapEngine.NewCastOverlay(
x,
y,
overlayRecord,
)
if err != nil {
return err
}
overlayEntity.SetOnDoneFunc(func() {
g.MapEngine.RemoveEntity(overlayEntity)
})
g.MapEngine.AddEntity(overlayEntity)
return nil
}
func (g *GameClient) handlePingPacket() error {
pongPacket, err := d2netpacket.CreatePongPacket(g.PlayerID)
if err != nil {
return err
}
err = g.clientConnection.SendPacketToServer(pongPacket)
if err != nil {
return err
}
return nil
}
func (g *GameClient) handlePlayerDisconnectionPacket(packet d2netpacket.NetPacket) error {
disconnectPacket, err := d2netpacket.UnmarshalPlayerDisconnectionRequest(packet.PacketData)
if err != nil {
return err
}
player := g.Players[disconnectPacket.ID]
g.MapEngine.RemoveEntity(player)
delete(g.Players, disconnectPacket.ID)
return nil
}
// IsSinglePlayer returns a bool for whether the game is a single-player game
func (g *GameClient) IsSinglePlayer() bool {
return g.connectionType == d2clientconnectiontype.Local
}