diff --git a/CREDITS.md b/CREDITS.md index c8555f9fa7..887d92f43a 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -38,6 +38,7 @@ This page lists all the individual contributions to the project by their author. - Recursive transport killer fix - Custom locomotors example implementation and piggybacking test warheads - Jumpjet initial facing fix + - Techno Attachment logic - Migration utility - GitHub Actions setup - Official docs diff --git a/Phobos.vcxproj b/Phobos.vcxproj index da0fa52603..1ea8d1aa62 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -62,7 +62,8 @@ - + + @@ -79,7 +80,9 @@ + + @@ -125,6 +128,7 @@ + @@ -147,6 +151,7 @@ + @@ -206,7 +211,8 @@ - + + @@ -220,8 +226,10 @@ + + diff --git a/YRpp b/YRpp index d107962d13..b08060737e 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit d107962d130d898132c182540a2c218f23548b2f +Subproject commit b08060737e9358f9660d3435888b61066fe62aa2 diff --git a/docs/Contributing.md b/docs/Contributing.md index a994803569..f8ddd6c118 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -146,7 +146,7 @@ ExtData(TerrainTypeClass* OwnerObject) : Extension(OwnerObject - Class fields that can be set via INI tags should be named exactly like ini tags with dots replaced with underscores. - Pointer type declarations always have pointer sign `*` attached to the type declaration. - Non-static class extension methods faked by declaring a static method with `pThis` as a first argument are only to be placed in the extension class for the class instance of which `pThis` is. - - If it's crucial to fake `__thiscall` you may use `__fastcall` and use `void*` or `void* _` as a second argument to discard value passed through `EDX` register. Such methods are to be used for call replacement. + - If it's crucial to fake `__thiscall` you may use `__fastcall` and use `discard_t` or `discard_t _` as a second argument to discard value passed through `EDX` register. Such methods are to be used for call replacement. - Hooks have to be named using a following scheme: `HookedFunction_HookPurpose`, or `ClassName_HookedMethod_HookPurpose`. Defined-again hooks are exempt from this scheme due to impossibility to define different names for the same hook. - Return addresses should use anonymous enums to make it clear what address means what, if applicable. The enum has to be placed right at the function start and include all addresses that are used in this hook: ```cpp diff --git a/docs/Miscellanous.md b/docs/Miscellanous.md index 82806776b0..c3c364c389 100644 --- a/docs/Miscellanous.md +++ b/docs/Miscellanous.md @@ -45,7 +45,7 @@ SkirmishUnlimitedColors=false ; boolean ### Frame Step In - There's a new hotkey to execute the game frame by frame for development usage. - - You can switch to frame by frame mode and then use frame step in command to forward 1, 5, 10, 15, 30 or 60 frames by one hit. + - You can switch to frame by frame mode and then use frame step in command to forward 1, 5, 10, 15, 30 or 60 frames by one hit. ### Save variables to file @@ -64,20 +64,23 @@ SaveVariablesOnScenarioEnd=false ; boolean ### Semantic locomotor aliases - It's now possible to write locomotor aliases instead of their CLSIDs in the `Locomotor` tag value. Use the table below to find the needed alias for a locomotor. + - The feature is also supported for Phobos locomotors. + +| *Alias* | *CLSID* | +| -------: | :--------------------------------------: | +Drive | `{4A582741-9839-11d1-B709-00A024DDAFD1}` | +Jumpjet | `{92612C46-F71F-11d1-AC9F-006008055BB5}` | +Hover | `{4A582742-9839-11d1-B709-00A024DDAFD1}` | +Rocket | `{B7B49766-E576-11d3-9BD9-00104B972FE8}` | +Tunnel | `{4A582743-9839-11d1-B709-00A024DDAFD1}` | +Walk | `{4A582744-9839-11d1-B709-00A024DDAFD1}` | +DropPod | `{4A582745-9839-11d1-B709-00A024DDAFD1}` | +Fly | `{4A582746-9839-11d1-B709-00A024DDAFD1}` | +Teleport | `{4A582747-9839-11d1-B709-00A024DDAFD1}` | +Mech | `{55D141B8-DB94-11d1-AC98-006008055BB5}` | +Ship | `{2BEA74E1-7CCA-11d3-BE14-00104B62A16C}` | +Attachment | `{C5D54B98-8C98-4275-8CE4-EF75CB0CBE3E}` | -| *Alias*| *CLSID* | -| -----: | :--------------------------------------: | -Drive | `{4A582741-9839-11d1-B709-00A024DDAFD1}` | -Jumpjet | `{92612C46-F71F-11d1-AC9F-006008055BB5}` | -Hover | `{4A582742-9839-11d1-B709-00A024DDAFD1}` | -Rocket | `{B7B49766-E576-11d3-9BD9-00104B972FE8}` | -Tunnel | `{4A582743-9839-11d1-B709-00A024DDAFD1}` | -Walk | `{4A582744-9839-11d1-B709-00A024DDAFD1}` | -DropPod | `{4A582745-9839-11d1-B709-00A024DDAFD1}` | -Fly | `{4A582746-9839-11d1-B709-00A024DDAFD1}` | -Teleport | `{4A582747-9839-11d1-B709-00A024DDAFD1}` | -Mech | `{55D141B8-DB94-11d1-AC98-006008055BB5}` | -Ship | `{2BEA74E1-7CCA-11d3-BE14-00104B62A16C}` | ## Game Speed diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index f42cf5bb76..158c9cdf64 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -153,6 +153,59 @@ SuppressReflectDamage=false ; boolean SuppressReflectDamage.Types= ; List of AttachEffectTypes ``` +### Attachments + +![Unit Attachment](your image here) +*Attachments used in [mod name](link)* + +```{warning} +This feature is not final and is under development. +``` + +- Technos now can be attached one to another in a tree like way. The attached units won't process any locomotion code and act like a part of a parent unit in a configurable. + - Currently the attached techno may only be a vehicle. + - When attached, the special `Attachment` (`{C5D54B98-8C98-4275-8CE4-EF75CB0CBE3E}`) locomotor is automatically casted on a unit. You may also specify it in the child unit types manually if the unit is not intended to move without a parent (f. ex. a turret). + +In `rulesmd.ini`: +```ini +[AttachmentTypes] +0=MNT ; (example) + +[MNT] +RespawnAtCreation=true ; boolean +RespawnDelay=-1 ; integer, non-negative values enable the respawn timer +InheritOwner=true ; boolean, whether the child inherits owner of the parent while it's attached +InheritStateEffects=true ; boolean (state effects = chaos, iron curtain etc.) +InheritCommands=true ; boolean +InheritCommands.StopCommand=true ; boolean +InheritCommands.DeployCommand=true ; boolean +LowSelectionPriority=true ; boolean, whether the child is low priority while attached +TransparentToMouse=false ; boolean, can't click on attached techno if set +YSortPosition=default ; Attachment YSort position enumeration - default|underparent|overparent +InheritDestruction=true ; boolean +InheritHeightStatus=true ; boolean, whether the layer and InAir/OnGround/IsSurfaced inherited from parent +OccupiesCell=true ; boolean +DestructionWeapon.Child= ; WeaponType, detonated on child when parent is destroyed +DestructionWeapon.Parent= ; WeaponType, detonated on parent when child is destroyed +ParentDestructionMission= ; MissionType, queued to child when parent is destroyed +ParentDetachmentMission= ; MissionType, queued to child when it's detached from parent + +[SOMETECHNO] ; TechnoTypeClass +; used when this techno is attached +AttachmentTopLayerMinHeight= ; integer +AttachmentUndergroundLayerMaxHeight= ; integer +; used for attaching other technos +AttachmentX.Type=MNT ; AttachmentType (example) +AttachmentX.TechnoType= ; TechnoType that can be attached, currently only units are supported +AttachmentX.FLH=0,0,0 ; integer - Forward, Lateral, Height +AttachmentX.IsOnTurret=false ; boolean +AttachmentX.RotationAdjust=0 ; rotation in DirType, from -255 to 255 + +[General] +AttachmentTopLayerMinHeight=500 ; integer, +AttachmentUndergroundLayerMaxHeight=-256 ; integer +``` + ### Custom Radiation Types ![image](_static/images/radtype-01.png) diff --git a/docs/Whats-New.md b/docs/Whats-New.md index dd8bb1a747..ba49a782ec 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -300,6 +300,15 @@ SaveGameOnScenarioStart=true ; boolean ## Changelog +### 0.4 + +
+ Click to show + +New: +- Techno Attachment logic (by Kerbiter) +
+ ### Version TBD (develop branch nightly builds)
diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 336cae4272..e76d11be3f 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -9,6 +9,7 @@ #include #include #include +#include std::unique_ptr RulesExt::Data = nullptr; @@ -34,6 +35,7 @@ void RulesExt::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) ShieldTypeClass::LoadFromINIList(pINI); LaserTrailTypeClass::LoadFromINIList(&CCINIClass::INI_Art.get()); AttachEffectTypeClass::LoadFromINIList(pINI); + AttachmentTypeClass::LoadFromINIList(pINI); Data->LoadBeforeTypeData(pThis, pINI); } @@ -192,6 +194,9 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->UseFixedVoxelLighting.Read(exINI, GameStrings::AudioVisual, "UseFixedVoxelLighting"); + this->AttachmentTopLayerMinHeight.Read(exINI, GameStrings::General, "AttachmentTopLayerMinHeight"); + this->AttachmentUndergroundLayerMaxHeight.Read(exINI, GameStrings::General, "AttachmentUndergroundLayerMaxHeight"); + // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); for (int i = 0; i < itemsCount; ++i) @@ -363,6 +368,8 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->VoxelLightSource) // .Process(this->VoxelShadowLightSource) .Process(this->UseFixedVoxelLighting) + .Process(this->AttachmentTopLayerMinHeight) + .Process(this->AttachmentUndergroundLayerMaxHeight) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index b6dc6a00ac..56afb8f7a7 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -151,6 +151,9 @@ class RulesExt // Nullable> VoxelShadowLightSource; Valueable UseFixedVoxelLighting; + Valueable AttachmentTopLayerMinHeight; + Valueable AttachmentUndergroundLayerMaxHeight; + ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } , InfantryGainSelfHealCap {} @@ -259,6 +262,8 @@ class RulesExt , VoxelLightSource { } // , VoxelShadowLightSource { } , UseFixedVoxelLighting { false } + , AttachmentTopLayerMinHeight { 500 } + , AttachmentUndergroundLayerMaxHeight { -256 } { } virtual ~ExtData() = default; diff --git a/src/Ext/Techno/Body.Internal.cpp b/src/Ext/Techno/Body.Internal.cpp index 30417cc62a..d04e689c42 100644 --- a/src/Ext/Techno/Body.Internal.cpp +++ b/src/Ext/Techno/Body.Internal.cpp @@ -1,7 +1,5 @@ #include "Body.h" - #include - #include // Unsorted methods @@ -39,23 +37,26 @@ void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller) } } -// reversed from 6F3D60 -CoordStruct TechnoExt::GetFLHAbsoluteCoords(TechnoClass* pThis, CoordStruct pCoord, bool isOnTurret) +Matrix3D TechnoExt::GetTransform(TechnoClass* pThis, VoxelIndexKey* pKey, bool isShadow) { - auto const pType = pThis->GetTechnoType(); - auto const pFoot = abstract_cast(pThis); Matrix3D mtx; - // Step 1: get body transform matrix - if (pFoot && pFoot->Locomotor) - mtx = pFoot->Locomotor->Draw_Matrix(nullptr); + if ((pThis->AbstractFlags & AbstractFlags::Foot) && ((FootClass*)pThis)->Locomotor) + mtx = isShadow ? ((FootClass*)pThis)->Locomotor->Shadow_Matrix(pKey) : ((FootClass*)pThis)->Locomotor->Draw_Matrix(pKey); else // no locomotor means no rotation or transform of any kind (f.ex. buildings) - Kerbiter mtx.MakeIdentity(); - // Steps 2-3: turret offset and rotation + return mtx; +} + +Matrix3D TechnoExt::TransformFLHForTurret(TechnoClass* pThis, Matrix3D mtx, bool isOnTurret, double factor) +{ + auto const pType = pThis->GetTechnoType(); + + // turret offset and rotation if (isOnTurret && pThis->HasTurret()) { - TechnoTypeExt::ApplyTurretOffset(pType, &mtx); + TechnoTypeExt::ApplyTurretOffset(pType, &mtx, factor); double turretRad = pThis->TurretFacing().GetRadian<32>(); double bodyRad = pThis->PrimaryFacing.Current().GetRadian<32>(); @@ -64,12 +65,27 @@ CoordStruct TechnoExt::GetFLHAbsoluteCoords(TechnoClass* pThis, CoordStruct pCoo mtx.RotateZ(angle); } - // Step 4: apply FLH offset - mtx.Translate((float)pCoord.X, (float)pCoord.Y, (float)pCoord.Z); + return mtx; +} + +Matrix3D TechnoExt::GetFLHMatrix(TechnoClass* pThis, CoordStruct pCoord, bool isOnTurret, double factor, bool isShadow) +{ + Matrix3D transform = TechnoExt::GetTransform(pThis, nullptr, isShadow); + Matrix3D mtx = TechnoExt::TransformFLHForTurret(pThis, transform, isOnTurret, factor); - auto result = mtx.GetTranslation(); + CoordStruct scaledCoord = pCoord * factor; + // apply FLH offset + mtx.Translate((float)scaledCoord.X, (float)scaledCoord.Y, (float)scaledCoord.Z); + + return mtx; +} + +// reversed from 6F3D60 +CoordStruct TechnoExt::GetFLHAbsoluteCoords(TechnoClass* pThis, CoordStruct pCoord, bool isOnTurret) +{ + auto result = TechnoExt::GetFLHMatrix(pThis, pCoord, isOnTurret).GetTranslation(); - // Step 5: apply as an offset to global object coords + // apply as an offset to global object coords // Resulting coords are mirrored along X axis, so we mirror it back auto location = pThis->GetCoords() + CoordStruct { (int)result.X, -(int)result.Y, (int)result.Z }; diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 4adfa8df6b..99cef47027 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -746,19 +746,29 @@ void TechnoExt::KillSelf(TechnoClass* pThis, AutoDeathBehavior deathOption, Anim } default: //must be AutoDeathBehavior::Kill - if (IS_ARES_FUN_AVAILABLE(SpawnSurvivors)) + TechnoExt::Kill(pThis, nullptr, pThis->Owner); + } +} + +void TechnoExt::Kill(TechnoClass* pThis, ObjectClass* pAttacker, HouseClass* pAttackingHouse) +{ + if (IS_ARES_FUN_AVAILABLE(SpawnSurvivors)) + { + switch (pThis->WhatAmI()) { - switch (pThis->WhatAmI()) - { - case AbstractType::Unit: - case AbstractType::Aircraft: - AresFunctions::SpawnSurvivors(static_cast(pThis), nullptr, false, false); - default:break; - } + case AbstractType::Unit: + case AbstractType::Aircraft: + AresFunctions::SpawnSurvivors(abstract_cast(pThis), abstract_cast(pAttacker), false, false); + default: break; } - pThis->ReceiveDamage(&pThis->Health, 0, RulesClass::Instance->C4Warhead, nullptr, true, false, pThis->Owner); - return; } + + pThis->ReceiveDamage(&pThis->Health, 0, RulesClass::Instance->C4Warhead, pAttacker, true, false, pAttackingHouse); +} + +void TechnoExt::Kill(TechnoClass* pThis, TechnoClass* pAttacker) +{ + TechnoExt::Kill(pThis, pAttacker, pAttacker ? pAttacker->Owner : nullptr); } void TechnoExt::UpdateSharedAmmo(TechnoClass* pThis) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 2a0d4bf97d..c31549ebe5 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -9,6 +9,8 @@ #include +#include + TechnoExt::ExtContainer TechnoExt::ExtMap; TechnoExt::ExtData::~ExtData() @@ -465,6 +467,138 @@ int TechnoExt::ExtData::GetAttachedEffectCumulativeCount(AttachEffectTypeClass* return foundCount; } +// Attaches this techno in a first available attachment "slot". +// Returns true if the attachment is successful. +bool TechnoExt::AttachTo(TechnoClass* pThis, TechnoClass* pParent) +{ + auto const pParentExt = TechnoExt::ExtMap.Find(pParent); + + for (auto const& pAttachment : pParentExt->ChildAttachments) + { + if (pAttachment->AttachChild(pThis)) + return true; + } + + return false; +} + +bool TechnoExt::DetachFromParent(TechnoClass* pThis) +{ + auto const pExt = TechnoExt::ExtMap.Find(pThis); + return pExt->ParentAttachment->DetachChild(); +} + +void TechnoExt::InitializeAttachments(TechnoClass* pThis) +{ + auto const pExt = TechnoExt::ExtMap.Find(pThis); + auto const pType = pThis->GetTechnoType(); + auto const pTypeExt = TechnoTypeExt::ExtMap.Find(pType); + + for (auto& entry : pTypeExt->AttachmentData) + { + pExt->ChildAttachments.push_back(std::make_unique(&entry, pThis, nullptr)); + pExt->ChildAttachments.back()->Initialize(); + } +} + +void TechnoExt::DestroyAttachments(TechnoClass* pThis, TechnoClass* pSource) +{ + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + + for (auto const& pAttachment : pExt->ChildAttachments) + pAttachment->Destroy(pSource); + + // TODO I am not sure, without clearing the attachments it sometimes crashes under + // weird circumstances, like if the techno exists but the parent attachment isn't, + // in particular in can enter cell hook, this may be a bandaid fix for something + // way worse like improper occupation clearance or whatever - Kerbiter + pExt->ChildAttachments.clear(); +} + +void TechnoExt::HandleDestructionAsChild(TechnoClass* pThis) +{ + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + + if (pExt->ParentAttachment) + pExt->ParentAttachment->ChildDestroyed(); +} + +void TechnoExt::UnlimboAttachments(TechnoClass* pThis) +{ + auto const pExt = TechnoExt::ExtMap.Find(pThis); + for (auto const& pAttachment : pExt->ChildAttachments) + pAttachment->Unlimbo(); +} + +void TechnoExt::LimboAttachments(TechnoClass* pThis) +{ + auto const pExt = TechnoExt::ExtMap.Find(pThis); + for (auto const& pAttachment : pExt->ChildAttachments) + pAttachment->Limbo(); +} + +void TechnoExt::TransferAttachments(TechnoClass* pThis, TechnoClass* pThat) +{ + auto const pThisExt = TechnoExt::ExtMap.Find(pThis); + auto const pThatExt = TechnoExt::ExtMap.Find(pThat); + + for (auto& pAttachment : pThisExt->ChildAttachments) + { + pAttachment->Parent = pThat; + pThatExt->ChildAttachments.push_back(std::move(pAttachment)); + } + + pThisExt->ChildAttachments.clear(); +} + +bool TechnoExt::IsAttached(TechnoClass* pThis) +{ + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + return pExt && pExt->ParentAttachment; +} + +bool TechnoExt::HasAttachmentLoco(FootClass* pThis) +{ + IPersistPtr pPersist = pThis->Locomotor; + CLSID locoCLSID {}; + return pPersist && SUCCEEDED(pPersist->GetClassID(&locoCLSID)) + && locoCLSID == __uuidof(AttachmentLocomotionClass); +} + +bool TechnoExt::DoesntOccupyCellAsChild(TechnoClass* pThis) +{ + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + return pExt && pExt->ParentAttachment + && !pExt->ParentAttachment->GetType()->OccupiesCell; +} + +bool TechnoExt::IsChildOf(TechnoClass* pThis, TechnoClass* pParent, bool deep) +{ + auto const pThisExt = TechnoExt::ExtMap.Find(pThis); + + return pThis && pThisExt && pParent // sanity check, sometimes crashes because ext is null - Kerbiter + && pThisExt->ParentAttachment + && (pThisExt->ParentAttachment->Parent == pParent + || (deep && TechnoExt::IsChildOf(pThisExt->ParentAttachment->Parent, pParent))); +} + +bool TechnoExt::AreRelatives(TechnoClass* pThis, TechnoClass* pThat) +{ + return TechnoExt::GetTopLevelParent(pThis) + == TechnoExt::GetTopLevelParent(pThat); +} + +// Returns this if no parent. +TechnoClass* TechnoExt::GetTopLevelParent(TechnoClass* pThis) +{ + auto const pThisExt = TechnoExt::ExtMap.Find(pThis); + + return pThis && pThisExt // sanity check, sometimes crashes because ext is null - Kerbiter + && pThisExt->ParentAttachment + ? TechnoExt::GetTopLevelParent(pThisExt->ParentAttachment->Parent) + : pThis; +} + // ============================= // load / save diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 0a90b4f688..9fab58ec81 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -9,6 +9,7 @@ #include #include #include +#include class BulletClass; @@ -56,6 +57,9 @@ class TechnoExt int LastWarpInDelay; // Last-warp in delay for this unit, used by HasCarryoverWarpInDelay. bool IsBeingChronoSphered; // Set to true on units currently being ChronoSphered, does not apply to Ares-ChronoSphere'd buildings or Chrono reinforcements. + AttachmentClass* ParentAttachment; + ValueableVector> ChildAttachments; + ExtData(TechnoClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } , Shield {} @@ -86,6 +90,8 @@ class TechnoExt , HasRemainingWarpInDelay { false } , LastWarpInDelay { 0 } , IsBeingChronoSphered { false} + , ParentAttachment {} + , ChildAttachments {} { } void OnEarlyUpdate(); @@ -111,7 +117,13 @@ class TechnoExt int GetAttachedEffectCumulativeCount(AttachEffectTypeClass* pAttachEffectType, bool ignoreSameSource = false, TechnoClass* pInvoker = nullptr, AbstractClass* pSource = nullptr) const; virtual ~ExtData() override; - virtual void InvalidatePointer(void* ptr, bool bRemoved) override { } + + virtual void InvalidatePointer(void* ptr, bool bRemoved) override + { + for (auto const& pAttachment : ChildAttachments) + pAttachment->InvalidatePointer(ptr); + } + virtual void LoadFromStream(PhobosStreamReader& Stm) override; virtual void SaveToStream(PhobosStreamWriter& Stm) override; @@ -137,13 +149,38 @@ class TechnoExt static bool IsHarvesting(TechnoClass* pThis); static bool HasAvailableDock(TechnoClass* pThis); - static CoordStruct GetFLHAbsoluteCoords(TechnoClass* pThis, CoordStruct flh, bool turretFLH = false); + static void InitializeLaserTrails(TechnoClass* pThis); + static void InitializeShield(TechnoClass* pThis); + + static Matrix3D GetTransform(TechnoClass* pThis, VoxelIndexKey* pKey = nullptr, bool isShadow = false); + static Matrix3D GetFLHMatrix(TechnoClass* pThis, CoordStruct flh, bool isOnTurret, double factor = 1.0, bool isShadow = false); + static Matrix3D TransformFLHForTurret(TechnoClass* pThis, Matrix3D mtx, bool isOnTurret, double factor = 1.0); + static CoordStruct GetFLHAbsoluteCoords(TechnoClass* pThis, CoordStruct flh, bool isOnTurret = false); static CoordStruct GetBurstFLH(TechnoClass* pThis, int weaponIndex, bool& FLHFound); static CoordStruct GetSimpleFLH(InfantryClass* pThis, int weaponIndex, bool& FLHFound); + static bool AttachTo(TechnoClass* pThis, TechnoClass* pParent); + static bool DetachFromParent(TechnoClass* pThis); + + static void InitializeAttachments(TechnoClass* pThis); + static void DestroyAttachments(TechnoClass* pThis, TechnoClass* pSource); + static void HandleDestructionAsChild(TechnoClass* pThis); + static void UnlimboAttachments(TechnoClass* pThis); + static void LimboAttachments(TechnoClass* pThis); + static void TransferAttachments(TechnoClass* pThis, TechnoClass* pThat); + + static bool IsAttached(TechnoClass* pThis); + static bool HasAttachmentLoco(FootClass* pThis); // FIXME shouldn't be here + static bool DoesntOccupyCellAsChild(TechnoClass* pThis); + static bool IsChildOf(TechnoClass* pThis, TechnoClass* pParent, bool deep = true); + static bool AreRelatives(TechnoClass* pThis, TechnoClass* pThat); + static TechnoClass* GetTopLevelParent(TechnoClass* pThis); + static void ChangeOwnerMissionFix(FootClass* pThis); static void KillSelf(TechnoClass* pThis, AutoDeathBehavior deathOption, AnimTypeClass* pVanishAnimation, bool isInLimbo = false); + static void Kill(TechnoClass* pThis, ObjectClass* pAttacker, HouseClass* pAttackingHouse); + static void Kill(TechnoClass* pThis, TechnoClass* pAttacker); static void TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo); static void ApplyMindControlRangeLimit(TechnoClass* pThis); static void ObjectKilledBy(TechnoClass* pThis, TechnoClass* pKiller); diff --git a/src/Ext/Techno/Hooks.Shield.cpp b/src/Ext/Techno/Hooks.Shield.cpp index 1fb90ba80c..ef2521f55f 100644 --- a/src/Ext/Techno/Hooks.Shield.cpp +++ b/src/Ext/Techno/Hooks.Shield.cpp @@ -277,7 +277,7 @@ class AresScheme }; -FireError __fastcall UnitClass__GetFireError_Wrapper(UnitClass* pThis, void* _, ObjectClass* pObj, int nWeaponIndex, bool ignoreRange) +FireError __fastcall UnitClass__GetFireError_Wrapper(UnitClass* pThis, discard_t _, ObjectClass* pObj, int nWeaponIndex, bool ignoreRange) { AresScheme::Prefix(pThis, pObj, nWeaponIndex, false); auto const result = pThis->UnitClass::GetFireError(pObj, nWeaponIndex, ignoreRange); @@ -286,7 +286,7 @@ FireError __fastcall UnitClass__GetFireError_Wrapper(UnitClass* pThis, void* _, } DEFINE_JUMP(VTABLE, 0x7F6030, GET_OFFSET(UnitClass__GetFireError_Wrapper)) -FireError __fastcall InfantryClass__GetFireError_Wrapper(InfantryClass* pThis, void* _, ObjectClass* pObj, int nWeaponIndex, bool ignoreRange) +FireError __fastcall InfantryClass__GetFireError_Wrapper(InfantryClass* pThis, discard_t _, ObjectClass* pObj, int nWeaponIndex, bool ignoreRange) { AresScheme::Prefix(pThis, pObj, nWeaponIndex, false); auto const result = pThis->InfantryClass::GetFireError(pObj, nWeaponIndex, ignoreRange); @@ -295,20 +295,50 @@ FireError __fastcall InfantryClass__GetFireError_Wrapper(InfantryClass* pThis, v } DEFINE_JUMP(VTABLE, 0x7EB418, GET_OFFSET(InfantryClass__GetFireError_Wrapper)) -Action __fastcall UnitClass__WhatAction_Wrapper(UnitClass* pThis, void* _, ObjectClass* pObj, bool ignoreForce) +Action __fastcall UnitClass__WhatAction_Wrapper(UnitClass* pThis, discard_t _, ObjectClass* pObj, bool ignoreForce) { AresScheme::Prefix(pThis, pObj, -1, false); - auto const result = pThis->UnitClass::MouseOverObject(pObj, ignoreForce); + auto result = pThis->UnitClass::MouseOverObject(pObj, ignoreForce); AresScheme::Suffix(); + + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + if (!pExt->ParentAttachment) + return result; + + switch (result) + { + case Action::Repair: + result = Action::NoRepair; + break; + + case Action::Self_Deploy: + if (pThis->Type->DeploysInto) + result = Action::NoDeploy; + break; + + case Action::Sabotage: + case Action::Capture: + case Action::Enter: + result = Action::NoEnter; + break; + + case Action::GuardArea: + case Action::AttackMoveNav: + case Action::Move: + result = Action::NoMove; + break; + } + return result; } DEFINE_JUMP(VTABLE, 0x7F5CE4, GET_OFFSET(UnitClass__WhatAction_Wrapper)) -Action __fastcall InfantryClass__WhatAction_Wrapper(InfantryClass* pThis, void* _, ObjectClass* pObj, bool ignoreForce) +Action __fastcall InfantryClass__WhatAction_Wrapper(InfantryClass* pThis, discard_t _, ObjectClass* pObj, bool ignoreForce) { AresScheme::Prefix(pThis, pObj, -1, pThis->Type->Engineer); auto const result = pThis->InfantryClass::MouseOverObject(pObj, ignoreForce); AresScheme::Suffix(); + return result; } DEFINE_JUMP(VTABLE, 0x7EB0CC, GET_OFFSET(InfantryClass__WhatAction_Wrapper)) diff --git a/src/Ext/Techno/Hooks.TechnoAttachment.cpp b/src/Ext/Techno/Hooks.TechnoAttachment.cpp new file mode 100644 index 0000000000..dadce93067 --- /dev/null +++ b/src/Ext/Techno/Hooks.TechnoAttachment.cpp @@ -0,0 +1,857 @@ +#include "Body.h" + +#include +#include +#include + +#include + +#include + + +DEFINE_HOOK(0x707CB3, TechnoClass_KillCargo_HandleAttachments, 0x6) +{ + GET(TechnoClass*, pThis, EBX); + GET_STACK(TechnoClass*, pSource, STACK_OFFSET(0x4, 0x4)); + + TechnoExt::DestroyAttachments(pThis, pSource); + + return 0; +} + +DEFINE_HOOK(0x5F6609, ObjectClass_RemoveThis_TechnoClass_NotifyParent, 0x9) +{ + GET(TechnoClass*, pThis, ESI); + + pThis->KillPassengers(nullptr); // restored code + TechnoExt::HandleDestructionAsChild(pThis); + + return 0x5F6612; +} + +DEFINE_HOOK(0x4DEBB4, FootClass_OnDestroyed_NotifyParent, 0x8) +{ + GET(FootClass*, pThis, ESI); + + TechnoExt::HandleDestructionAsChild(pThis); + + return 0; +} + + +DEFINE_HOOK(0x6F6F20, TechnoClass_Unlimbo_UnlimboAttachments, 0x6) +{ + GET(TechnoClass*, pThis, ESI); + + TechnoExt::UnlimboAttachments(pThis); + + return 0; +} + +DEFINE_HOOK(0x6F6B1C, TechnoClass_Limbo_LimboAttachments, 0x6) +{ + GET(TechnoClass*, pThis, ESI); + + TechnoExt::LimboAttachments(pThis); + + return 0; +} + +#pragma region Cell occupation handling + +// because Ares hooks in the single usable position we need to do a detour instead +// screw Ares + +void __fastcall UnitClass_SetOccupyBit_SkipVirtual(UnitClass* pThis, discard_t, const CoordStruct& coords) +{ + if (!TechnoExt::DoesntOccupyCellAsChild(pThis)) + pThis->UnitClass::MarkAllOccupationBits(coords); +} + +void __fastcall UnitClass_ClearOccupyBit_SkipVirtual(UnitClass* pThis, discard_t, const CoordStruct& coords) +{ + if (!TechnoExt::DoesntOccupyCellAsChild(pThis)) + pThis->UnitClass::UnmarkAllOccupationBits(coords); +} + +DEFINE_JUMP(VTABLE, 0x7F5D60, GET_OFFSET(UnitClass_SetOccupyBit_SkipVirtual)) +DEFINE_JUMP(VTABLE, 0x7F5D64, GET_OFFSET(UnitClass_ClearOccupyBit_SkipVirtual)) + +// TODO ^ same for non-UnitClass, not needed so cba for now + +namespace TechnoAttachmentTemp +{ + // no idea what Ares or w/e else is doing with occupation flags, + // so just to be safe assume it can be nothing and store it + byte storedVehicleFlag; +} + +// Game assumes cell is occupied by a vehicle by default and if this vehicle +// turns out to be self, then it un-assumes the occupancy. Because with techno +// attachment logic it's possible to have multiple vehicles on the same cell, +// we flip the logic from "passable if special case is found" to "impassable if +// non-special case is found" - Kerbiter + +void AssumeNoVehicleByDefault(byte& occupyFlags, bool& isVehicleFlagSet) +{ + TechnoAttachmentTemp::storedVehicleFlag = occupyFlags & 0x20; + + occupyFlags &= ~0x20; + isVehicleFlagSet = false; +} + +DEFINE_HOOK(0x73F520, UnitClass_CanEnterCell_AssumeNoVehicleByDefault, 0x0) +{ + enum { Check = 0x73F528, Skip = 0x73FA92 }; + + REF_STACK(byte, occupyFlags, STACK_OFFSET(0x90, -0x7C)); + REF_STACK(bool, isVehicleFlagSet, STACK_OFFSET(0x90, -0x7B)); + + GET(TechnoClass*, pOccupier, ESI); + + if (!pOccupier) // stolen code + return Skip; + + AssumeNoVehicleByDefault(occupyFlags, isVehicleFlagSet); + + return Check; +} + +bool IsOccupierIgnorable(TechnoClass* pThis, ObjectClass* pOccupier, byte& occupyFlags, bool& isVehicleFlagSet) +{ + if (pThis == pOccupier) + return true; + + auto const pTechno = abstract_cast(pOccupier); + if (pTechno && + (TechnoExt::DoesntOccupyCellAsChild(pTechno) || TechnoExt::IsChildOf(pTechno, pThis))) + { + return true; + } + + if (abstract_cast(pOccupier)) + { + occupyFlags |= TechnoAttachmentTemp::storedVehicleFlag; + isVehicleFlagSet = TechnoAttachmentTemp::storedVehicleFlag != 0; + } + + return false; +} + +DEFINE_HOOK(0x73F528, UnitClass_CanEnterCell_SkipChildren, 0x0) +{ + enum { IgnoreOccupier = 0x73FA87, Continue = 0x73F530 }; + + GET(UnitClass*, pThis, EBX); + GET(ObjectClass*, pOccupier, ESI); + + REF_STACK(byte, occupyFlags, STACK_OFFSET(0x90, -0x7C)); + REF_STACK(bool, isVehicleFlagSet, STACK_OFFSET(0x90, -0x7B)); + + return IsOccupierIgnorable(pThis, pOccupier, occupyFlags, isVehicleFlagSet) + ? IgnoreOccupier : Continue; +} + +// Also because the occupancy can be marked by something moving into the spot +// we also check the nearby (yes this is an assumption, if you are reviewing +// this voice your opinion in the review) cells for the presence of technos +// that are moving into this spot right now - Kerbiter + +void AccountForMovingInto(CellClass* into, bool isAlt, TechnoClass* pThis, byte& occupyFlags, bool& isVehicleFlagSet) +{ + // see 42B080, A* checks 2 cells around, this is approx. it + CellStruct center = into->MapCoords; + CoordStruct intoCoords = into->GetCellCoords(); + if (isAlt) + intoCoords.Z += CellClass::BridgeHeight; + + for (CellRectEnumerator cell({ center.X - 2, center.Y - 2, center.X + 2, center.Y + 2}); cell; ++cell) + { + if (auto const pCell = MapClass::Instance->TryGetCellAt(*cell)) + { + for (NextObject object(isAlt ? pCell->AltObject : pCell->FirstObject); object; ++object) + { + if (*object == pThis) + continue; + + auto const pUnit = abstract_cast(*object); + if (pUnit && pUnit->Locomotor->Is_Moving_Here(intoCoords) + && !TechnoExt::DoesntOccupyCellAsChild(pUnit) + && !TechnoExt::IsChildOf(pUnit, pThis)) + { + occupyFlags |= TechnoAttachmentTemp::storedVehicleFlag; + isVehicleFlagSet = TechnoAttachmentTemp::storedVehicleFlag != 0; + } + } + } + } +} + +DEFINE_HOOK(0x73FA92, UnitClass_CanEnterCell_CheckMovingInto, 0x0) +{ + GET_STACK(CellClass*, into, STACK_OFFSET(0x90, 0x4)); + GET_STACK(bool const, isAlt, STACK_OFFSET(0x90, -0x7D)); + GET(UnitClass*, pThis, EBX); + + REF_STACK(byte, occupyFlags, STACK_OFFSET(0x90, -0x7C)); + REF_STACK(bool, isVehicleFlagSet, STACK_OFFSET(0x90, -0x7B)); + + AccountForMovingInto(into, isAlt, pThis, occupyFlags, isVehicleFlagSet); + + // stolen code ahead + if (!isAlt) + return 0x73FA9E; + + return 0x73FC24; +} + +DEFINE_HOOK(0x51C249, InfantryClass_CanEnterCell_AssumeNoVehicleByDefault, 0x0) +{ + enum { Check = 0x51C251, Skip = 0x51C78F }; + + REF_STACK(byte, occupyFlags, STACK_OFFSET(0x34, -0x21)); + REF_STACK(bool, isVehicleFlagSet, STACK_OFFSET(0x34, -0x22)); + + GET(TechnoClass*, pOccupier, ESI); + + if (!pOccupier) // stolen code + return Skip; + + AssumeNoVehicleByDefault(occupyFlags, isVehicleFlagSet); + + return Check; +} + +DEFINE_HOOK(0x51C251, InfantryClass_CanEnterCell_SkipChildren, 0x0) +{ + enum { IgnoreOccupier = 0x51C70F, Continue = 0x51C259 }; + + GET(InfantryClass*, pThis, EBP); + GET(ObjectClass*, pOccupier, ESI); + + REF_STACK(byte, occupyFlags, STACK_OFFSET(0x34, -0x21)); + REF_STACK(bool, isVehicleFlagSet, STACK_OFFSET(0x34, -0x22)); + + return IsOccupierIgnorable(pThis, pOccupier, occupyFlags, isVehicleFlagSet) + ? IgnoreOccupier : Continue; +} + +DEFINE_HOOK(0x51C78F, InfantryClass_CanEnterCell_CheckMovingInto, 0x6) +{ + GET_STACK(CellClass*, into, STACK_OFFSET(0x34, 0x4)); + GET_STACK(bool const, isAlt, STACK_OFFSET(0x34, -0x23)); + GET(InfantryClass*, pThis, EBP); + + REF_STACK(byte, occupyFlags, STACK_OFFSET(0x34, -0x21)); + REF_STACK(bool, isVehicleFlagSet, STACK_OFFSET(0x34, -0x22)); + + AccountForMovingInto(into, isAlt, pThis, occupyFlags, isVehicleFlagSet); + + return 0; +} + +enum class CellTechnoMode +{ + NoAttachments, + NoVirtualOrRelatives, + NoVirtual, + NoRelatives, // misleading name but I think doesn't matter for the use case for now + All, + + DefaultBehavior = All, +}; + +namespace TechnoAttachmentTemp +{ + CellTechnoMode currentMode = CellTechnoMode::DefaultBehavior; +} + +#define DEFINE_CELLTECHNO_WRAPPER(mode) \ +TechnoClass* __fastcall CellTechno_##mode(CellClass* pThis, discard_t, Point2D *a2, bool check_alt, TechnoClass* techno) \ +{ \ + TechnoAttachmentTemp::currentMode = CellTechnoMode::mode; \ + auto const retval = pThis->FindTechnoNearestTo(*a2, check_alt, techno); \ + TechnoAttachmentTemp::currentMode = CellTechnoMode::DefaultBehavior; \ + return retval; \ +} + +DEFINE_CELLTECHNO_WRAPPER(NoAttachments); +DEFINE_CELLTECHNO_WRAPPER(NoVirtualOrRelatives); +DEFINE_CELLTECHNO_WRAPPER(NoVirtual); +DEFINE_CELLTECHNO_WRAPPER(NoRelatives); +DEFINE_CELLTECHNO_WRAPPER(All); + +#undef DEFINE_CELLTECHNO_WRAPPER + +DEFINE_HOOK(0x47C432, CellClass_CellTechno_HandleAttachments, 0x0) +{ + enum { Continue = 0x47C437, IgnoreOccupier = 0x47C4A7 }; + + GET(TechnoClass*, pOccupier, ESI); + GET_BASE(TechnoClass*, pSelf, 0x10); + + using namespace TechnoAttachmentTemp; + const bool noAttachments = + currentMode == CellTechnoMode::NoAttachments; + const bool noVirtual = + currentMode == CellTechnoMode::NoVirtual || + currentMode == CellTechnoMode::NoVirtualOrRelatives; + const bool noRelatives = + currentMode == CellTechnoMode::NoRelatives || + currentMode == CellTechnoMode::NoVirtualOrRelatives; + + if (pOccupier == pSelf // restored code + || noAttachments && TechnoExt::IsAttached(pOccupier) + || noVirtual && TechnoExt::DoesntOccupyCellAsChild(pOccupier) + || noRelatives && TechnoExt::IsChildOf(pOccupier, (TechnoClass*)pSelf)) + { + return IgnoreOccupier; + } + + return Continue; +} + +// skip building placement occupation checks for virtuals +DEFINE_JUMP(CALL, 0x47C805, GET_OFFSET(CellTechno_NoVirtual)); +DEFINE_JUMP(CALL, 0x47C738, GET_OFFSET(CellTechno_NoVirtual)); + +// skip building attachments in bib check +DEFINE_JUMP(CALL, 0x4495F2, GET_OFFSET(CellTechno_NoVirtualOrRelatives)); +DEFINE_JUMP(CALL, 0x44964E, GET_OFFSET(CellTechno_NoVirtualOrRelatives)); + +DEFINE_HOOK(0x4495F7, BuildingClass_ClearFactoryBib_SkipCreatedUnitAttachments, 0x0) +{ + enum { BibClear = 0x44969B, NotClear = 0x4495FF }; + + GET(TechnoClass*, pBibTechno, EAX); + + if (!pBibTechno) + return BibClear; + + GET(BuildingClass*, pThis, ESI); + + TechnoClass* pBuiltTechno = pThis->GetNthLink(0); + if (TechnoExt::IsChildOf(pBibTechno, pBuiltTechno)) + return BibClear; + + return NotClear; +} + +// original code doesn't account for multiple possible technos on the cell +DEFINE_HOOK(0x73A5EA, UnitClass_PerCellProcess_EntryLoopTechnos, 0x0) +{ + enum { SkipEntry = 0x73A7D2, TryEnterTarget = 0x73A6D1 }; + + GET(UnitClass*, pThis, EBP); + + if (pThis->GetCurrentMission() != Mission::Enter) + return SkipEntry; + + CellClass* pCell = pThis->GetCell(); + ObjectClass*& pFirst = pThis->OnBridge + ? pCell->AltObject : pCell->FirstObject; + + for (ObjectClass* pObject = pFirst; pObject; pObject = pObject->NextObject) + { + auto pEntryTarget = abstract_cast(pObject); + + if (pEntryTarget + && pEntryTarget != pThis + && pEntryTarget->GetMapCoords() == pThis->GetMapCoords() + && pThis->ContainsLink(pEntryTarget) + && pEntryTarget->GetTechnoType()->Passengers > 0) + { + R->ESI(pEntryTarget); + return TryEnterTarget; + } + } + + return SkipEntry; +} + +enum class AttachCargoMode +{ + SingleObject, + ObjectChain, + + DefaultBehavior = SingleObject, +}; + +namespace TechnoAttachmentTemp +{ + AttachCargoMode currentAttachMode = AttachCargoMode::DefaultBehavior; +} + +#define DEFINE_ATTACH_WRAPPER(mode) \ +void __fastcall CargoClass_Attach_##mode(PassengersClass* pThis, discard_t, FootClass* pThat) \ +{ \ + TechnoAttachmentTemp::currentAttachMode = AttachCargoMode::mode; \ + pThis->AddPassenger(pThat); \ + TechnoAttachmentTemp::currentAttachMode = AttachCargoMode::DefaultBehavior; \ +} + +DEFINE_ATTACH_WRAPPER(SingleObject); +DEFINE_ATTACH_WRAPPER(ObjectChain); + +DEFINE_JUMP(CALL, 0x65DF88, GET_OFFSET(CargoClass_Attach_ObjectChain)); // Create_Group +DEFINE_JUMP(CALL, 0x65DCF0, GET_OFFSET(CargoClass_Attach_ObjectChain)); // Do_Reinforcements, paradrop loading + +DEFINE_HOOK(0x4733BD, CargoClass_Attach_HandleCurrentAttachMode, 0x6) +{ + enum { SkipAttachingChain = 0x4733FA, Continue = 0x0 }; + + return TechnoAttachmentTemp::currentAttachMode == AttachCargoMode::SingleObject + ? SkipAttachingChain + : Continue; +} + +#pragma endregion + +#pragma region InAir/OnGround + +bool __fastcall TechnoClass_OnGround(TechnoClass* pThis) +{ + auto const pExt = TechnoExt::ExtMap.Find(pThis); + + return pExt->ParentAttachment && pExt->ParentAttachment->GetType()->InheritHeightStatus + ? pExt->ParentAttachment->Parent->IsOnFloor() + : pThis->ObjectClass::IsOnFloor(); +} + +bool __fastcall TechnoClass_InAir(TechnoClass* pThis) +{ + auto const pExt = TechnoExt::ExtMap.Find(pThis); + + return pExt->ParentAttachment && pExt->ParentAttachment->GetType()->InheritHeightStatus + ? pExt->ParentAttachment->Parent->IsInAir() + : pThis->ObjectClass::IsInAir(); +} + +bool __fastcall TechnoClass_IsSurfaced(TechnoClass* pThis) +{ + auto const pExt = TechnoExt::ExtMap.Find(pThis); + + return pExt->ParentAttachment && pExt->ParentAttachment->GetType()->InheritHeightStatus + ? pExt->ParentAttachment->Parent->IsSurfaced() + : pThis->ObjectClass::IsSurfaced(); +} + +// TechnoClass +DEFINE_JUMP(VTABLE, 0x7F49B0, GET_OFFSET(TechnoClass_OnGround)); +DEFINE_JUMP(VTABLE, 0x7F49B4, GET_OFFSET(TechnoClass_InAir)); +DEFINE_JUMP(VTABLE, 0x7F49DC, GET_OFFSET(TechnoClass_IsSurfaced)); + +// BuildingClass +DEFINE_JUMP(VTABLE, 0x7E3F0C, GET_OFFSET(TechnoClass_OnGround)); +DEFINE_JUMP(VTABLE, 0x7E3F10, GET_OFFSET(TechnoClass_InAir)); +DEFINE_JUMP(VTABLE, 0x7E3F38, GET_OFFSET(TechnoClass_IsSurfaced)); + +// FootClass +DEFINE_JUMP(VTABLE, 0x7E8CE4, GET_OFFSET(TechnoClass_OnGround)); +DEFINE_JUMP(VTABLE, 0x7E8CE8, GET_OFFSET(TechnoClass_InAir)); +DEFINE_JUMP(VTABLE, 0x7E8D10, GET_OFFSET(TechnoClass_IsSurfaced)); + +// UnitClass +DEFINE_JUMP(VTABLE, 0x7F5CC0, GET_OFFSET(TechnoClass_OnGround)); +DEFINE_JUMP(VTABLE, 0x7F5CC4, GET_OFFSET(TechnoClass_InAir)); +DEFINE_JUMP(VTABLE, 0x7F5CEC, GET_OFFSET(TechnoClass_IsSurfaced)); + +// InfantryClass +DEFINE_JUMP(VTABLE, 0x7EB0A8, GET_OFFSET(TechnoClass_OnGround)); +DEFINE_JUMP(VTABLE, 0x7EB0AC, GET_OFFSET(TechnoClass_InAir)); +DEFINE_JUMP(VTABLE, 0x7EB0D4, GET_OFFSET(TechnoClass_IsSurfaced)); + +// AircraftClass has it's own logic, who would want to attach aircrafts anyways + +#pragma endregion + +DEFINE_HOOK(0x6CC763, SuperClass_Place_ChronoWarp_SkipChildren, 0x6) +{ + enum { Skip = 0x6CCCCA, Continue = 0 }; + + GET(FootClass* const, pFoot, ESI); + + return TechnoExt::IsAttached(pFoot) ? Skip : Continue; +} + +#pragma region Command inheritance + +void ParentClickedWaypoint(TechnoClass* pThis, int idxPath, signed char idxWP) +{ + // Rewrite of the original code + pThis->AssignPlanningPath(idxPath, idxWP); + + if ((pThis->AbstractFlags & AbstractFlags::Foot) == AbstractFlags::Foot) + pThis->unknown_bool_430 = false; + + // Children handling + if (auto const& pExt = TechnoExt::ExtMap.Find(pThis)) + { + for (auto const& pAttachment : pExt->ChildAttachments) + { + if (pAttachment->Child && pAttachment->GetType()->InheritCommands) + ParentClickedWaypoint(pAttachment->Child, idxPath, idxWP); + } + } +} + +void ParentClickedAction(TechnoClass* pThis, ObjectClass* pTarget, CellStruct* pCell, CellStruct* pSecondCell) +{ + // Rewrite of the original code + if (pTarget) + { + Action whatAction = pThis->MouseOverObject(pTarget, false); + pThis->ObjectClickedAction(whatAction, pTarget, false); + } + else + { + Action whatAction = pThis->MouseOverCell(pCell, false, false); + pThis->CellClickedAction(whatAction, pCell, pSecondCell, false); + } + + Unsorted::MoveFeedback = false; + + // Children handling + if (auto const& pExt = TechnoExt::ExtMap.Find(pThis)) + { + for (auto const& pAttachment : pExt->ChildAttachments) + { + if (pAttachment->Child && pAttachment->GetType()->InheritCommands) + ParentClickedAction(pAttachment->Child, pTarget, pCell, pSecondCell); + } + } +} + +DEFINE_HOOK(0x4AE7B3, DisplayClass_ActiveClickWith_Iterate, 0x0) +{ + REF_STACK(int, idxPath, STACK_OFFSET(0x18, -0x8)); + REF_STACK(unsigned char, idxWP, STACK_OFFSET(0x18, -0xC)); + + for (auto const& pObject : ObjectClass::CurrentObjects.get()) + { + if (auto pTechno = abstract_cast(pObject)) + ParentClickedWaypoint(pTechno, idxPath, idxWP); + } + + GET_STACK(ObjectClass* const, pTarget, STACK_OFFSET(0x18, +0x4)); + LEA_STACK(CellStruct* const, pCell, STACK_OFFSET(0x18, +0x8)); + GET_STACK(Action const, action, STACK_OFFSET(0x18, +0xC)); + + CellStruct invalidCell { -1, -1 }; + CellStruct* pSecondCell = &invalidCell; + + if (action == Action::Move || action == Action::PatrolWaypoint || action == Action::NoMove) + pSecondCell = pCell; + + for (auto const& pObject : ObjectClass::CurrentObjects.get()) + { + if (auto pTechno = abstract_cast(pObject)) + ParentClickedAction(pTechno, pTarget, pCell, pSecondCell); + } + + Unsorted::MoveFeedback = true; + + return 0x4AE99B; +} + +namespace TechnoAttachmentTemp +{ + bool stopPressed = false; + bool deployPressed = false; +} + +DEFINE_HOOK(0x730EA0, StopCommand_Context_Set, 0x5) +{ + TechnoAttachmentTemp::stopPressed = true; + return 0; +} + +DEFINE_HOOK(0x730AF0, DeployCommand_Context_Set, 0x8) +{ + TechnoAttachmentTemp::deployPressed = true; + return 0; +} + +namespace TechnoAttachmentTemp +{ + TechnoClass* pParent = nullptr; +} + +DEFINE_HOOK(0x6FFE00, TechnoClass_ClickedEvent_Context_Set, 0x5) +{ + TechnoAttachmentTemp::pParent = R->ECX(); + return 0; +} + +DEFINE_HOOK_AGAIN(0x6FFEB1, TechnoClass_ClickedEvent_HandleChildren, 0x6) +DEFINE_HOOK(0x6FFE4F, TechnoClass_ClickedEvent_HandleChildren, 0x6) +{ + if ((TechnoAttachmentTemp::stopPressed || TechnoAttachmentTemp::deployPressed) + && TechnoAttachmentTemp::pParent) + { + if (auto const& pExt = TechnoExt::ExtMap.Find(TechnoAttachmentTemp::pParent)) + { + for (auto const& pAttachment : pExt->ChildAttachments) + { + if (!pAttachment->Child) + continue; + + if (pAttachment->GetType()->InheritCommands_StopCommand) + pAttachment->Child->ClickedEvent(EventType::Idle); + + if (pAttachment->GetType()->InheritCommands_DeployCommand) + pAttachment->Child->ClickedEvent(EventType::Deploy); + } + } + } + + return 0; +} + +DEFINE_HOOK(0x730F1C, StopCommand_Context_Unset, 0x5) +{ + TechnoAttachmentTemp::stopPressed = false; + return 0; +} + +DEFINE_HOOK(0x730D55, DeployCommand_Context_Unset, 0x7) +{ + TechnoAttachmentTemp::deployPressed = false; + return 0; +} + + +#pragma endregion + +DEFINE_HOOK(0x469672, BulletClass_Logics_Locomotor_CheckIfAttached, 0x6) +{ + enum { SkipInfliction = 0x469AA4, ContinueCheck = 0x0 }; + + GET(FootClass*, pThis, EDI); + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + + return pExt->ParentAttachment + ? SkipInfliction + : ContinueCheck; +} + +DEFINE_HOOK(0x6FC3F4, TechnoClass_CanFire_HandleAttachmentLogics, 0x6) +{ + enum { ReturnFireErrorIllegal = 0x6FC86A, ContinueCheck = 0x0 }; + + GET(TechnoClass*, pThis, ESI); + GET(TechnoClass*, pTarget, EBP); + GET(WeaponTypeClass*, pWeapon, EDI); + + //auto const& pExt = TechnoExt::ExtMap.Find(pThis); + //auto const& pTargetExt = TechnoExt::ExtMap.Find(pTarget); + + bool illegalParentTargetWarhead = pWeapon->Warhead + && pWeapon->Warhead->IsLocomotor; + + if (illegalParentTargetWarhead && TechnoExt::IsChildOf(pThis, pTarget)) + return ReturnFireErrorIllegal; + + return ContinueCheck; +} + +// TODO WhatWeaponShouldIUse + +DEFINE_HOOK(0x6F3283, TechnoClass_CanScatter_CheckIfAttached, 0x8) +{ + enum { ReturnFalse = 0x6F32C5, ContinueCheck = 0x0 }; + + GET(TechnoClass*, pThis, ECX); + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + + return pExt->ParentAttachment + ? ReturnFalse + : ContinueCheck; +} + +DEFINE_HOOK(0x4817A8, CellClass_Incoming_CheckIfTechnoOccupies, 0x6) +{ + enum { ConditionIsTrue = 0x4817C3, ContinueCheck = 0x0 }; + + GET(TechnoClass*, pTechno, ESI); + auto const& pExt = TechnoExt::ExtMap.Find(pTechno); + + return pExt->ParentAttachment && pExt->ParentAttachment->GetType()->OccupiesCell + ? ConditionIsTrue + : ContinueCheck; +} + +DEFINE_HOOK(0x4817C3, CellClass_Incoming_HandleScatterWithAttachments, 0x0) +{ + GET(TechnoClass*, pTechno, ESI); + + GET(CoordStruct*, pThreatCoord, EBP); + GET(bool, isForced, EBX); + GET_STACK(bool, isNoKidding, STACK_OFFSET(0x2C, 0xC)); // direct all complaints to tomsons26 for the variable naming + CoordStruct const& threatCoord = *pThreatCoord; + + // we already checked that this is something that occupies the cell, see the hook above - Kerbiter + TechnoExt::GetTopLevelParent(pTechno)->Scatter(threatCoord, isForced, isNoKidding); + + return 0x4817D9; +} + +DEFINE_HOOK(0x51D0DD, InfantryClass_Scatter_CheckAttachments, 0x6) +{ + enum { Bail = 0x51D6E6, Continue = 0x0 }; + + GET(InfantryClass*, pThis, ESI); + + return TechnoExt::HasAttachmentLoco(pThis) + ? Bail + : Continue; +} + + +DEFINE_HOOK(0x736FB6, UnitClass_FiringAI_ForbidAttachmentRotation, 0x6) +{ + enum { SkipBodyRotation = 0x737063, ContinueCheck = 0x0 }; + + GET(UnitClass*, pThis, ESI); + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + + return pExt->ParentAttachment + ? SkipBodyRotation + : ContinueCheck; +} + +DEFINE_HOOK(0x736A2F, UnitClass_RotationAI_ForbidAttachmentRotation, 0x7) +{ + enum { SkipBodyRotation = 0x736A8E, ContinueCheck = 0x0 }; + + GET(UnitClass*, pThis, ESI); + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + + return pExt->ParentAttachment + ? SkipBodyRotation + : ContinueCheck; +} + +Action __fastcall UnitClass_MouseOverCell_Wrapper(UnitClass* pThis, discard_t, CellStruct const* pCell, bool checkFog, bool ignoreForce) +{ + Action result = pThis->UnitClass::MouseOverCell(pCell, checkFog, ignoreForce); + + auto const& pExt = TechnoExt::ExtMap.Find(pThis); + if (!pExt->ParentAttachment) + return result; + + switch (result) + { + case Action::GuardArea: + case Action::AttackMoveNav: + case Action::PatrolWaypoint: + case Action::Harvest: + case Action::Move: + result = Action::NoMove; + break; + case Action::EnterTunnel: + result = Action::NoEnterTunnel; + break; + } + + return result; +} + +// MouseOverObject for entering bunkers, grinder, buildings etc +// is handled along with the shield logics in another file + +DEFINE_JUMP(VTABLE, 0x7F5CE0, GET_OFFSET(UnitClass_MouseOverCell_Wrapper)) + +// YSort for attachments +int __fastcall TechnoClass_SortY_Wrapper(ObjectClass* pThis) +{ + auto const pTechno = abstract_cast(pThis); + + if (pTechno) + { + const auto pExt = TechnoExt::ExtMap.Find(pTechno); + + if (pExt->ParentAttachment) + { + const auto ySortPosition = pExt->ParentAttachment->GetType()->YSortPosition.Get(); + const auto pParentTechno = pExt->ParentAttachment->Parent; + + if (ySortPosition != AttachmentYSortPosition::Default && pParentTechno) + { + int parentYSort = pParentTechno->GetYSort(); + + return parentYSort + (ySortPosition == AttachmentYSortPosition::OverParent ? 1 : -1); + } + } + } + + return pThis->ObjectClass::GetYSort(); +} + +DEFINE_JUMP(CALL, 0x449413, GET_OFFSET(TechnoClass_SortY_Wrapper)) // BuildingClass +DEFINE_JUMP(VTABLE, 0x7E235C, GET_OFFSET(TechnoClass_SortY_Wrapper)) // AircraftClass +DEFINE_JUMP(VTABLE, 0x7EB110, GET_OFFSET(TechnoClass_SortY_Wrapper)) // InfantryClass +DEFINE_JUMP(VTABLE, 0x7F5D28, GET_OFFSET(TechnoClass_SortY_Wrapper)) // UnitClass + +DEFINE_JUMP(LJMP, 0x568831, 0x568841); // Skip locomotion layer check in MapClass::PickUp +DEFINE_JUMP(LJMP, 0x4D37A2, 0x4D37AE); // Skip locomotion layer check in FootClass::Mark + +DEFINE_HOOK(0x6DA3FF, TacticalClass_SelectAt_TransparentToMouse_TacticalSelectable, 0x6) +{ + enum { SkipTechno = 0x6DA440, ContinueCheck = 0x0 }; + + GET(TechnoClass*, pTechno, EAX); + + auto const pExt = TechnoExt::ExtMap.Find(pTechno); + if (pExt && pExt->ParentAttachment && pExt->ParentAttachment->GetType()->TransparentToMouse) + return SkipTechno; + + return ContinueCheck; +} + +DEFINE_HOOK(0x6DA4FB, TacticalClass_SelectAt_TransparentToMouse_OccupierPtr, 0x6) +{ + GET(CellClass*, pCell, EAX); + + ObjectClass* pFoundObject = nullptr; + for (ObjectClass* pOccupier = pCell->FirstObject; pOccupier; pOccupier = pOccupier->NextObject) + { + // find first non-transparent to mouse techno and return it + if (auto const pOccupierAsTechno = abstract_cast(pOccupier)) + { + auto const pExt = TechnoExt::ExtMap.Find(pOccupierAsTechno); + if (pExt && pExt->ParentAttachment && pExt->ParentAttachment->GetType()->TransparentToMouse) + continue; + } + + pFoundObject = pOccupier; + break; + } + + R->EAX(pFoundObject); + return 0x6DA501; +} + +// this is probably not the best way to implement sight since we may be hijacking +// into some undesirable side effects, cause this is intended for air units that +// don't run Per Cell Process function, ergo, don't update their sight - Kerbiter +DEFINE_HOOK(0x4DA6A0, FootClass_AI_CheckLocoForSight, 0x0) +{ + enum { ContinueCheck = 0x4DA6AF, NoSightUpdate = 0x4DA7B0 }; + + GET(FootClass*, pThis, ESI); + + return pThis->IsInAir() || TechnoExt::HasAttachmentLoco(pThis) + ? ContinueCheck + : NoSightUpdate; +} + +DEFINE_HOOK(0x440951, BuildingClass_Unlimbo_AttachmentsFromUpgrade, 0x6) +{ + GET(BuildingClass*, pBuilding, EDI); + GET(BuildingClass*, pUpgrade, ESI); + + TechnoExt::TransferAttachments(pUpgrade, pBuilding); + + return 0; +} diff --git a/src/Ext/Techno/Hooks.cpp b/src/Ext/Techno/Hooks.cpp index 5ecf15359d..61ada33e7b 100644 --- a/src/Ext/Techno/Hooks.cpp +++ b/src/Ext/Techno/Hooks.cpp @@ -64,6 +64,8 @@ DEFINE_HOOK(0x6F42F7, TechnoClass_Init, 0x2) { GET(TechnoClass*, pThis, ESI); + TechnoExt::InitializeAttachments(pThis); + auto const pType = pThis->GetTechnoType(); if (!pType) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index aef8425b07..d49da19efd 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -125,6 +125,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) if (!pINI->GetSection(pSection)) return; + char tempBuffer[32]; INI_EX exINI(pINI); this->HealthBar_Hide.Read(exINI, pSection, "HealthBar.Hide"); @@ -225,6 +226,43 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->AutoFire.Read(exINI, pSection, "AutoFire"); this->AutoFire_TargetSelf.Read(exINI, pSection, "AutoFire.TargetSelf"); + this->AttachmentTopLayerMinHeight.Read(exINI, pSection, "AttachmentTopLayerMinHeight"); + this->AttachmentUndergroundLayerMaxHeight.Read(exINI, pSection, "AttachmentUndergroundLayerMaxHeight"); + + // The following loop iterates over size + 1 INI entries so that the + // vector contents can be properly overriden via scenario rules - Kerbiter + for (size_t i = 0; i <= this->AttachmentData.size(); ++i) + { + NullableIdx type; + _snprintf_s(tempBuffer, sizeof(tempBuffer), "Attachment%d.Type", i); + type.Read(exINI, pSection, tempBuffer); + + if (!type.isset()) + continue; + + NullableIdx technoType; + _snprintf_s(tempBuffer, sizeof(tempBuffer), "Attachment%d.TechnoType", i); + technoType.Read(exINI, pSection, tempBuffer); + + Valueable flh; + _snprintf_s(tempBuffer, sizeof(tempBuffer), "Attachment%d.FLH", i); + flh.Read(exINI, pSection, tempBuffer); + + Valueable isOnTurret; + _snprintf_s(tempBuffer, sizeof(tempBuffer), "Attachment%d.IsOnTurret", i); + isOnTurret.Read(exINI, pSection, tempBuffer); + + Valueable rotationAdjust; + _snprintf_s(tempBuffer, sizeof(tempBuffer), "Attachment%d.RotationAdjust", i); + rotationAdjust.Read(exINI, pSection, tempBuffer); + + AttachmentDataEntry const entry { ValueableIdx(type), technoType, flh, isOnTurret, rotationAdjust }; + if (i == AttachmentData.size()) + this->AttachmentData.push_back(entry); + else + this->AttachmentData[i] = entry; + } + this->NoSecondaryWeaponFallback.Read(exINI, pSection, "NoSecondaryWeaponFallback"); this->NoSecondaryWeaponFallback_AllowAA.Read(exINI, pSection, "NoSecondaryWeaponFallback.AllowAA"); @@ -333,8 +371,6 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->NoAmmoWeapon.Read(exINI, pSection, "NoAmmoWeapon"); this->NoAmmoAmount.Read(exINI, pSection, "NoAmmoAmount"); - char tempBuffer[32]; - if (this->OwnerObject()->Gunner && this->Insignia_Weapon.empty()) { int weaponCount = this->OwnerObject()->WeaponCount; @@ -561,8 +597,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->OreGathering_Anims) .Process(this->OreGathering_Tiberiums) .Process(this->OreGathering_FramesPerDir) - .Process(this->LaserTrailData) .Process(this->DestroyAnim_Random) + .Process(this->LaserTrailData) .Process(this->NotHuman_RandomDeathSequence) .Process(this->DefaultDisguise) .Process(this->UseDisguiseMovementSpeed) @@ -688,6 +724,10 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->Wake) .Process(this->Wake_Grapple) .Process(this->Wake_Sinking) + + .Process(this->AttachmentTopLayerMinHeight) + .Process(this->AttachmentUndergroundLayerMaxHeight) + .Process(this->AttachmentData) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) @@ -702,6 +742,8 @@ void TechnoTypeExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) this->Serialize(Stm); } +#pragma region Data entry save/load + bool TechnoTypeExt::ExtData::LaserTrailDataEntry::Load(PhobosStreamReader& stm, bool registerForChange) { return this->Serialize(stm); @@ -722,6 +764,30 @@ bool TechnoTypeExt::ExtData::LaserTrailDataEntry::Serialize(T& stm) .Success(); } +bool TechnoTypeExt::ExtData::AttachmentDataEntry::Load(PhobosStreamReader& stm, bool registerForChange) +{ + return this->Serialize(stm); +} + +bool TechnoTypeExt::ExtData::AttachmentDataEntry::Save(PhobosStreamWriter& stm) const +{ + return const_cast(this)->Serialize(stm); +} + +template +bool TechnoTypeExt::ExtData::AttachmentDataEntry::Serialize(T& stm) +{ + return stm + .Process(this->Type) + .Process(this->TechnoType) + .Process(this->FLH) + .Process(this->IsOnTurret) + .Process(this->RotationAdjust) + .Success(); +} + +#pragma endregion + // ============================= // container diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index b390188549..b5430cba14 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -12,6 +12,7 @@ #include #include #include +#include class Matrix3D; @@ -233,6 +234,27 @@ class TechnoTypeExt Nullable Wake_Grapple; Nullable Wake_Sinking; + Valueable AttachmentTopLayerMinHeight; + Valueable AttachmentUndergroundLayerMaxHeight; + + struct AttachmentDataEntry + { + ValueableIdx Type; + NullableIdx TechnoType; + Valueable FLH; + Valueable IsOnTurret; + Valueable RotationAdjust; + + bool Load(PhobosStreamReader& stm, bool registerForChange); + bool Save(PhobosStreamWriter& stm) const; + + private: + template + bool Serialize(T& stm); + }; + + ValueableVector AttachmentData; + struct LaserTrailDataEntry { ValueableIdx idxType; @@ -461,6 +483,10 @@ class TechnoTypeExt , Wake { } , Wake_Grapple { } , Wake_Sinking { } + + , AttachmentTopLayerMinHeight { RulesExt::Global()->AttachmentTopLayerMinHeight } + , AttachmentUndergroundLayerMaxHeight { RulesExt::Global()->AttachmentUndergroundLayerMaxHeight } + , AttachmentData {} { } virtual ~ExtData() = default; diff --git a/src/Ext/TechnoType/Hooks.cpp b/src/Ext/TechnoType/Hooks.cpp index cfd9a1b36a..b2153055d1 100644 --- a/src/Ext/TechnoType/Hooks.cpp +++ b/src/Ext/TechnoType/Hooks.cpp @@ -188,11 +188,12 @@ DEFINE_HOOK(0x73D223, UnitClass_DrawIt_OreGath, 0x6) DEFINE_HOOK(0x700C58, TechnoClass_CanPlayerMove_NoManualMove, 0x6) { GET(TechnoClass*, pThis, ESI); + // auto const& pExt = TechnoExt::ExtMap.Find(pThis); + auto const& pTypeExt = TechnoTypeExt::ExtMap.Find(pThis->GetTechnoType()); - if (auto pExt = TechnoTypeExt::ExtMap.Find(pThis->GetTechnoType())) - return pExt->NoManualMove ? 0x700C62 : 0; + bool noMove = pTypeExt && pTypeExt->NoManualMove; - return 0; + return noMove ? 0x700C62 : 0; } // Issue #503 diff --git a/src/Ext/Unit/Hooks.DisallowMoving.cpp b/src/Ext/Unit/Hooks.DisallowMoving.cpp index 72187c7748..5b14e7d079 100644 --- a/src/Ext/Unit/Hooks.DisallowMoving.cpp +++ b/src/Ext/Unit/Hooks.DisallowMoving.cpp @@ -4,27 +4,59 @@ #include "UnitClass.h" #include +#include + #include +#include DEFINE_HOOK(0x740A93, UnitClass_Mission_Move_DisallowMoving, 0x6) { + enum { QueueGuardInstead = 0x740AEF, ReturnTrue = 0x740AFD, ContinueCheck = 0x0 }; + GET(UnitClass*, pThis, ESI); - return pThis->Type->Speed == 0 ? 0x740AEF : 0; + if (TechnoExt::HasAttachmentLoco(pThis)) + { + auto const pExt = TechnoExt::ExtMap.Find(pThis); + if (pExt && pExt->ParentAttachment) + { + auto const& pParent = pExt->ParentAttachment->Parent; + if (pThis->PlanningToken && pThis->PlanningToken->PlanningNodes.Count + && pParent->PlanningToken && pParent->PlanningToken->PlanningNodes.Count + && pThis->PlanningToken->PlanningNodes[0] == pParent->PlanningToken->PlanningNodes[0]) + { + return ReturnTrue; + } + } + pThis->EnterIdleMode(false, true); + return ReturnTrue; + } + + // skips this->IsHarvesting = 0, may backfire somewhere - Kerbiter + return pThis->Type->Speed == 0 + ? QueueGuardInstead + : ContinueCheck; } DEFINE_HOOK(0x741AA7, UnitClass_Assign_Destination_DisallowMoving, 0x6) { + enum { ClearNavComsAndReturn = 0x743173, ContinueCheck = 0x0 }; + GET(UnitClass*, pThis, EBP); - return pThis->Type->Speed == 0 ? 0x743173 : 0; + return pThis->Type->Speed == 0 || TechnoExt::HasAttachmentLoco(pThis) + ? ClearNavComsAndReturn + : ContinueCheck; } DEFINE_HOOK(0x743B4B, UnitClass_Scatter_DisallowMoving, 0x6) { + enum { ReleaseReturn = 0x74408E, ContinueCheck = 0x0 }; + GET(UnitClass*, pThis, EBP); - return pThis->Type->Speed == 0 ? 0x74408E : 0; + return pThis->Type->Speed == 0 || TechnoExt::HasAttachmentLoco(pThis) + ? ReleaseReturn : ContinueCheck; } DEFINE_HOOK(0x74038F, UnitClass_What_Action_ObjectClass_DisallowMoving_1, 0x6) @@ -68,11 +100,13 @@ DEFINE_HOOK(0x740744, UnitClass_What_Action_DisallowMoving_2, 0x6) return 0; } +// Makes the vehicle keep it's current turret heading without snapping back to neutral position DEFINE_HOOK(0x736B60, UnitClass_Rotation_AI_DisallowMoving, 0x6) { GET(UnitClass*, pThis, ESI); - return pThis->Type->Speed == 0 ? 0x736AFB : 0; + return pThis->Type->Speed == 0 + ? 0x736AFB : 0; } DEFINE_HOOK(0x73891D, UnitClass_Active_Click_With_DisallowMoving, 0x6) diff --git a/src/Ext/WeaponType/Body.cpp b/src/Ext/WeaponType/Body.cpp index ad123810c6..51215c766a 100644 --- a/src/Ext/WeaponType/Body.cpp +++ b/src/Ext/WeaponType/Body.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include + WeaponTypeExt::ExtContainer WeaponTypeExt::ExtMap; bool WeaponTypeExt::ExtData::HasRequiredAttachedEffects(TechnoClass* pTarget, TechnoClass* pFirer) const diff --git a/src/Locomotion/AttachmentLocomotionClass.cpp b/src/Locomotion/AttachmentLocomotionClass.cpp new file mode 100644 index 0000000000..ca68de32ad --- /dev/null +++ b/src/Locomotion/AttachmentLocomotionClass.cpp @@ -0,0 +1,378 @@ +#include "AttachmentLocomotionClass.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +// TODO maybe some macros for repeated parent function calls? + +bool AttachmentLocomotionClass::Is_Moving() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco && pParentLoco->Is_Moving(); +} + +Matrix3D AttachmentLocomotionClass::Draw_Matrix(VoxelIndexKey* key) +{ + if (auto const pParentFoot = abstract_cast(this->GetAttachmentParent())) + { + Matrix3D mtx = pParentFoot->Locomotor->Draw_Matrix(key); + + // adjust for the real facing which is the source of truth for hor. rotation + double childRotation = this->LinkedTo->PrimaryFacing.Current().GetRadian<32>(); + double parentRotation = pParentFoot->PrimaryFacing.Current().GetRadian<32>(); + float adjustmentAngle = (float)(childRotation - parentRotation); + + mtx.RotateZ(adjustmentAngle); + + if (key && key->Is_Valid_Key()) + key->MainVoxel.FrameIndex = this->LinkedTo->PrimaryFacing.Current().GetFacing<32>(); + + return mtx; + } + + return LocomotionClass::Draw_Matrix(key); +} + +// Shadow drawing works acceptable as is. It draws separate units as normal. +// A possibly better solution would be to actually "merge" the shadows +// and draw them as a single one, but this needs calculating the extension +// of the parent slope plane to calculate the correct offset for Shadow_Point, +// complicated trigonometry that would be a waste of time at this point. + +// If you want to work on this - Shadow_Matrix should be fine to copy from Draw_Matrix, +// (even the key shenanigans can be left the same), butShadow_Point would need to be +// calculated using height from the ramp extension plane - Kerbiter + +Point2D AttachmentLocomotionClass::Draw_Point() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + ? pParentLoco->Draw_Point() + : LocomotionClass::Draw_Point(); +} + +VisualType AttachmentLocomotionClass::Visual_Character(bool raw) +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + ? pParentLoco->Visual_Character(raw) + : LocomotionClass::Visual_Character(raw); +} + +int AttachmentLocomotionClass::Z_Adjust() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + ? pParentLoco->Z_Adjust() + : LocomotionClass::Z_Adjust(); +} + +ZGradient AttachmentLocomotionClass::Z_Gradient() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + ? pParentLoco->Z_Gradient() + : LocomotionClass::Z_Gradient(); +} + +bool AttachmentLocomotionClass::Process() +{ + if (this->LinkedTo->IsAlive) + { + Layer newLayer = this->In_Which_Layer(); + Layer oldLayer = this->PreviousLayer; + + bool changedAirborneStatus = false; + + if (oldLayer != newLayer) + { + DisplayClass::Instance->Submit(this->LinkedTo); + + if (oldLayer < Layer::Air && Layer::Air <= newLayer) + { + AircraftTrackerClass::Instance->Add(this->LinkedTo); + changedAirborneStatus = true; + } + else if (newLayer < Layer::Air && Layer::Air <= oldLayer) + { + AircraftTrackerClass::Instance->Remove(this->LinkedTo); + changedAirborneStatus = true; + } + + this->PreviousLayer = newLayer; + } + + CellStruct oldPos = this->PreviousCell; + CellStruct newPos = this->LinkedTo->GetMapCoords(); + + if (oldPos != newPos) + { + if (Layer::Air <= newLayer && !changedAirborneStatus) + AircraftTrackerClass::Instance->Update(this->LinkedTo, oldPos, newPos); + + if (this->LinkedTo->GetTechnoType()->SensorsSight) + { + this->LinkedTo->RemoveSensorsAt(oldPos); + this->LinkedTo->AddSensorsAt(newPos); + } + } + + this->PreviousCell = newPos; + } + + // TODO sensors and sight + + AttachmentClass* pAttachment = this->GetAttachment(); + if (pAttachment && pAttachment->GetType()->InheritHeightStatus) + { + this->LinkedTo->OnBridge = pAttachment->Parent->OnBridge; + } + else + { + this->LinkedTo->OnBridge = false; // GetHeight returns different height depending on this + this->LinkedTo->OnBridge = this->ShouldBeOnBridge(); + } + + return LocomotionClass::Process(); +} + +// I am not sure this does anything and could probably be removed +bool AttachmentLocomotionClass::Is_Powered() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + && pParentLoco->Is_Powered(); +} + +bool AttachmentLocomotionClass::Is_Ion_Sensitive() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + && pParentLoco->Is_Ion_Sensitive() + || LocomotionClass::Is_Ion_Sensitive(); +} + +Layer AttachmentLocomotionClass::In_Which_Layer() +{ + AttachmentClass* pAttachment = this->GetAttachment(); + if (!pAttachment || !pAttachment->GetType()->InheritHeightStatus) + return this->CalculateLayer(); + + auto const pParentAsFoot = abstract_cast(pAttachment->Parent); + return pParentAsFoot && pParentAsFoot->Locomotor + ? pParentAsFoot->Locomotor->In_Which_Layer() + : this->CalculateLayer(); +} + +bool AttachmentLocomotionClass::Is_Moving_Now() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco && pParentLoco->Is_Moving_Now(); +} + +int AttachmentLocomotionClass::Apparent_Speed() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + ? pParentLoco->Apparent_Speed() + : LocomotionClass::Apparent_Speed(); +} + +FireError AttachmentLocomotionClass::Can_Fire() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + ? pParentLoco->Can_Fire() + : LocomotionClass::Can_Fire(); +} + +int AttachmentLocomotionClass::Get_Status() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + ? pParentLoco->Get_Status() + : LocomotionClass::Get_Status(); +} + +bool AttachmentLocomotionClass::Is_Surfacing() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco + && pParentLoco->Is_Surfacing() + || LocomotionClass::Is_Surfacing(); +} + +bool AttachmentLocomotionClass::Is_Really_Moving_Now() +{ + ILocomotionPtr pParentLoco = this->GetAttachmentParentLoco(); + return pParentLoco && pParentLoco->Is_Really_Moving_Now(); +} + +void AttachmentLocomotionClass::Limbo() +{ + this->PreviousLayer = Layer::None; + this->PreviousCell = CellStruct::Empty; + // AircraftTracker is handled by FootClass::Limbo +} + +HRESULT AttachmentLocomotionClass::Begin_Piggyback(ILocomotion* pointer) +{ + if (!pointer) + return E_POINTER; + + if (this->Piggybacker) + return E_FAIL; + + // since LinkedTo may've been managed by AircraftTracker before we need to remove the AircraftTracker entry + if (this->LinkedTo && this->LinkedTo->GetLastFlightMapCoords() != CellStruct::Empty) + AircraftTrackerClass::Instance->Remove(this->LinkedTo); + + this->Piggybacker = pointer; + + return S_OK; +} + +HRESULT AttachmentLocomotionClass::End_Piggyback(ILocomotion** pointer) +{ + if (!pointer) + return E_POINTER; + + if (!this->Piggybacker) + return S_FALSE; + + // since LinkedTo may no longer be considered airborne we need to remove the AircraftTracker entry + if (this->LinkedTo && this->LinkedTo->GetLastFlightMapCoords() != CellStruct::Empty) + AircraftTrackerClass::Instance->Remove(this->LinkedTo); + + // since pointer is a dumb pointer, we don't need to call Release, + // hence we use Detach, otherwise the locomotor gets trashed + *pointer = this->Piggybacker.Detach(); + + // in order to play nice with IsLocomotor warheads probably also should + // handle IsAttackedByLocomotor etc. warheads here, but none of the vanilla + // warheads do this (except JumpjetLocomotionClass::End_Piggyback) + + return S_OK; +} + +bool AttachmentLocomotionClass::Is_Ok_To_End() +{ + // Actually a confusing name, should return true only if the piggybacking should be ended. + return this->Piggybacker + && !this->GetAttachmentParent(); +} + +HRESULT AttachmentLocomotionClass::Piggyback_CLSID(GUID* classid) +{ + HRESULT hr; + + if (classid == nullptr) + return E_POINTER; + + if (this->Piggybacker) + { + IPersistStreamPtr piggyAsPersist(this->Piggybacker); + + hr = piggyAsPersist->GetClassID(classid); + } + else + { + if (reinterpret_cast(this) == nullptr) + return E_FAIL; + + IPersistStreamPtr thisAsPersist(this); + + if (thisAsPersist == nullptr) + return E_FAIL; + + hr = thisAsPersist->GetClassID(classid); + } + + return hr; +} + +bool AttachmentLocomotionClass::Is_Piggybacking() +{ + return this->Piggybacker != nullptr; +} + +// non-virtuals + +AttachmentClass* AttachmentLocomotionClass::GetAttachment() +{ + AttachmentClass* result = nullptr; + + if (this->LinkedTo) + { + if (auto const pExt = TechnoExt::ExtMap.Find(this->LinkedTo)) + result = pExt->ParentAttachment; + } + + return result; +} + +TechnoClass* AttachmentLocomotionClass::GetAttachmentParent() +{ + TechnoClass* result = nullptr; + + if (auto const pAttachment = this->GetAttachment()) + result = pAttachment->Parent; + + return result; +} + +ILocomotionPtr AttachmentLocomotionClass::GetAttachmentParentLoco() +{ + ILocomotionPtr result { }; + + if (auto const pTechno = this->GetAttachmentParent()) + { + if (auto const pFoot = abstract_cast(pTechno)) + result = pFoot->Locomotor; + } + + return result; +} + +Layer AttachmentLocomotionClass::CalculateLayer() +{ + auto const pExt = TechnoTypeExt::ExtMap.Find(this->LinkedTo->GetTechnoType()); + int height = this->LinkedTo->GetHeight(); + + if (this->LinkedTo->IsInAir()) + { + if (!this->LinkedTo->OnBridge && this->ShouldBeOnBridge()) + height -= CellClass::BridgeHeight; + + return height >= pExt->AttachmentTopLayerMinHeight + ? Layer::Top : Layer::Air; + } + else if (this->LinkedTo->IsOnFloor()) + { + return height <= pExt->AttachmentUndergroundLayerMaxHeight + ? Layer::Underground : Layer::Ground; + } + + return Layer::None; +} + +bool AttachmentLocomotionClass::ShouldBeOnBridge() +{ + return MapClass::Instance->GetCellAt(this->LinkedTo->Location)->ContainsBridge() + && this->LinkedTo->GetHeight() >= CellClass::BridgeHeight && !this->LinkedTo->IsFallingDown; +} diff --git a/src/Locomotion/AttachmentLocomotionClass.h b/src/Locomotion/AttachmentLocomotionClass.h new file mode 100644 index 0000000000..bf1f06dd6b --- /dev/null +++ b/src/Locomotion/AttachmentLocomotionClass.h @@ -0,0 +1,215 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include +#include + +class AttachmentClass; + + +class __declspec(uuid("C5D54B98-8C98-4275-8CE4-EF75CB0CBE3E")) + AttachmentLocomotionClass : public LocomotionClass + , public IPiggyback +{ +public: + + //IUnknown + virtual HRESULT __stdcall QueryInterface(REFIID iid, LPVOID* ppvObject) + { + HRESULT hr = LocomotionClass::QueryInterface(iid, ppvObject); + if (hr != E_NOINTERFACE) + return hr; + + if (iid == __uuidof(IPiggyback)) + { + *ppvObject = static_cast(this); + this->AddRef(); + return S_OK; + } + + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + virtual ULONG __stdcall AddRef() { return LocomotionClass::AddRef(); } + virtual ULONG __stdcall Release() { return LocomotionClass::Release(); } + + //IPersist + virtual HRESULT __stdcall GetClassID(CLSID* pClassID) + { + if (pClassID == nullptr) + return E_POINTER; + + *pClassID = __uuidof(this); + + return S_OK; + } + + //IPersistStream + // virtual HRESULT __stdcall IsDirty() override; + + virtual HRESULT __stdcall Load(IStream* pStm) + { + // This loads the whole object + HRESULT hr = LocomotionClass::Load(pStm); + if (FAILED(hr)) + return hr; + + if (this) + { + this->Piggybacker.Detach(); + // this reconstructs the object in-place, no-init constructor just refreshes + // the virtual function table pointers because most likely they will + // point to incorrect place due to different base address or code changes + new (this) AttachmentLocomotionClass(noinit_t()); + } + + bool piggybackerPresent; + hr = pStm->Read(&piggybackerPresent, sizeof(piggybackerPresent), nullptr); + if (!piggybackerPresent) + return hr; + + hr = OleLoadFromStream(pStm, __uuidof(ILocomotion), reinterpret_cast(&this->Piggybacker)); + return hr; + } + + virtual HRESULT __stdcall Save(IStream* pStm, BOOL fClearDirty) + { + // This saves the whole object + HRESULT hr = LocomotionClass::Save(pStm, fClearDirty); + if (FAILED(hr)) + return hr; + + // Piggybacker handling + bool piggybackerPresent = this->Piggybacker != nullptr; + hr = pStm->Write(&piggybackerPresent, sizeof(piggybackerPresent), nullptr); + + if (!piggybackerPresent) + return hr; + + IPersistStreamPtr piggyPersist(this->Piggybacker); + hr = OleSaveToStream(piggyPersist, pStm); + return hr; + } + + // virtual HRESULT __stdcall GetSizeMax(ULARGE_INTEGER* pcbSize) + // { + // if (pcbSize == nullptr) + // return E_POINTER; + + // return LocomotionClass::GetSizeMax(pcbSize); + // } + + // virtual HRESULT __stdcall Link_To_Object(void* pointer) override + // { + // HRESULT hr = LocomotionClass::Link_To_Object(pointer); + + // if (SUCCEEDED(hr)) + // Debug::Log("AttachmentLocomotionClass - Sucessfully linked to \"%s\"\n", Owner->get_ID()); + + // return hr; + // } + + virtual bool __stdcall Is_Moving() override; + // virtual CoordStruct __stdcall Destination() override; + // virtual CoordStruct __stdcall Head_To_Coord() override; + // virtual Move __stdcall Can_Enter_Cell(CellStruct cell) override; + //virtual bool __stdcall Is_To_Have_Shadow() override; + virtual Matrix3D __stdcall Draw_Matrix(VoxelIndexKey* key) override; + // virtual Matrix3D __stdcall Shadow_Matrix(VoxelIndexKey* key) override; + virtual Point2D __stdcall Draw_Point() override; + // virtual Point2D __stdcall Shadow_Point() override; + virtual VisualType __stdcall Visual_Character(bool raw) override; + virtual int __stdcall Z_Adjust() override; + virtual ZGradient __stdcall Z_Gradient() override; + virtual bool __stdcall Process() override; + // virtual void __stdcall Move_To(CoordStruct to) override; + // virtual void __stdcall Stop_Moving() override; + // virtual void __stdcall Do_Turn(DirStruct coord) override; + // virtual void __stdcall Unlimbo() override; + //virtual void __stdcall Tilt_Pitch_AI() override; + //virtual bool __stdcall Power_On() override; + //virtual bool __stdcall Power_Off() override; + virtual bool __stdcall Is_Powered() override; + virtual bool __stdcall Is_Ion_Sensitive() override; + //virtual bool __stdcall Push(DirStruct dir) override; + //virtual bool __stdcall Shove(DirStruct dir) override; + //virtual void __stdcall Force_Track(int track, CoordStruct coord) override; + virtual Layer __stdcall In_Which_Layer() override; + //virtual void __stdcall Force_Immediate_Destination(CoordStruct coord) override; + //virtual void __stdcall Force_New_Slope(int ramp) override; + virtual bool __stdcall Is_Moving_Now() override; + virtual int __stdcall Apparent_Speed() override; + //virtual int __stdcall Drawing_Code() override; + virtual FireError __stdcall Can_Fire() override; + virtual int __stdcall Get_Status() override; + //virtual void __stdcall Acquire_Hunter_Seeker_Target() override; + virtual bool __stdcall Is_Surfacing() override; + // virtual void __stdcall Mark_All_Occupation_Bits(MarkType mark) override; + // virtual bool __stdcall Is_Moving_Here(CoordStruct to) override; + //virtual bool __stdcall Will_Jump_Tracks() override; + virtual bool __stdcall Is_Really_Moving_Now() override; + //virtual void __stdcall Stop_Movement_Animation() override; + virtual void __stdcall Limbo() override; + //virtual void __stdcall Lock() override; + //virtual void __stdcall Unlock() override; + //virtual int __stdcall Get_Track_Number() override; + //virtual int __stdcall Get_Track_Index() override; + //virtual int __stdcall Get_Speed_Accum() override; + + //IPiggy + virtual HRESULT __stdcall Begin_Piggyback(ILocomotion* pointer) override; + virtual HRESULT __stdcall End_Piggyback(ILocomotion** pointer) override; + virtual bool __stdcall Is_Ok_To_End() override; + virtual HRESULT __stdcall Piggyback_CLSID(GUID* classid) override; + virtual bool __stdcall Is_Piggybacking() override; + +private: + // Shortcut to attachment the LinkedTo is attached to. + AttachmentClass* GetAttachment(); + + // Shortcut to parent techno of this locomotor's owner. + TechnoClass* GetAttachmentParent(); + + // Shortcut to parent techno of this locomotor's owner. + ILocomotionPtr GetAttachmentParentLoco(); + + // Non-parent layer calculation. + Layer CalculateLayer(); + + // Should the LinkedTo be on bridge (when it's currently not)? + // (yoinked from JumpjetLocomotionClass::In_Which_Layer) + bool ShouldBeOnBridge(); + +public: + inline AttachmentLocomotionClass() : LocomotionClass { } + , PreviousLayer { Layer::None } + , PreviousCell { CellStruct::Empty } + , Piggybacker { nullptr } + { } + + inline AttachmentLocomotionClass(noinit_t) : LocomotionClass { noinit_t() } { } + + inline virtual ~AttachmentLocomotionClass() override = default; + virtual int Size() override { return sizeof(*this); } + +public: + // The layer this locomotor's user was in previously. + // Used for resubmitting the FootClass to another layer. + Layer PreviousLayer; + + // The cell this locomotor's user was in previously. + // Used for tracking the FootClass while it's in air. + CellStruct PreviousCell; + + // The piggybacking locomotor. + ILocomotionPtr Piggybacker; +}; + diff --git a/src/Misc/Hooks.AI.cpp b/src/Misc/Hooks.AI.cpp new file mode 100644 index 0000000000..aecb10ca8f --- /dev/null +++ b/src/Misc/Hooks.AI.cpp @@ -0,0 +1,11 @@ +#include + +#include + +DEFINE_HOOK(0x55B6B3, LogicClass_AI_After, 0x5) +{ + for (auto const& attachment : AttachmentClass::Array) + attachment->AI(); + + return 0; +} diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index 0760e4d0f7..ea61179bff 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include diff --git a/src/Misc/Hooks.INIInheritance.cpp b/src/Misc/Hooks.INIInheritance.cpp index 0834a036f9..5fd5ec8c06 100644 --- a/src/Misc/Hooks.INIInheritance.cpp +++ b/src/Misc/Hooks.INIInheritance.cpp @@ -297,7 +297,7 @@ DEFINE_HOOK(0x527920, INIClass_ReadGUID_Overwrite, 0x5) // locomotor } // Fix issue with TilesInSet caused by incorrect vanilla INIs and the fixed parser returning correct default value (-1) instead of 0 for existing non-integer values -int __fastcall IsometricTileTypeClass_ReadINI_TilesInSet_Wrapper(INIClass* pThis, void* _, const char* pSection, const char* pKey, int defaultValue) +int __fastcall IsometricTileTypeClass_ReadINI_TilesInSet_Wrapper(INIClass* pThis, discard_t _, const char* pSection, const char* pKey, int defaultValue) { if (pThis->Exists(pSection, pKey)) return pThis->ReadInteger(pSection, pKey, 0); diff --git a/src/Misc/Selection.cpp b/src/Misc/Selection.cpp index d69bbc58dd..6b648da4b7 100644 --- a/src/Misc/Selection.cpp +++ b/src/Misc/Selection.cpp @@ -1,6 +1,10 @@ #include "Phobos.h" -#include "Utilities/Macro.h" -#include "Ext/TechnoType/Body.h" +#include + +#include +#include +#include +#include #include #include @@ -47,10 +51,8 @@ class ExtSelection int nLocalX = selectable.X - pThis->TacticalPos.X; int nLocalY = selectable.Y - pThis->TacticalPos.Y; - if ((nLocalX >= pRect->Left && nLocalX < pRect->Right + pRect->Left) && - (nLocalY >= pRect->Top && nLocalY < pRect->Bottom + pRect->Top)) { - return true; - } + return (nLocalX >= pRect->Left && nLocalX < pRect->Right + pRect->Left) && + (nLocalY >= pRect->Top && nLocalY < pRect->Bottom + pRect->Top); } return false; } @@ -58,15 +60,23 @@ class ExtSelection static bool Tactical_IsHighPriorityInRect(TacticalClass* pThis, LTRBStruct* rect) { for (const auto& selected : Array) + { if (Tactical_IsInSelectionRect(pThis, rect, selected) && ObjectClass_IsSelectable(selected.Techno)) - if (!TechnoTypeExt::ExtMap.Find(selected.Techno->GetTechnoType())->LowSelectionPriority) + { + auto const& pExt = TechnoExt::ExtMap.Find(selected.Techno); + auto const& pTypeExt = TechnoTypeExt::ExtMap.Find(selected.Techno->GetTechnoType()); + + bool isLowPriorityByAttachment = pExt->ParentAttachment && pExt->ParentAttachment->GetType()->LowSelectionPriority; + if (!pTypeExt->LowSelectionPriority && !isLowPriorityByAttachment) return true; + } + } return false; } static // Reversed from Tactical::Select - void Tactical_SelectFiltered(TacticalClass* pThis, LTRBStruct* pRect, callback_type check_callback, bool bPriorityFiltering) + void Tactical_SelectFiltered(TacticalClass* pThis, LTRBStruct* pRect, callback_type fpCheckCallback, bool bFilter) { Unsorted::MoveFeedback = true; @@ -74,23 +84,33 @@ class ExtSelection return; for (const auto& selected : Array) + { if (Tactical_IsInSelectionRect(pThis, pRect, selected)) { - const auto pTechno = selected.Techno; - auto pTechnoType = pTechno->GetTechnoType(); - auto TypeExt = TechnoTypeExt::ExtMap.Find(pTechnoType); + auto const& pTechno = selected.Techno; + auto const& pExt = TechnoExt::ExtMap.Find(pTechno); + auto const& pTechnoType = pTechno->GetTechnoType(); + auto const& pTypeExt = TechnoTypeExt::ExtMap.Find(pTechnoType); + + // Attached units shouldn't be selected regardless of the setting + bool isLowPriorityByAttachment = pExt && pExt->ParentAttachment && pExt->ParentAttachment->GetType()->LowSelectionPriority; + bool isLowPriorityByTechno = Phobos::Config::PrioritySelectionFiltering && pTypeExt && pTypeExt->LowSelectionPriority; - if (bPriorityFiltering && TypeExt && TypeExt->LowSelectionPriority) + if (bFilter && (isLowPriorityByAttachment || isLowPriorityByTechno)) continue; - if (TypeExt && Game::IsTypeSelecting()) - Game::UICommands_TypeSelect_7327D0(TypeExt->GetSelectionGroupID()); - else if (check_callback) - (*check_callback)(pTechno); + if (pTypeExt && Game::IsTypeSelecting()) + { + Game::UICommands_TypeSelect_7327D0(pTypeExt->GetSelectionGroupID()); + } + else if (fpCheckCallback) + { + (*fpCheckCallback)(pTechno); + } else { - const auto pBldType = abstract_cast(pTechnoType); - const auto pOwner = pTechno->GetOwningHouse(); + const auto& pBldType = abstract_cast(pTechnoType); + const auto& pOwner = pTechno->GetOwningHouse(); if (pOwner && pOwner->IsControlledByCurrentPlayer() && pTechno->CanBeSelected() && (!pBldType || (pBldType && pBldType->UndeploysInto && pBldType->IsVehicle()))) @@ -99,12 +119,13 @@ class ExtSelection } } } + } Unsorted::MoveFeedback = true; } static // Reversed from Tactical::MakeSelection - void __fastcall Tactical_MakeFilteredSelection(TacticalClass* pThis, void*_, callback_type check_callback) + void __fastcall Tactical_MakeFilteredSelection(TacticalClass* pThis, void*_, callback_type fpCheckCallback) { if (pThis->Band.Left || pThis->Band.Top) { int nLeft = pThis->Band.Left; @@ -119,8 +140,8 @@ class ExtSelection LTRBStruct rect { nLeft , nTop, nRight - nLeft + 1, nBottom - nTop + 1 }; - bool bPriorityFiltering = Phobos::Config::PrioritySelectionFiltering && Tactical_IsHighPriorityInRect(pThis, &rect); - Tactical_SelectFiltered(pThis, &rect, check_callback, bPriorityFiltering); + Tactical_SelectFiltered(pThis, &rect, fpCheckCallback, + Tactical_IsHighPriorityInRect(pThis, &rect)); pThis->Band.Left = 0; pThis->Band.Top = 0; diff --git a/src/New/Entity/AttachmentClass.cpp b/src/New/Entity/AttachmentClass.cpp new file mode 100644 index 0000000000..69d1c0cc4c --- /dev/null +++ b/src/New/Entity/AttachmentClass.cpp @@ -0,0 +1,311 @@ +#include "AttachmentClass.h" + +#include +#include +#include +#include + +#include + +#include +#include + +std::vector AttachmentClass::Array; + +AttachmentTypeClass* AttachmentClass::GetType() +{ + return AttachmentTypeClass::Array[this->Data->Type].get(); +} + +TechnoTypeClass* AttachmentClass::GetChildType() +{ + return this->Data->TechnoType.isset() + ? TechnoTypeClass::Array()->GetItem(this->Data->TechnoType) + : nullptr; +} + +CoordStruct AttachmentClass::GetChildLocation() +{ + auto& flh = this->Data->FLH.Get(); + return TechnoExt::GetFLHAbsoluteCoords(this->Parent, flh, this->Data->IsOnTurret); +} + +AttachmentClass::~AttachmentClass() +{ + // clean up non-owning references + if (this->Child) + { + auto const& pChildExt = TechnoExt::ExtMap.Find(Child); + pChildExt->ParentAttachment = nullptr; + } + + auto position = std::find(Array.begin(), Array.end(), this); + if (position != Array.end()) + Array.erase(position); +} + +void AttachmentClass::Initialize() +{ + if (this->Child) + return; + + if (this->GetType()->RespawnAtCreation) + this->CreateChild(); +} + +void AttachmentClass::CreateChild() +{ + if (auto const pChildType = this->GetChildType()) + { + if (pChildType->WhatAmI() != AbstractType::UnitType) + return; + + if (const auto pTechno = static_cast(pChildType->CreateObject(this->Parent->Owner))) + { + this->AttachChild(pTechno); + } + else + { + Debug::Log("[" __FUNCTION__ "] Failed to create child %s of parent %s!\n", + pChildType->ID, this->Parent->GetTechnoType()->ID); + } + } +} + +void AttachmentClass::AI() +{ + AttachmentTypeClass* pType = this->GetType(); + + if (!this->Child) + { + if (pType->RespawnDelay == 0) + { + this->CreateChild(); + } + else if (pType->RespawnDelay > 0) + { + if (!this->RespawnTimer.HasStarted()) + { + this->RespawnTimer.Start(pType->RespawnDelay); + } + else if (this->RespawnTimer.Completed()) + { + this->CreateChild(); + this->RespawnTimer.Stop(); + } + } + } + + if (this->Child) + { + if (this->Child->InLimbo && !this->Parent->InLimbo) + this->Unlimbo(); + else if (!this->Child->InLimbo && this->Parent->InLimbo) + this->Limbo(); + + this->Child->SetLocation(this->GetChildLocation()); + + DirStruct childDir = this->Data->IsOnTurret + ? this->Parent->SecondaryFacing.Current() : this->Parent->PrimaryFacing.Current(); + + childDir.Raw += DirStruct(this->Data->RotationAdjust).Raw; // overflow = free modulo for rotation + + this->Child->PrimaryFacing.SetCurrent(childDir); + // TODO handle secondary facing in case the turret is idle + + FootClass* pParentAsFoot = abstract_cast(this->Parent); + FootClass* pChildAsFoot = abstract_cast(this->Child); + if (pParentAsFoot && pChildAsFoot) + { + pChildAsFoot->TubeIndex = pParentAsFoot->TubeIndex; + } + + if (pType->InheritStateEffects) + { + this->Child->IsFallingDown = this->Parent->IsFallingDown; + this->Child->WasFallingDown = this->Parent->WasFallingDown; + this->Child->CloakState = this->Parent->CloakState; + this->Child->WarpingOut = this->Parent->WarpingOut; + this->Child->unknown_280 = this->Parent->unknown_280; // sth related to teleport + this->Child->BeingWarpedOut = this->Parent->BeingWarpedOut; + this->Child->Deactivated = this->Parent->Deactivated; + this->Child->Flash(this->Parent->Flashing.DurationRemaining); + + this->Child->IronCurtainTimer = this->Parent->IronCurtainTimer; + this->Child->IdleActionTimer = this->Parent->IdleActionTimer; + this->Child->IronTintTimer = this->Parent->IronTintTimer; + this->Child->CloakDelayTimer = this->Parent->CloakDelayTimer; + this->Child->ChronoLockRemaining = this->Parent->ChronoLockRemaining; + this->Child->Berzerk = this->Parent->Berzerk; + this->Child->BerzerkDurationLeft = this->Parent->BerzerkDurationLeft; + this->Child->ChronoWarpedByHouse = this->Parent->ChronoWarpedByHouse; + this->Child->EMPLockRemaining = this->Parent->EMPLockRemaining; + this->Child->ShouldLoseTargetNow = this->Parent->ShouldLoseTargetNow; + } + + if (pType->InheritOwner) + this->Child->SetOwningHouse(this->Parent->GetOwningHouse(), false); + } +} + +// Called in Kill_Cargo, handles logics for parent destruction on children +void AttachmentClass::Destroy(TechnoClass* pSource) +{ + if (this->Child) + { + auto const pChildExt = TechnoExt::ExtMap.Find(this->Child); + pChildExt->ParentAttachment = nullptr; + + auto pType = this->GetType(); + + if (pType->DestructionWeapon_Child.isset()) + TechnoExt::FireWeaponAtSelf(this->Child, pType->DestructionWeapon_Child); + + if (pType->InheritDestruction && this->Child) + TechnoExt::Kill(this->Child, pSource); + else if (!this->Child->InLimbo && pType->ParentDestructionMission.isset()) + this->Child->QueueMission(pType->ParentDestructionMission.Get(), false); + + this->Child = nullptr; + } +} + +void AttachmentClass::ChildDestroyed() +{ + if (this->Child) + { + if (auto const pChildExt = TechnoExt::ExtMap.Find(this->Child)) + pChildExt->ParentAttachment = nullptr; + + AttachmentTypeClass* pType = this->GetType(); + if (pType->DestructionWeapon_Parent.isset()) + TechnoExt::FireWeaponAtSelf(this->Parent, pType->DestructionWeapon_Parent); + + this->Child = nullptr; + } +} + +void AttachmentClass::Unlimbo() +{ + if (this->Child) + { + CoordStruct childCoord = TechnoExt::GetFLHAbsoluteCoords( + this->Parent, this->Data->FLH, this->Data->IsOnTurret); + + DirStruct childDir = this->Data->IsOnTurret + ? this->Parent->SecondaryFacing.Current() : this->Parent->PrimaryFacing.Current(); + + childDir.Raw += DirStruct(this->Data->RotationAdjust).Raw; // overflow = free modulo for rotation + + ++Unsorted::IKnowWhatImDoing; + this->Child->Unlimbo(childCoord, childDir.GetDir()); + --Unsorted::IKnowWhatImDoing; + } +} + +void AttachmentClass::Limbo() +{ + if (this->Child) + this->Child->Limbo(); +} + +bool AttachmentClass::AttachChild(TechnoClass* pChild) +{ + if (this->Child) + return false; + + if (pChild->WhatAmI() != AbstractType::Unit) + return false; + + if (auto const pChildAsFoot = abstract_cast(pChild)) + { + if (IPersistPtr pLocoPersist = pChildAsFoot->Locomotor) + { + CLSID locoCLSID { }; + if (SUCCEEDED(pLocoPersist->GetClassID(&locoCLSID)) + && locoCLSID != __uuidof(AttachmentLocomotionClass)) + { + LocomotionClass::ChangeLocomotorTo(pChildAsFoot, + __uuidof(AttachmentLocomotionClass)); + } + } + } + + this->Child = pChild; + + auto pChildExt = TechnoExt::ExtMap.Find(this->Child); + pChildExt->ParentAttachment = this; + + // bandaid for jitterless drawing. TODO fix properly + // this->Child->GetTechnoType()->DisableVoxelCache = true; + // this->Child->GetTechnoType()->DisableShadowCache = true; + + AttachmentTypeClass* pType = this->GetType(); + + if (pType->InheritOwner) + { + if (auto pController = this->Child->MindControlledBy) + pController->CaptureManager->FreeUnit(this->Child); + } + + return true; +} + +bool AttachmentClass::DetachChild() +{ + if (this->Child) + { + AttachmentTypeClass* pType = this->GetType(); + + if (!this->Child->InLimbo && pType->ParentDetachmentMission.isset()) + this->Child->QueueMission(pType->ParentDetachmentMission.Get(), false); + + // FIXME this won't work probably + if (pType->InheritOwner) + this->Child->SetOwningHouse(this->Parent->GetOriginalOwner(), false); + + // remove the attachment locomotor manually just to be safe + if (auto const pChildAsFoot = abstract_cast(this->Child)) + LocomotionClass::End_Piggyback(pChildAsFoot->Locomotor); + + auto pChildExt = TechnoExt::ExtMap.Find(this->Child); + pChildExt->ParentAttachment = nullptr; + this->Child = nullptr; + + return true; + } + + return false; +} + + +void AttachmentClass::InvalidatePointer(void* ptr) +{ + AnnounceInvalidPointer(this->Parent, ptr); + AnnounceInvalidPointer(this->Child, ptr); +} + +#pragma region Save/Load + +template +bool AttachmentClass::Serialize(T& stm) +{ + return stm + .Process(this->Data) + .Process(this->Parent) + .Process(this->Child) + .Process(this->RespawnTimer) + .Success(); +} + +bool AttachmentClass::Load(PhobosStreamReader& stm, bool RegisterForChange) +{ + return Serialize(stm); +} + +bool AttachmentClass::Save(PhobosStreamWriter& stm) const +{ + return const_cast(this)->Serialize(stm); +} + +#pragma endregion diff --git a/src/New/Entity/AttachmentClass.h b/src/New/Entity/AttachmentClass.h new file mode 100644 index 0000000000..74af53a8e1 --- /dev/null +++ b/src/New/Entity/AttachmentClass.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +#include + +#include +#include + +class TechnoClass; + +class AttachmentClass +{ +public: + static std::vector Array; + + TechnoTypeExt::ExtData::AttachmentDataEntry* Data; + TechnoClass* Parent; + TechnoClass* Child; + CDTimerClass RespawnTimer; + + + AttachmentClass(TechnoTypeExt::ExtData::AttachmentDataEntry* data, + TechnoClass* pParent, TechnoClass* pChild = nullptr) : + Data { data }, + Parent { pParent }, + Child { pChild }, + RespawnTimer { } + { + Array.push_back(this); + } + + AttachmentClass() : + Data { }, + Parent { }, + Child { }, + RespawnTimer { } + { + Array.push_back(this); + } + + ~AttachmentClass(); + + AttachmentTypeClass* GetType(); + TechnoTypeClass* GetChildType(); + CoordStruct GetChildLocation(); + + void Initialize(); + void CreateChild(); + void AI(); + void Destroy(TechnoClass* pSource); + void ChildDestroyed(); + + void Unlimbo(); + void Limbo(); + + bool AttachChild(TechnoClass* pChild); + bool DetachChild(); + + void InvalidatePointer(void* ptr); + + bool Load(PhobosStreamReader& stm, bool registerForChange); + bool Save(PhobosStreamWriter& stm) const; + +private: + template + bool Serialize(T& stm); +}; diff --git a/src/New/Type/AttachmentTypeClass.cpp b/src/New/Type/AttachmentTypeClass.cpp new file mode 100644 index 0000000000..2290eb1571 --- /dev/null +++ b/src/New/Type/AttachmentTypeClass.cpp @@ -0,0 +1,69 @@ +#include "AttachmentTypeClass.h" + +#include + +Enumerable::container_t Enumerable::Array; + +const char* Enumerable::GetMainSection() +{ + return "AttachmentTypes"; +} + +void AttachmentTypeClass::LoadFromINI(CCINIClass* pINI) +{ + const char* section = this->Name; + + INI_EX exINI(pINI); + + this->RespawnAtCreation.Read(exINI, section, "RespawnAtCreation"); + this->RespawnDelay.Read(exINI, section, "RespawnDelay"); + this->InheritCommands.Read(exINI, section, "InheritCommands"); + this->InheritCommands_StopCommand.Read(exINI, section, "InheritCommands.StopCommand"); + this->InheritCommands_DeployCommand.Read(exINI, section, "InheritCommands.DeployCommand"); + this->InheritOwner.Read(exINI, section, "InheritOwner"); + this->InheritStateEffects.Read(exINI, section, "InheritStateEffects"); + this->InheritDestruction.Read(exINI, section, "InheritDestruction"); + this->InheritHeightStatus.Read(exINI, section, "InheritHeightStatus"); + this->OccupiesCell.Read(exINI, section, "OccupiesCell"); + this->LowSelectionPriority.Read(exINI, section, "LowSelectionPriority"); + this->TransparentToMouse.Read(exINI, section, "TransparentToMouse"); + this->YSortPosition.Read(exINI, section, "YSortPosition"); + this->DestructionWeapon_Child.Read(exINI, section, "DestructionWeapon.Child"); + this->DestructionWeapon_Parent.Read(exINI, section, "DestructionWeapon.Parent"); + this->ParentDestructionMission.Read(exINI, section, "ParentDestructionMission"); + this->ParentDetachmentMission.Read(exINI, section, "ParentDetachmentMission"); +} + +template +void AttachmentTypeClass::Serialize(T& Stm) +{ + Stm + .Process(this->RespawnAtCreation) + .Process(this->RespawnDelay) + .Process(this->InheritCommands) + .Process(this->InheritCommands_StopCommand) + .Process(this->InheritCommands_DeployCommand) + .Process(this->InheritOwner) + .Process(this->InheritStateEffects) + .Process(this->InheritDestruction) + .Process(this->InheritHeightStatus) + .Process(this->OccupiesCell) + .Process(this->LowSelectionPriority) + .Process(this->TransparentToMouse) + .Process(this->YSortPosition) + .Process(this->DestructionWeapon_Child) + .Process(this->DestructionWeapon_Parent) + .Process(this->ParentDestructionMission) + .Process(this->ParentDetachmentMission) + ; +} + +void AttachmentTypeClass::LoadFromStream(PhobosStreamReader& Stm) +{ + this->Serialize(Stm); +} + +void AttachmentTypeClass::SaveToStream(PhobosStreamWriter& Stm) +{ + this->Serialize(Stm); +} diff --git a/src/New/Type/AttachmentTypeClass.h b/src/New/Type/AttachmentTypeClass.h new file mode 100644 index 0000000000..b4acf4bbae --- /dev/null +++ b/src/New/Type/AttachmentTypeClass.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +#include + +class AttachmentTypeClass final : public Enumerable +{ +public: + Valueable RespawnAtCreation; // whether to spawn the attachment initially + Valueable RespawnDelay; + Valueable InheritCommands; + Valueable InheritCommands_StopCommand; + Valueable InheritCommands_DeployCommand; + Valueable InheritOwner; // aka mind control inheritance + Valueable InheritStateEffects; // phasing out, stealth etc. + Valueable InheritDestruction; + Valueable InheritHeightStatus; + Valueable OccupiesCell; + Valueable LowSelectionPriority; + Valueable TransparentToMouse; + Valueable YSortPosition; + Nullable DestructionWeapon_Child; + Nullable DestructionWeapon_Parent; + Nullable ParentDestructionMission; + Nullable ParentDetachmentMission; + + AttachmentTypeClass(const char* pTitle = NONE_STR) : Enumerable(pTitle) + , RespawnAtCreation { true } + , RespawnDelay { -1 } + , InheritCommands { true } + , InheritCommands_StopCommand { true } + , InheritCommands_DeployCommand { true } + , InheritOwner { true } + , InheritStateEffects { true } + , OccupiesCell { true } + , InheritDestruction { true } + , InheritHeightStatus { true } + , LowSelectionPriority { true } + , TransparentToMouse { false } + , YSortPosition { AttachmentYSortPosition::Default } + , DestructionWeapon_Child { } + , DestructionWeapon_Parent { } + , ParentDestructionMission { } + , ParentDetachmentMission { } + { } + + virtual ~AttachmentTypeClass() override = default; + + virtual void LoadFromINI(CCINIClass* pINI) override; + virtual void LoadFromStream(PhobosStreamReader& Stm); + virtual void SaveToStream(PhobosStreamWriter& Stm); + +private: + template + void Serialize(T& Stm); +}; diff --git a/src/Phobos.COM.cpp b/src/Phobos.COM.cpp index b31c953dfd..c80f7270b1 100644 --- a/src/Phobos.COM.cpp +++ b/src/Phobos.COM.cpp @@ -3,18 +3,20 @@ #include #include +#include -#ifdef CUSTOM_LOCO_EXAMPLE_ENABLED // Register the loco DEFINE_HOOK(0x6BD68D, WinMain_PhobosRegistrations, 0x6) { Debug::Log("Starting COM registration...\n"); // Add new classes to be COM-registered below +#ifdef CUSTOM_LOCO_EXAMPLE_ENABLED // Register the loco RegisterFactoryForClass(); +#endif + RegisterFactoryForClass(); Debug::Log("COM registration done!\n"); return 0; } -#endif diff --git a/src/Phobos.Ext.cpp b/src/Phobos.Ext.cpp index 9d743dd835..98e2f48820 100644 --- a/src/Phobos.Ext.cpp +++ b/src/Phobos.Ext.cpp @@ -229,7 +229,9 @@ using PhobosTypeRegistry = TypeRegistry< ShieldClass, DigitalDisplayTypeClass, AttachEffectTypeClass, - AttachEffectClass + AttachEffectClass, + AttachmentClass, + AttachmentTypeClass // other classes >; diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h index f58794b211..56175dded7 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -32,6 +32,7 @@ #pragma once +#include #include #include @@ -202,6 +203,13 @@ enum class DamageDisplayType Intercept = 2 }; +enum class AttachmentYSortPosition +{ + Default = 0, + UnderParent = 1, + OverParent = 2 +}; + enum class ChronoSparkleDisplayPosition : unsigned char { None = 0x0, diff --git a/src/Utilities/Macro.h b/src/Utilities/Macro.h index fcf943903e..4fa816b4d1 100644 --- a/src/Utilities/Macro.h +++ b/src/Utilities/Macro.h @@ -3,6 +3,14 @@ #include #include "Patch.h" +// Use when some function argument is unneeded. +// Currently that happens when faking __thiscall functions +// via __fastcall ones (fastcall function accepts args via +// ECX, EDX, then stack, thiscall via ECX for this and stack +// for rest, so second arg in fastcall-faked function would +// need to be discarded). +typedef size_t discard_t; + #define GET_REGISTER_STATIC_TYPE(type, dst, reg) static type dst; _asm { mov dst, reg } template diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index b42486f542..77927c4327 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -55,6 +55,7 @@ #include #include +#include namespace detail { @@ -565,14 +566,17 @@ namespace detail if (parser.ReadInteger(pSection, pKey, &buffer)) { - if (buffer <= (int)DirType::NorthWest && buffer >= (int)DirType::North) + unsigned int absValue = abs(buffer); + bool isNegative = buffer < 0; + + if ((int)DirType::North <= absValue && absValue <= (int)DirType::Max) { - value = static_cast(buffer); + value = static_cast(!isNegative ? absValue : (int)DirType::Max - absValue); return true; } else { - Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected a valid DirType (0-255)."); + Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected a valid DirType (0-255 abs. value)."); } } @@ -1194,6 +1198,7 @@ if(_strcmpi(parser.value(), #name) == 0){ value = __uuidof(name ## LocomotionCla #ifdef CUSTOM_LOCO_EXAMPLE_ENABLED // Add semantic parsing for loco PARSE_IF_IS_PHOBOS_LOCO(Test); #endif + PARSE_IF_IS_PHOBOS_LOCO(Attachment); #undef PARSE_IF_IS_PHOBOS_LOCO @@ -1369,6 +1374,33 @@ if(_strcmpi(parser.value(), #name) == 0){ value = __uuidof(name ## LocomotionCla return false; } + template <> + inline bool read(AttachmentYSortPosition& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + if (_strcmpi(parser.value(), "default") == 0) + { + value = AttachmentYSortPosition::Default; + } + else if (_strcmpi(parser.value(), "underparent") == 0) + { + value = AttachmentYSortPosition::UnderParent; + } + else if (_strcmpi(parser.value(), "overparent") == 0) + { + value = AttachmentYSortPosition::OverParent; + } + else + { + Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected an attachment YSort position"); + return false; + } + return true; + } + return false; + } + template void parse_values(std::vector& vector, INI_EX& parser, const char* pSection, const char* pKey) {