From 88326b527829f9fd2d74e4bc0abfcbc11ba09a57 Mon Sep 17 00:00:00 2001 From: presiyan-ivanov <15377841+presiyan-ivanov@users.noreply.github.com> Date: Sun, 11 Oct 2020 01:47:51 +0300 Subject: [PATCH] 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 --- d2common/d2enum/player_animation_mode.go | 1 + d2core/d2hero/hero_skill.go | 9 +- d2core/d2hero/hero_skill_util.go | 15 ++++ d2core/d2map/d2mapengine/engine.go | 10 +++ d2core/d2map/d2mapentity/cast_overlay.go | 57 +++++++++++++ d2core/d2map/d2mapentity/factory.go | 37 +++++++++ d2core/d2map/d2mapentity/player.go | 13 ++- d2core/d2map/d2maprenderer/renderer.go | 6 ++ d2core/d2records/skill_details_loader.go | 47 ++++++++++- d2core/d2records/skill_details_record.go | 7 +- d2game/d2gamescreen/game.go | 1 + d2game/d2player/game_controls.go | 45 +++++----- .../remote_client_connection.go | 7 ++ d2networking/d2client/game_client.go | 82 ++++++++++++++----- d2networking/d2server/game_server.go | 13 +++ 15 files changed, 298 insertions(+), 52 deletions(-) create mode 100644 d2core/d2hero/hero_skill_util.go create mode 100644 d2core/d2map/d2mapentity/cast_overlay.go diff --git a/d2common/d2enum/player_animation_mode.go b/d2common/d2enum/player_animation_mode.go index 64c817a8..13a993a4 100644 --- a/d2common/d2enum/player_animation_mode.go +++ b/d2common/d2enum/player_animation_mode.go @@ -27,4 +27,5 @@ const ( PlayerAnimationModeDead // DD PlayerAnimationModeSequence // GH PlayerAnimationModeKnockBack // GH + PlayerAnimationModeNone // "" - aura skills, e.g. Paladin's Concentration Aura ) diff --git a/d2core/d2hero/hero_skill.go b/d2core/d2hero/hero_skill.go index f7c46305..fd54655c 100644 --- a/d2core/d2hero/hero_skill.go +++ b/d2core/d2hero/hero_skill.go @@ -23,13 +23,8 @@ type shallowHeroSkill struct { // MarshalJSON overrides the default logic used when the HeroSkill is serialized to a byte array. func (hs *HeroSkill) MarshalJSON() ([]byte, error) { - // only serialize the ID instead of the whole SkillRecord object. - shallow := shallowHeroSkill{ - SkillID: hs.SkillRecord.ID, - SkillPoints: hs.SkillPoints, - } - - bytes, err := json.Marshal(shallow) + // only serialize the shallow object instead of the SkillRecord & SkillDescriptionRecord + bytes, err := json.Marshal(hs.shallow) if err != nil { log.Fatalln(err) } diff --git a/d2core/d2hero/hero_skill_util.go b/d2core/d2hero/hero_skill_util.go new file mode 100644 index 00000000..478b39a9 --- /dev/null +++ b/d2core/d2hero/hero_skill_util.go @@ -0,0 +1,15 @@ +package d2hero + +import "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + +// HydrateSkills will load the SkillRecord & SkillDescriptionRecord from the asset manager, using the skill ID. +// This is done to avoid serializing the whole record data of HeroSkill to a game save or network packets. +// We cant do this while unmarshalling because there is no reference to the asset manager. +func HydrateSkills(skills map[int]*HeroSkill, asset *d2asset.AssetManager) { + for skillID := range skills { + heroSkill := skills[skillID] + heroSkill.SkillRecord = asset.Records.Skill.Details[skillID] + heroSkill.SkillDescriptionRecord = asset.Records.Skill.Descriptions[heroSkill.SkillRecord.Skilldesc] + heroSkill.SkillPoints = skills[skillID].SkillPoints + } +} diff --git a/d2core/d2map/d2mapengine/engine.go b/d2core/d2map/d2mapengine/engine.go index e69e4352..7f8606fe 100644 --- a/d2core/d2map/d2mapengine/engine.go +++ b/d2core/d2map/d2mapengine/engine.go @@ -31,6 +31,8 @@ type MapEngine struct { startSubTileX int // Starting X position startSubTileY int // Starting Y position dt1Files []string // List of DS1 strings + // TODO: remove this flag and show loading screen until the initial server packets are handled and the map is generated (only for remote client) + IsLoading bool // (temp) Whether we have processed the GenerateMapPacket(only for remote client) } // CreateMapEngine creates a new instance of the map engine and returns a pointer to it. @@ -42,6 +44,8 @@ func CreateMapEngine(asset *d2asset.AssetManager) *MapEngine { asset: asset, MapEntityFactory: entity, StampFactory: stamp, + // This will be set to true when we are using a remote client connection, and then set to false after we process the GenerateMapPacket + IsLoading: false, } return engine @@ -285,6 +289,12 @@ func (m *MapEngine) GetCenterPosition() (x, y float64) { // Advance calls the Advance() method for all entities, // processing a single tick. func (m *MapEngine) Advance(tickTime float64) { + // TODO:(temp hack) prevents concurrent map read & write exceptions that occur when we join a TCP game as a remote client + // due to the engine updating entities before handling the GenerateMapPacket + if m.IsLoading { + return + } + for ID := range m.entities { m.entities[ID].Advance(tickTime) } diff --git a/d2core/d2map/d2mapentity/cast_overlay.go b/d2core/d2map/d2mapentity/cast_overlay.go new file mode 100644 index 00000000..ce74820a --- /dev/null +++ b/d2core/d2map/d2mapentity/cast_overlay.go @@ -0,0 +1,57 @@ +package d2mapentity + +import ( + "math" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2records" +) + +// CastOverlay is an animated entity representing a projectile that is a result of a skill cast. +type CastOverlay struct { + *AnimatedEntity + record *d2records.OverlayRecord + playLoop bool + onDoneFunc func() +} + +// ID returns the overlay uuid +func (co *CastOverlay) ID() string { + return co.AnimatedEntity.uuid +} + +// GetPosition returns the position of the overlay +func (co *CastOverlay) GetPosition() d2vector.Position { + return co.AnimatedEntity.Position +} + +// GetVelocity returns the velocity vector of the overlay +func (co *CastOverlay) GetVelocity() d2vector.Vector { + return co.AnimatedEntity.velocity +} + +// SetRadians adjusts the entity target based on it's range, rotating it's +// current destination by the value of angle in radians. +func (co *CastOverlay) SetRadians(angle float64, done func()) { + rads := float64(co.record.Height2) // TODO: + + x := co.Position.X() + (rads * math.Cos(angle)) + y := co.Position.Y() + (rads * math.Sin(angle)) + + co.setTarget(d2vector.NewPosition(x, y), done) +} + +// SetOnDoneFunc changes the handler func that gets called when the overlay finishes playing. +func (co *CastOverlay) SetOnDoneFunc(onDoneFunc func()) { + co.onDoneFunc = onDoneFunc +} + +// Advance is called once per frame and processes a single game tick. +func (co *CastOverlay) Advance(tickTime float64) { + co.Step(tickTime) + co.AnimatedEntity.Advance(tickTime) + + if !co.playLoop && co.AnimatedEntity.animation.GetPlayedCount() >= 1 { + co.onDoneFunc() + } +} diff --git a/d2core/d2map/d2mapentity/factory.go b/d2core/d2map/d2mapentity/factory.go index 4139d04c..ae67e383 100644 --- a/d2core/d2map/d2mapentity/factory.go +++ b/d2core/d2map/d2mapentity/factory.go @@ -218,6 +218,43 @@ func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatsRecord, d return result, nil } +// NewCastOverlay creates a cast overlay map entity +func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.OverlayRecord) (*CastOverlay, error) { + animation, err := f.asset.LoadAnimationWithEffect( + fmt.Sprintf("/data/Global/Overlays/%s.dcc", overlayRecord.Filename), + d2resource.PaletteUnits, + d2enum.DrawEffectModulate, + ) + + // TODO: Frame index and played count seem to be shared across the cloned animation objects when we retrieve the animation from the asset manager cache. + animation.Rewind() + animation.ResetPlayedCount() + + if err != nil { + return nil, err + } + + animationSpeed := float64(overlayRecord.AnimRate*25.0) / 1000.0 + playLoop := false // TODO: should be based on the overlay record, some overlays can repeat(e.g. Bone Shield, Frozen Armor) + + animation.SetPlayLength(animationSpeed) + animation.SetPlayLoop(playLoop) + animation.PlayForward() + + targetX := x + overlayRecord.XOffset + targetY := y + overlayRecord.YOffset + + entity := NewAnimatedEntity(targetX, targetY, animation) + + result := &CastOverlay{ + AnimatedEntity: entity, + record: overlayRecord, + playLoop: playLoop, + } + + return result, nil +} + // NewObject creates an instance of AnimatedComposite func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailsRecord, palettePath string) (*Object, error) { diff --git a/d2core/d2map/d2mapentity/player.go b/d2core/d2map/d2mapentity/player.go index 254c1dae..8f8c66cd 100644 --- a/d2core/d2map/d2mapentity/player.go +++ b/d2core/d2map/d2mapentity/player.go @@ -185,12 +185,19 @@ func (p *Player) IsCasting() bool { // StartCasting sets a flag indicating the player is casting a skill and // sets the animation mode to the casting animation. -func (p *Player) StartCasting(onFinishedCasting func()) { +// This handles all types of skills - melee, ranged, kick, summon, etc. +func (p *Player) StartCasting(animMode d2enum.PlayerAnimationMode, onFinishedCasting func()) { + // passive skills, auras, etc. + if animMode == d2enum.PlayerAnimationModeNone { + return + } + p.isCasting = true p.onFinishedCasting = onFinishedCasting - if err := p.SetAnimationMode(d2enum.PlayerAnimationModeCast); err != nil { + + if err := p.SetAnimationMode(animMode); err != nil { fmtStr := "failed to set animationMode of player: %s to: %d, err: %v\n" - fmt.Printf(fmtStr, p.ID(), d2enum.PlayerAnimationModeCast, err) + fmt.Printf(fmtStr, p.ID(), animMode, err) } } diff --git a/d2core/d2map/d2maprenderer/renderer.go b/d2core/d2map/d2maprenderer/renderer.go index b613be05..11c7b9e1 100644 --- a/d2core/d2map/d2maprenderer/renderer.go +++ b/d2core/d2map/d2maprenderer/renderer.go @@ -124,6 +124,12 @@ func (mr *MapRenderer) SetMapEngine(mapEngine *d2mapengine.MapEngine) { // // Pass 4: Roof tiles. func (mr *MapRenderer) Render(target d2interface.Surface) { + // TODO:(temp hack) should not render before the map has been fully generated - + // Prevents concurrent map read & write exceptions that otherwise occur when we join a TCP game + // as a remote client, due to rendering before we have handled the GenerateMapPacket. + if mr.mapEngine.IsLoading { + return + } mapSize := mr.mapEngine.Size() stxf, styf := mr.viewport.ScreenToWorld(screenMiddleX, -200) diff --git a/d2core/d2records/skill_details_loader.go b/d2core/d2records/skill_details_loader.go index 2259c607..f320e663 100644 --- a/d2core/d2records/skill_details_loader.go +++ b/d2core/d2records/skill_details_loader.go @@ -4,6 +4,7 @@ import ( "log" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation/d2parser" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt" ) @@ -140,7 +141,7 @@ func skillDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error { Itypeb3: d.String("itypeb3"), Etypeb1: d.String("etypeb1"), Etypeb2: d.String("etypeb2"), - Anim: d.String("anim"), + Anim: animToEnum(d.String("anim")), Seqtrans: d.String("seqtrans"), Monanim: d.String("monanim"), Seqnum: d.Number("seqnum"), @@ -275,3 +276,47 @@ func skillDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error { return nil } + + +func animToEnum(anim string) d2enum.PlayerAnimationMode { + switch anim { + case "SC": + return d2enum.PlayerAnimationModeCast + + case "TH": + return d2enum.PlayerAnimationModeThrow + + case "KK": + return d2enum.PlayerAnimationModeKick + + case "SQ": + return d2enum.PlayerAnimationModeSequence + + case "S1": + return d2enum.PlayerAnimationModeSkill1 + + case "S2": + return d2enum.PlayerAnimationModeSkill1 + + case "S3": + return d2enum.PlayerAnimationModeSkill3 + + case "S4": + return d2enum.PlayerAnimationModeSkill4 + + case "A1": + return d2enum.PlayerAnimationModeAttack1 + + case "A2": + return d2enum.PlayerAnimationModeAttack2 + + case "": + return d2enum.PlayerAnimationModeNone + + default: + log.Fatalf("Unknown skill anim value [%s]", anim) + } + + // should not be reached + return d2enum.PlayerAnimationModeNone +} diff --git a/d2core/d2records/skill_details_record.go b/d2core/d2records/skill_details_record.go index 46d05ef8..7cadca33 100644 --- a/d2core/d2records/skill_details_record.go +++ b/d2core/d2records/skill_details_record.go @@ -1,6 +1,9 @@ package d2records -import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation" +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" +) // [https://d2mods.info/forum/viewtopic.php?t=41556, https://d2mods.info/forum/kb/viewarticle?a=246] @@ -101,7 +104,7 @@ type SkillRecord struct { Itypeb3 string Etypeb1 string Etypeb2 string - Anim string + Anim d2enum.PlayerAnimationMode Seqtrans string Monanim string ItemCastSound string diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index 2bf7b0d0..6d08f9ba 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -194,6 +194,7 @@ func (v *Game) Render(screen d2interface.Surface) error { if v.gameClient.RegenMap { v.gameClient.RegenMap = false v.mapRenderer.RegenerateTileCache() + v.gameClient.MapEngine.IsLoading = false } if err := screen.Clear(color.Black); err != nil { diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index b7dad9f9..52fce180 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -68,8 +68,8 @@ type GameControls struct { hpManaStatusSprite *d2ui.Sprite mainPanel *d2ui.Sprite menuButton *d2ui.Sprite - leftSkill *SkillResource - rightSkill *SkillResource + leftSkillResource *SkillResource + rightSkillResource *SkillResource zoneChangeText *d2ui.Label nameLabel *d2ui.Label hpManaStatsLabel *d2ui.Label @@ -90,9 +90,12 @@ type ActionableRegion struct { Rect d2geom.Rectangle } +// SkillResource represents a Skill with its corresponding icon sprite, path to DC6 file and icon number. +// SkillResourcePath points to a DC6 resource which contains the icons of multiple skills as frames. +// The IconNumber is the frame at which we can find our skill sprite in the DC6 file. type SkillResource struct { - SkillResourcePath string - IconNumber int + SkillResourcePath string // path to a skills DC6 file(see getSkillResourceByClass) + IconNumber int // the index of the frame in the DC6 file SkillIcon *d2ui.Sprite } @@ -230,7 +233,8 @@ func NewGameControls( skillRecord := gc.asset.Records.Skill.Details[id] skill, err := heroState.CreateHeroSkill(0, skillRecord.Skill) if err != nil { - term.OutputErrorf("cannot create skill with ID of %d", id) + term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err) + return } gc.hero.LeftSkill = skill @@ -240,7 +244,8 @@ func NewGameControls( skillRecord := gc.asset.Records.Skill.Details[id] skill, err := heroState.CreateHeroSkill(0, skillRecord.Skill) if err != nil { - term.OutputErrorf("cannot create skill with ID of %d", id) + term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err) + return } gc.hero.RightSkill = skill @@ -503,8 +508,8 @@ func (g *GameControls) Load() { attackIconID := 2 - g.leftSkill = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills} - g.rightSkill = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills} + g.leftSkillResource = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills} + g.rightSkillResource = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills} g.loadUIButtons() @@ -688,19 +693,19 @@ func (g *GameControls) Render(target d2interface.Surface) error { // Left skill skillResourcePath := g.getSkillResourceByClass(g.hero.LeftSkill.Charclass) - if skillResourcePath != g.leftSkill.SkillResourcePath { - g.leftSkill.SkillIcon, _ = g.ui.NewSprite(skillResourcePath, d2resource.PaletteSky) + if skillResourcePath != g.leftSkillResource.SkillResourcePath { + g.leftSkillResource.SkillIcon, _ = g.ui.NewSprite(skillResourcePath, d2resource.PaletteSky) } - if err := g.leftSkill.SkillIcon.SetCurrentFrame(g.hero.LeftSkill.IconCel); err != nil { + if err := g.leftSkillResource.SkillIcon.SetCurrentFrame(g.hero.LeftSkill.IconCel); err != nil { return err } - w, _ = g.leftSkill.SkillIcon.GetCurrentFrameSize() + w, _ = g.leftSkillResource.SkillIcon.GetCurrentFrameSize() - g.leftSkill.SkillIcon.SetPosition(offset, height) + g.leftSkillResource.SkillIcon.SetPosition(offset, height) - if err := g.leftSkill.SkillIcon.Render(target); err != nil { + if err := g.leftSkillResource.SkillIcon.Render(target); err != nil { return err } @@ -807,19 +812,19 @@ func (g *GameControls) Render(target d2interface.Surface) error { // Right skill skillResourcePath = g.getSkillResourceByClass(g.hero.RightSkill.Charclass) - if skillResourcePath != g.rightSkill.SkillResourcePath { - g.rightSkill.SkillIcon, _ = g.ui.NewSprite(skillResourcePath, d2resource.PaletteSky) + if skillResourcePath != g.rightSkillResource.SkillResourcePath { + g.rightSkillResource.SkillIcon, _ = g.ui.NewSprite(skillResourcePath, d2resource.PaletteSky) } - if err := g.rightSkill.SkillIcon.SetCurrentFrame(g.hero.RightSkill.IconCel); err != nil { + if err := g.rightSkillResource.SkillIcon.SetCurrentFrame(g.hero.RightSkill.IconCel); err != nil { return err } - w, _ = g.rightSkill.SkillIcon.GetCurrentFrameSize() + w, _ = g.rightSkillResource.SkillIcon.GetCurrentFrameSize() - g.rightSkill.SkillIcon.SetPosition(offset, height) + g.rightSkillResource.SkillIcon.SetPosition(offset, height) - if err := g.rightSkill.SkillIcon.Render(target); err != nil { + if err := g.rightSkillResource.SkillIcon.Render(target); err != nil { return err } diff --git a/d2networking/d2client/d2remoteclient/remote_client_connection.go b/d2networking/d2client/d2remoteclient/remote_client_connection.go index 116147fc..06adc0ae 100644 --- a/d2networking/d2client/d2remoteclient/remote_client_connection.go +++ b/d2networking/d2client/d2remoteclient/remote_client_connection.go @@ -198,6 +198,13 @@ func (r *RemoteClientConnection) decodeToPacket(t d2netpackettype.NetPacketType, if err = json.Unmarshal([]byte(data), &p); err != nil { break } + np = d2netpacket.NetPacket{PacketType: t, PacketData: d2netpacket.MarshalPacket(p)} + + case d2netpackettype.CastSkill: + var p d2netpacket.CastPacket + if err = json.Unmarshal([]byte(data), &p); err != nil { + break + } np = d2netpacket.NetPacket{PacketType: t, PacketData: d2netpacket.MarshalPacket(p)} diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index 1015d54d..044c9c87 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -17,6 +17,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" "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" @@ -56,6 +57,10 @@ func Create(connectionType d2clientconnectiontype.ClientConnectionType, scriptEngine: scriptEngine, } + // 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, result.MapEngine) if err != nil { return nil, err @@ -198,6 +203,8 @@ func (g *GameClient) handleAddPlayerPacket(packet d2netpacket.NetPacket) error { 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) @@ -268,42 +275,79 @@ func (g *GameClient) handleCastSkillPacket(packet d2netpacket.NetPacket) error { castX := playerCast.TargetX * numSubtilesPerTile castY := playerCast.TargetY * numSubtilesPerTile - rads := d2math.GetRadiansBetween( + direction := player.Position.DirectionTo(*d2vector.NewVector(castX, castY)) + player.SetDirection(direction) + + skillRecord := g.asset.Records.Skill.Details[playerCast.SkillID] + missileEntity, err := g.createMissileEntity(skillRecord, player, castX, castY) + if err != nil { + return err + } + + player.StartCasting(skillRecord.Anim, func() { + if missileEntity != nil { + // shoot the missile after the player has finished casting + g.MapEngine.AddEntity(missileEntity) + } + }) + + overlayRecord := g.asset.Records.Layout.Overlays[skillRecord.Castoverlay] + g.playCastOverlay(overlayRecord, int(player.Position.X()), int(player.Position.Y())) + + return nil +} + +func (g *GameClient) createMissileEntity(skillRecord *d2records.SkillRecord, player *d2mapentity.Player, castX float64, castY float64) (*d2mapentity.Missile, error) { + missileRecord := g.asset.Records.GetMissileByName(skillRecord.Cltmissile) + if missileRecord == nil { + return nil, nil + } + + var missileEntity *d2mapentity.Missile + + radians := d2math.GetRadiansBetween( player.Position.X(), player.Position.Y(), castX, castY, ) - direction := player.Position.DirectionTo(*d2vector.NewVector(castX, castY)) - player.SetDirection(direction) - skill := g.asset.Records.Skill.Details[playerCast.SkillID] - missileRecord := g.asset.Records.GetMissileByName(skill.Cltmissile) + missileEntity, err := g.MapEngine.NewMissile( + int(player.Position.X()), + int(player.Position.Y()), + g.asset.Records.Missiles[missileRecord.Id], + ) - if missileRecord == nil { - //TODO: handle casts that have no missiles(or have multiple missiles and require additional logic) - log.Println("Missile not found for skill ID", skill.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 int, y int) error { + if overlayRecord == nil { return nil } - missile, err := g.MapEngine.NewMissile( - int(player.Position.X()), - int(player.Position.Y()), - missileRecord, + overlayEntity, err := g.MapEngine.NewCastOverlay( + x, + y, + overlayRecord, ) - if err != nil { return err } - missile.SetRadians(rads, func() { - g.MapEngine.RemoveEntity(missile) + overlayEntity.SetOnDoneFunc(func() { + g.MapEngine.RemoveEntity(overlayEntity) }) - player.StartCasting(func() { - // shoot the missile after the player finished casting - g.MapEngine.AddEntity(missile) - }) + g.MapEngine.AddEntity(overlayEntity) return nil } diff --git a/d2networking/d2server/game_server.go b/d2networking/d2server/game_server.go index df35da30..8cdd25ce 100644 --- a/d2networking/d2server/game_server.go +++ b/d2networking/d2server/game_server.go @@ -14,6 +14,7 @@ import ( "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" @@ -181,6 +182,14 @@ func (g *GameServer) packetManager() { } 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 { @@ -345,6 +354,8 @@ func handleClientConnection(gameServer *GameServer, client ClientConnection, x, playerX := int(x*subtilesPerTile) + middleOfTileOffset playerY := int(y*subtilesPerTile) + middleOfTileOffset + d2hero.HydrateSkills(playerState.Skills, gameServer.asset) + createPlayerPacket := d2netpacket.CreateAddPlayerPacket( client.GetUniqueID(), playerState.HeroName, @@ -430,6 +441,8 @@ func OnPacketReceived(client ClientConnection, packet d2netpacket.NetPacket) err 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