1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2025-02-03 15:17:04 -05:00
OpenDiablo2/d2networking/d2client/game_client.go
Ian 9121209f86
Backmerge master into ecs (#1021)
* quest log disabled icon

* removed unused functions for text colorizing

* terminal printing

* fixed saving key change bug

* help overlay text

* removed unused issue comments

* Update Ebiten to v2.0.2

* changed terminal color separator & changed logLevelNone to logLevelDefault in app.go

* status and screens update

* quest animation initial.

* escape menu hotkeys

* hero save file

* add-buttons init

* add-buttons actions

* stats changing: hero stats panel

* skill tre - remaining points label

* revert:hero save file (app.go)

* escape menu hotkeys

* hero save file

* updated d2hero.HeroStatsState

* corrected grammar errors

* animation is played and last frame is completedFrame

* animation stops playing, when quest log is closed & quest socket gets highlighted, when animation is playing & fixed highlight bug

* fixed quest descr bug & added code description

* level-up buttons tooltips

* Replaced kingping with flag package

* Cleanup d2records logging

* Renamed CharStatRecord

* Renamed SoundDetailRecord

* Renamed MonStatRecord

* Renamed ObjectDetailRecord

* Renamed MonStat2Record

* fixed onHover bug in d2ui.Sprite (#992)

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* added static checks to d2ui (#990)

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* fixed onHover bug in d2ui.Label (#991)

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* Revert "fixed onHover bug in d2ui.Sprite"

This reverts commit 8b557062fb.

* tip-labels in tcpip menu

* skill select menu dependencies (when we open skillselect menu, other panels are closed)

* Cleaned up d2term

* A wild } appeared

* Fixed linter issues

* ckecked value of italian modifier

* game-controls refactor

* fixed build and lint errors

* removed unnecessary switch-case statments from onKeyUp and onEscKey

* fixed bug with terminal's logLevel

* Refactor StreamWriter

* Refactor StreamReader

* Fixed gocritic linter issues

* Reduce GetTiles slice allocation

* 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

* Switched to self hosted build agent

* Removed PR requirement for action

* remove build job requirement

* Fiddling with actions names

* Updated workflows

* Fix yaml errors

* Yet another yaml fix

* Removed improper ebiten dependency in d2interface.

* Add checkboxes, checkbox test scene

* Render HUD before Panels (in this Case 'Panels' only does mean Inventory Panel). This is to avoid Entity Labels to be renderd above the Inventory Panel. Fixes #936

* This DCC frame size calculation seems useless

TBH there some to be some other overcomplicated things going on in
DCCAnimation but too tired to use brain right now.

* De-lint ecs branch

* bugfix: file_handle_resolver

* Add boot state to scenes, fix loading screen scene

* added label-button widget (#989)

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* removed links to closed issues from code (#1005)

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>

* removed unused fields from d2player.GameControl.actionableRegions (#997)

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>

* d2ui.Frame refactor (#994)

* d2ui.Frame refactor

* removed unneccessery d2asset.AssetManager argument from d2ui.NewUIFrame

* d2ui.Frame refactor

* removed unneccessery d2asset.AssetManager argument from d2ui.NewUIFrame

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: Tim Sarbin <tim.sarbin@gmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>

* d2mpq refactored (#1020)

* d2mpq refactor

* d2mpq refactor last standing lint error

* d2mpq refactor: less linter noise

* d2mpq refactor: more linter issues

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: Tim Sarbin <tim.sarbin@gmail.com>
Co-authored-by: Hajime Hoshi <hajimehoshi@gmail.com>
Co-authored-by: gucio321 <73652197+gucio321@users.noreply.github.com>
Co-authored-by: Intyre <intyre@gmail.com>
Co-authored-by: ThomasChr <thomaschristlieb@hotmail.com>
Co-authored-by: Ziemas <ziemas@ziemas.se>
Co-authored-by: gravestench <dknuth0101@gmail.com>
2021-01-09 00:25:44 -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
}