diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 0f1d78bbc2..bfb7bf54b7 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -3016,6 +3016,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 c7f490d54d..becbf849ba 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -583,6 +583,7 @@ HideShakeEffects=false ; boolean - Allow `RemoveMindControl` warhead to mute `MindClearedSound` (by Noble_Fish) - Introduce weight selection rules for ExtraWarheads (by Noble_Fish) - [Allow infantry to perform type conversion when deploying and undeploying](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying) (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 c24060f19a..fbdfe5a12a 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) @@ -204,10 +209,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 +223,34 @@ 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) + { + auto mode = pWeaponExt->LaserPositionUpdate; + const bool isSplit = BulletExt::ExtMap.Find(pBullet)->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 0f5ca9b39a..ac74be51dc 100644 --- a/src/Ext/Bullet/Body.h +++ b/src/Ext/Bullet/Body.h @@ -28,6 +28,7 @@ class BulletExt int ParabombFallRate; bool IsInstantDetonation; double FirepowerMult; + bool IsSplitFromAirburst = false; TrajectoryPointer Trajectory; diff --git a/src/Ext/Bullet/Hooks.DetonateLogics.cpp b/src/Ext/Bullet/Hooks.DetonateLogics.cpp index a06bf03a02..c217e3d31f 100644 --- a/src/Ext/Bullet/Hooks.DetonateLogics.cpp +++ b/src/Ext/Bullet/Hooks.DetonateLogics.cpp @@ -879,6 +879,7 @@ DEFINE_HOOK(0x469EC0, BulletClass_Logics_AirburstWeapon, 0x6) } BulletExt::ExtMap.Find(pBullet)->FirepowerMult = pBulletExt->FirepowerMult; + 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 9f9385eb10..88863d6534 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"); @@ -587,6 +588,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 fd873b2cb6..35593cb7a5 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; @@ -463,6 +464,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 feef97a273..b0350c11df 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"); @@ -240,6 +242,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 5135863882..170ff387f8 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; @@ -136,6 +138,8 @@ class WeaponTypeExt , AreaFire_Target { AreaFireTarget::Base } , FeedbackWeapon {} , Laser_IsSingleColor { false } + , LaserPositionUpdate { PositionFollow::None } + , LaserPositionUpdate_StopOnFirerConvert {} , LaserZAdjust {} , EBoltZAdjust {} , EBoltZAdjust_ClampInitialDepthForBuilding {} @@ -235,4 +239,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..9c4038256a 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,266 @@ 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); + } + + static bool InvalidatePointerIgnorable(void* ptr) + { + const auto pAbstract = static_cast(ptr); + return !abstract_cast(pAbstract); + } + }; + + 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) +{ + if (LaserRT::TrackingData::InvalidatePointerIgnorable(ptr)) + return; + + 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; + + const 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) {