diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index a26ebde75d..610f3e108d 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -2991,6 +2991,36 @@ UnlimboDetonate.KeepSelected=false ; boolean ## Weapons +### Allow Laser drawing position update + +- Now you can define via `LaserPositionUpdate` whether the endpoints of a laser drawing are updated during its duration. + - `None`: No update. + - `Firer`: The start point follows the firer's FLH; if the firer dies, the update stops. + - Since the FLH of `DiskLaser` actually determines the center of the ring, in this scenario, during the update process, the direction of the line connecting the beam's starting point to the center is fixed relative to the ground and the distance is constant - that is, the starting point will still remain on the ring. + - `Target`: The end point follows the target; if the target object dies, the update stops. + - `All`: Equivalent to specifying both `Firer` and `Target`. +- `LaserPositionUpdate.StopOnFirerConvert` determines whether the laser source stops updating when the firer transforms. If set to false (default), the laser will continue to update using the transformed unit's corresponding parameters. + +```{note} +For a sub-weapon created by `ShrapnelWeapon` or `AirburstWeapon`, its start point is the position where the parent weapon detonates, not the firer's FLH. +- If `Firer` is set, it will be treated as `None`. +- If `All` is set, it will be treated as `Target`. +``` + +In `rulesmd.ini`: +```ini +[AudioVisual] +LaserPositionUpdate.StopOnFirerConvert=false ; boolean + +[SOMEWEAPON] ; WeaponType with IsLaser=yes or DiskLaser=yes +LaserPositionUpdate=none ; Position Follow Enumeration (none|firer|target|all) +LaserPositionUpdate.StopOnFirerConvert=false ; boolean, default to [AudioVisual] -> LaserPositionUpdate.StopOnFirerConvert +``` + +```{warning} +If the weapon sets this logic to a non-`None` value while also using other logics that change the drawing position, such as `FlakScatter` and `VisualScatter`, then after initially drawing the laser according to those other logics, the drawing position will be forced to change due to the update rules. +``` + ### AreaFire target customization - You can now specify how AreaFire weapon picks its target. By default it targets the base cell the firer is currently on, but this can now be changed to fire on the firer itself or at a random cell within the radius of the weapon's `Range` by setting `AreaFire.Target` to `self` or `random` respectively. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 7e91c9da99..2b93d57028 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -579,6 +579,7 @@ HideShakeEffects=false ; boolean - [Allow customizing guard mission retry delay for buildings with weapons](Fixed-or-Improved-Logics.md#armed-building-guard-retry-delay) (by Starkku) - [Allow `Temporal` warhead to apply ratio and bonus](Fixed-or-Improved-Logics.md#allow-temporal-warhead-to-apply-ratio-and-bonus) (by NetsuNegi) - Allow enabling a looser movement state check for the `DiscardOn=move` condition of AE to support more usage scenarios (by Noble_Fish) +- [Allow Laser drawing position update](New-or-Enhanced-Logics.md#allow-laser-drawing-position-update) (by Noble_Fish) #### Vanilla fixes: - Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya) diff --git a/src/Ext/Bullet/Body.cpp b/src/Ext/Bullet/Body.cpp index 73aa3a2077..0773999025 100644 --- a/src/Ext/Bullet/Body.cpp +++ b/src/Ext/Bullet/Body.cpp @@ -7,6 +7,11 @@ #include #include +namespace LaserRT +{ + void SetLaserTrackingData(LaserDrawClass* pLaser, TechnoClass* pShooter, AbstractClass* pTarget, int weaponIdx, PositionFollow mode, bool ignoreShooter); +} + BulletExt::ExtContainer BulletExt::ExtMap; void BulletExt::ExtData::InterceptBullet(TechnoClass* pSource, BulletClass* pInterceptor) @@ -197,6 +202,9 @@ inline void BulletExt::SimulatedFiringReport(BulletClass* pBullet) inline void BulletExt::SimulatedFiringLaser(BulletClass* pBullet, HouseClass* pHouse) { // Can not use 0x6FD210 because the firer may die + if (!pBullet || !pBullet->WeaponType) + return; + const auto pWeapon = pBullet->WeaponType; if (!pWeapon->IsLaser) @@ -204,10 +212,12 @@ inline void BulletExt::SimulatedFiringLaser(BulletClass* pBullet, HouseClass* pH const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); + LaserDrawClass* pLaser = nullptr; + if (pWeapon->IsHouseColor || pWeaponExt->Laser_IsSingleColor) { const auto black = ColorStruct { 0, 0, 0 }; - const auto pLaser = GameCreate(pBullet->SourceCoords, BulletExt::GetTargetCoordsForFiring(pBullet), + pLaser = GameCreate(pBullet->SourceCoords, BulletExt::GetTargetCoordsForFiring(pBullet), ((pWeapon->IsHouseColor && pHouse) ? pHouse->LaserColor : pWeapon->LaserInnerColor), black, black, pWeapon->LaserDuration); pLaser->IsHouseColor = true; @@ -216,13 +226,35 @@ inline void BulletExt::SimulatedFiringLaser(BulletClass* pBullet, HouseClass* pH } else { - const auto pLaser = GameCreate(pBullet->SourceCoords, BulletExt::GetTargetCoordsForFiring(pBullet), + pLaser = GameCreate(pBullet->SourceCoords, BulletExt::GetTargetCoordsForFiring(pBullet), pWeapon->LaserInnerColor, pWeapon->LaserOuterColor, pWeapon->LaserOuterSpread, pWeapon->LaserDuration); pLaser->IsHouseColor = false; pLaser->Thickness = 3; pLaser->IsSupported = false; } + + // LaserPositionUpdate + if (pLaser && pWeaponExt) + { + auto mode = pWeaponExt->LaserPositionUpdate; + const auto pBulletExt = BulletExt::ExtMap.Find(pBullet); + const bool isSplit = (pBulletExt && pBulletExt->IsSplitFromAirburst); + + if (isSplit) + { + if (mode == PositionFollow::Firer) + mode = PositionFollow::None; + else if (mode == PositionFollow::All) + mode = PositionFollow::Target; + } + + if (mode != PositionFollow::None) + { + auto const pTarget = abstract_cast(pBullet->Target); + LaserRT::SetLaserTrackingData(pLaser, pBullet->Owner, pTarget, 0, mode, isSplit); + } + } } // Make sure pBullet and pBullet->WeaponType is not empty before call diff --git a/src/Ext/Bullet/Body.h b/src/Ext/Bullet/Body.h index 7b458e51b2..f54f75530f 100644 --- a/src/Ext/Bullet/Body.h +++ b/src/Ext/Bullet/Body.h @@ -27,6 +27,7 @@ class BulletExt int DamageNumberOffset; int ParabombFallRate; bool IsInstantDetonation; + bool IsSplitFromAirburst = false; TrajectoryPointer Trajectory; diff --git a/src/Ext/Bullet/Hooks.DetonateLogics.cpp b/src/Ext/Bullet/Hooks.DetonateLogics.cpp index 6e4d6b5c6a..9c63e6d077 100644 --- a/src/Ext/Bullet/Hooks.DetonateLogics.cpp +++ b/src/Ext/Bullet/Hooks.DetonateLogics.cpp @@ -833,6 +833,7 @@ DEFINE_HOOK(0x469EC0, BulletClass_Logics_AirburstWeapon, 0x6) coords = MapClass::GetRandomCoordsNear(coords, distance, false); } + BulletExt::ExtMap.Find(pBullet)->IsSplitFromAirburst = true; BulletExt::SimulatedFiringUnlimbo(pBullet, pOwner, pWeapon, coords, true); BulletExt::SimulatedFiringEffects(pBullet, pOwner, nullptr, useFiringEffects, true); } diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 14acfd38bf..c696cf31bf 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -197,6 +197,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->AirstrikeLineColor.Read(exINI, GameStrings::AudioVisual, "AirstrikeLineColor"); this->AirstrikeLineZAdjust.Read(exINI, GameStrings::AudioVisual, "AirstrikeLineZAdjust"); + this->LaserPositionUpdate_StopOnFirerConvert.Read(exINI, GameStrings::AudioVisual, "LaserPositionUpdate.StopOnFirerConvert"); this->LaserZAdjust.Read(exINI, GameStrings::AudioVisual, "LaserZAdjust"); this->EBoltZAdjust.Read(exINI, GameStrings::AudioVisual, "EBoltZAdjust"); this->EBoltZAdjust_ClampInitialDepthForBuilding.Read(exINI, GameStrings::AudioVisual, "EBoltZAdjust.ClampInitialDepthForBuilding"); @@ -580,6 +581,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->ColorAddUse8BitRGB) .Process(this->AirstrikeLineColor) .Process(this->AirstrikeLineZAdjust) + .Process(this->LaserPositionUpdate_StopOnFirerConvert) .Process(this->LaserZAdjust) .Process(this->EBoltZAdjust) .Process(this->EBoltZAdjust_ClampInitialDepthForBuilding) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index b9de53affd..8cb90d83fb 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -144,6 +144,7 @@ class RulesExt Valueable AirstrikeLineColor; Valueable AirstrikeLineZAdjust; + Valueable LaserPositionUpdate_StopOnFirerConvert; Valueable LaserZAdjust; Valueable EBoltZAdjust; Valueable EBoltZAdjust_ClampInitialDepthForBuilding; @@ -461,6 +462,7 @@ class RulesExt , ColorAddUse8BitRGB { false } , AirstrikeLineColor { { 255, 0, 0 } } , AirstrikeLineZAdjust { 0 } + , LaserPositionUpdate_StopOnFirerConvert { false } , LaserZAdjust { 0 } , EBoltZAdjust { 0 } , EBoltZAdjust_ClampInitialDepthForBuilding { true } diff --git a/src/Ext/WeaponType/Body.cpp b/src/Ext/WeaponType/Body.cpp index f98678cfdc..379cc8f588 100644 --- a/src/Ext/WeaponType/Body.cpp +++ b/src/Ext/WeaponType/Body.cpp @@ -117,6 +117,8 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->AreaFire_Target.Read(exINI, pSection, "AreaFire.Target"); this->FeedbackWeapon.Read(exINI, pSection, "FeedbackWeapon"); this->Laser_IsSingleColor.Read(exINI, pSection, "IsSingleColor"); + this->LaserPositionUpdate.Read(exINI, pSection, "LaserPositionUpdate"); + this->LaserPositionUpdate_StopOnFirerConvert.Read(exINI, pSection, "LaserPositionUpdate.StopOnFirerConvert"); this->LaserZAdjust.Read(exINI, pSection, "LaserZAdjust"); this->EBoltZAdjust.Read(exINI, pSection, "EBoltZAdjust"); this->EBoltZAdjust_ClampInitialDepthForBuilding.Read(exINI, pSection, "EBoltZAdjust.ClampInitialDepthForBuilding"); @@ -216,6 +218,8 @@ void WeaponTypeExt::ExtData::Serialize(T& Stm) .Process(this->AreaFire_Target) .Process(this->FeedbackWeapon) .Process(this->Laser_IsSingleColor) + .Process(this->LaserPositionUpdate) + .Process(this->LaserPositionUpdate_StopOnFirerConvert) .Process(this->LaserZAdjust) .Process(this->EBoltZAdjust) .Process(this->EBoltZAdjust_ClampInitialDepthForBuilding) diff --git a/src/Ext/WeaponType/Body.h b/src/Ext/WeaponType/Body.h index 5bb8734a4b..85b239c08d 100644 --- a/src/Ext/WeaponType/Body.h +++ b/src/Ext/WeaponType/Body.h @@ -45,6 +45,8 @@ class WeaponTypeExt Valueable AreaFire_Target; Valueable FeedbackWeapon; Valueable Laser_IsSingleColor; + Valueable LaserPositionUpdate; + Nullable LaserPositionUpdate_StopOnFirerConvert; Nullable LaserZAdjust; Nullable EBoltZAdjust; Nullable EBoltZAdjust_ClampInitialDepthForBuilding; @@ -134,6 +136,8 @@ class WeaponTypeExt , AreaFire_Target { AreaFireTarget::Base } , FeedbackWeapon {} , Laser_IsSingleColor { false } + , LaserPositionUpdate { PositionFollow::None } + , LaserPositionUpdate_StopOnFirerConvert {} , LaserZAdjust {} , EBoltZAdjust {} , EBoltZAdjust_ClampInitialDepthForBuilding {} @@ -231,4 +235,7 @@ class WeaponTypeExt static int GetRangeWithModifiers(WeaponTypeClass* pThis, TechnoClass* pFirer); static int GetRangeWithModifiers(WeaponTypeClass* pThis, TechnoClass* pFirer, int range); static int GetTechnoKeepRange(WeaponTypeClass* pThis, TechnoClass* pFirer, bool isMinimum); + + // Misc/Hooks.LaserDraw.cpp + static void LaserTrackingPointerExpired(void* ptr, bool removed); }; diff --git a/src/Misc/Hooks.LaserDraw.cpp b/src/Misc/Hooks.LaserDraw.cpp index 221502db44..beec9f244e 100644 --- a/src/Misc/Hooks.LaserDraw.cpp +++ b/src/Misc/Hooks.LaserDraw.cpp @@ -1,6 +1,8 @@ #include +#include #include #include +#include namespace LaserDrawTemp { @@ -53,3 +55,257 @@ DEFINE_HOOK(0x6FD3FD, TechnoClass_LaserZap_ZAdjust, 0x5) return 0; } + +#pragma region LaserPositionUpdate + +namespace LaserRT +{ + struct TrackingData + { + TechnoClass* Shooter { nullptr }; + ObjectClass* Target { nullptr }; + int WeaponIndex { 0 }; + PositionFollow FollowMode { PositionFollow::None }; + CoordStruct SavedOffset { CoordStruct::Empty }; + CoordStruct LocalFLH { CoordStruct::Empty }; + int FrozenBurstIndex { 0 }; + bool StopOnFirerConvert { false }; + const TechnoTypeClass* OriginalType { nullptr }; + + void Initialize(TechnoClass* pShooter, AbstractClass* pTarget, int weaponIdx, PositionFollow mode, const CoordStruct& initialSource, const CoordStruct& localFLH, int burstIndex, bool stopOnFirerConvert) + { + const auto pShooterBuilding = abstract_cast(pShooter); + + if (pShooterBuilding && pShooterBuilding->Type->MaxNumberOccupants > 0) + mode &= ~PositionFollow::Firer; + + if (pShooter && (mode & PositionFollow::Firer)) + { + this->Shooter = pShooter; + this->LocalFLH = localFLH; + this->FrozenBurstIndex = burstIndex; + this->StopOnFirerConvert = stopOnFirerConvert; + + if (stopOnFirerConvert) + this->OriginalType = pShooter->GetTechnoType(); + + const int savedBurstIndex = pShooter->CurrentBurstIndex; + pShooter->CurrentBurstIndex = burstIndex; + const CoordStruct worldFLH = pShooter->GetFLH(weaponIdx, localFLH); + pShooter->CurrentBurstIndex = savedBurstIndex; + + this->SavedOffset = initialSource - worldFLH; + } + + if (mode & PositionFollow::Target) + this->Target = abstract_cast(pTarget); + + this->WeaponIndex = weaponIdx; + this->FollowMode = mode; + } + + void PointerExpired(void* ptr, bool removed) + { + if (!removed) + return; + + AnnounceInvalidPointer(this->Shooter, ptr); + AnnounceInvalidPointer(this->Target, ptr); + } + }; + + std::unordered_map TrackingMap; + + void SetLaserTrackingData(LaserDrawClass* pLaser, TechnoClass* pShooter, AbstractClass* pTarget, int weaponIdx, PositionFollow mode, bool ignoreShooter) + { + CoordStruct localFLH; + int burstIndex = 0; + bool stopOnFirerConvert = false; + + if (pShooter) + { + bool flhFound = false; + localFLH = TechnoExt::GetBurstFLH(pShooter, weaponIdx, flhFound); + + if (!flhFound) + localFLH = pShooter->GetWeapon(weaponIdx)->FLH; + + burstIndex = pShooter->CurrentBurstIndex; + + if (const auto pWeapon = pShooter->GetWeapon(weaponIdx)->WeaponType) + { + const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); + stopOnFirerConvert = pWeaponExt->LaserPositionUpdate_StopOnFirerConvert.Get(RulesExt::Global()->LaserPositionUpdate_StopOnFirerConvert); + } + } + + TrackingData data; + data.Initialize(ignoreShooter ? nullptr : pShooter, pTarget, weaponIdx, mode, pLaser->Source, localFLH, burstIndex, stopOnFirerConvert); + TrackingMap[pLaser] = data; + } + + TechnoClass* Shooter = nullptr; + AbstractClass* Target = nullptr; + int WeaponIndex = 0; + bool IgnoreShooter = false; + CoordStruct SavedLocalFLH = CoordStruct::Empty; + int SavedBurstIndex = 0; +} + +// container hooks + +// IsLaser ctor/dtor/remove hooks +DEFINE_HOOK(0x54FE60, LaserDrawClass_CTOR_Update, 0x5) +{ + GET(LaserDrawClass*, pLaser, ECX); + LaserRT::TrackingMap[pLaser] = LaserRT::TrackingData {}; + return 0; +} + +DEFINE_HOOK_AGAIN(0x5501D7, LaserDrawClass_DTOR_Tracking, 0x5) +DEFINE_HOOK_AGAIN(0x5500EF, LaserDrawClass_DTOR_Tracking, 0x5) +DEFINE_HOOK_AGAIN(0x550016, LaserDrawClass_DTOR_Tracking, 0x6) +DEFINE_HOOK(0x54FFB0, LaserDrawClass_DTOR_Tracking, 0x7) // LaserDrawClass::DTOR +{ + GET(LaserDrawClass*, pLaser, ECX); + LaserRT::TrackingMap.erase(pLaser); + return 0; +} + +void WeaponTypeExt::LaserTrackingPointerExpired(void* ptr, bool removed) +{ + for (auto& [_, data] : LaserRT::TrackingMap) + data.PointerExpired(ptr, removed); +} + +// hooks + +DEFINE_HOOK(0x6FD210, TechnoClass_LaserZap_SetTrackingContext, 0x7) +{ + GET(TechnoClass*, pShooter, ECX); + GET_STACK(ObjectClass*, pTarget, 0x4); + GET_STACK(int, weaponIdx, 0x8); + + LaserRT::Shooter = LaserRT::IgnoreShooter ? nullptr : pShooter; + LaserRT::Target = pTarget; + LaserRT::WeaponIndex = weaponIdx; + + LaserRT::SavedBurstIndex = pShooter->CurrentBurstIndex; + bool flhFound = false; + LaserRT::SavedLocalFLH = TechnoExt::GetBurstFLH(pShooter, weaponIdx, flhFound); + + if (!flhFound) + { + LaserRT::SavedLocalFLH = pShooter->GetWeapon(weaponIdx)->FLH; + + if (LaserRT::SavedBurstIndex % 2 != 0) + LaserRT::SavedLocalFLH.Y = -LaserRT::SavedLocalFLH.Y; + } + return 0; +} + +DEFINE_HOOK(0x6FD446, TechnoClass_LaserZap_Tracking, 0x7) +{ + GET(WeaponTypeClass*, pWeapon, ECX); + const auto pShooter = std::exchange(LaserRT::Shooter, nullptr); + const auto pTarget = std::exchange(LaserRT::Target, nullptr); + const int weaponIdx = std::exchange(LaserRT::WeaponIndex, 0); + const CoordStruct localFLH = std::exchange(LaserRT::SavedLocalFLH, CoordStruct::Empty); + const int burstIndex = std::exchange(LaserRT::SavedBurstIndex, 0); + + GET(LaserDrawClass*, pLaser, EAX); + const auto it = LaserRT::TrackingMap.find(pLaser); + + if (it == LaserRT::TrackingMap.cend()) + return 0; + + const auto mode = WeaponTypeExt::ExtMap.Find(pWeapon)->LaserPositionUpdate.Get(); + + if (mode == PositionFollow::None) + return 0; + + bool stopOnFirerConvert = WeaponTypeExt::ExtMap.Find(pWeapon)->LaserPositionUpdate_StopOnFirerConvert.Get(RulesExt::Global()->LaserPositionUpdate_StopOnFirerConvert); + + it->second.Initialize(pShooter, pTarget, weaponIdx, mode, pLaser->Source, localFLH, burstIndex, stopOnFirerConvert); + return 0; +} + +static LaserDrawClass* __fastcall Shrapnel_CreateLaser_Wrapper(TechnoClass* pShooter, void*, ObjectClass* pTarget + , int weaponIdx, WeaponTypeClass* pWeapon, const CoordStruct& sourceCoords) +{ + LaserRT::IgnoreShooter = true; + const auto pLaser = pShooter->CreateLaser(pTarget, weaponIdx, pWeapon, sourceCoords); + LaserRT::IgnoreShooter = false; + return pLaser; +} +DEFINE_FUNCTION_JUMP(CALL, 0x46A8AC, Shrapnel_CreateLaser_Wrapper) +DEFINE_FUNCTION_JUMP(CALL, 0x46AD81, Shrapnel_CreateLaser_Wrapper) + +// DiskLaser main beam activation +DEFINE_HOOK(0x4A7696, DiskLaser_Update_ActivateMainBeam_Tracking, 0x6) +{ + GET(LaserDrawClass*, pLaser, EAX); + + if (!pLaser) + return 0; + + const auto it = LaserRT::TrackingMap.find(pLaser); + + if (it == LaserRT::TrackingMap.cend()) + return 0; + + GET(DiskLaserClass*, pDiskLaser, ESI); + const auto pWeapon = pDiskLaser->Weapon; + + if (!pWeapon) + return 0; + + const auto mode = WeaponTypeExt::ExtMap.Find(pWeapon)->LaserPositionUpdate.Get(); + + if (mode == PositionFollow::None) + return 0; + + if (pLaser->Source == pLaser->Target) + return 0; + + LaserRT::SetLaserTrackingData(pLaser, pDiskLaser->Owner, pDiskLaser->Target, 0, mode, false); + return 0; +} + +// Per‑frame coordinate update +DEFINE_HOOK(0x550173, LaserDrawClass_Update_Tracking, 0x6) +{ + GET(LaserDrawClass*, pLaser, ESI); + const auto it = LaserRT::TrackingMap.find(pLaser); + + if (it == LaserRT::TrackingMap.cend()) + return 0; + + auto& data = it->second; + + if (const auto pShooter = data.Shooter) + { + if (data.StopOnFirerConvert && data.OriginalType) + { + if (pShooter->GetTechnoType() != data.OriginalType) + data.Shooter = nullptr; + } + + if (data.Shooter) + { + const int savedBurstIndex = pShooter->CurrentBurstIndex; + pShooter->CurrentBurstIndex = data.FrozenBurstIndex; + const CoordStruct worldFLH = pShooter->GetFLH(data.WeaponIndex, data.LocalFLH); + pShooter->CurrentBurstIndex = savedBurstIndex; + + pLaser->Source = worldFLH + data.SavedOffset; + } + } + + if (const auto pTarget = data.Target) + pLaser->Target = pTarget->GetTargetCoords(); + + return 0; +} + +#pragma endregion diff --git a/src/Phobos.Ext.cpp b/src/Phobos.Ext.cpp index 1c0acf5406..d07812b7e0 100644 --- a/src/Phobos.Ext.cpp +++ b/src/Phobos.Ext.cpp @@ -254,6 +254,7 @@ DEFINE_HOOK(0x7258D0, AnnounceInvalidPointer, 0x6) GET(bool const, removed, EDX); PhobosTypeRegistry::InvalidatePointer(pInvalid, removed); + WeaponTypeExt::LaserTrackingPointerExpired(pInvalid, removed); return 0; } diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h index 2109fdf943..97356e2989 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -181,6 +181,16 @@ enum class SlaveChangeOwnerType Neutral = 4, }; +enum class PositionFollow : BYTE +{ + None = 0x0, + Firer = 0x1, + Target = 0x2, + All = Firer | Target +}; + +MAKE_ENUM_FLAGS(PositionFollow) + enum class AutoDeathBehavior { Kill = 0, // default death option diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index ec0fd1e1a7..53017df144 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -904,6 +904,33 @@ namespace detail return false; } + template <> + inline bool read(PositionFollow& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + static const std::pair Names[] = + { + {"none", PositionFollow::None}, + {"firer", PositionFollow::Firer}, + {"target", PositionFollow::Target}, + {"all", PositionFollow::All}, + }; + + for (auto const& [name, val] : Names) + { + if (_strcmpi(parser.value(), name) == 0) + { + value = val; + return true; + } + } + + Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected a position follow mode (None, Firer, Target, All)"); + } + return false; + } + template <> inline bool read(SelfHealGainType& value, INI_EX& parser, const char* pSection, const char* pKey) {