diff --git a/CREDITS.md b/CREDITS.md index a9620465ef..f3720edfe9 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -39,6 +39,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 - Initial jumpjet facing fix + - Techno Attachment logic - Migration utility - GitHub Actions setup - Official docs diff --git a/Phobos.vcxproj b/Phobos.vcxproj index d45a6ac91f..d0e181f027 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -18,7 +18,6 @@ - @@ -35,6 +34,7 @@ + @@ -95,6 +95,7 @@ + @@ -115,7 +116,9 @@ + + @@ -154,6 +157,7 @@ + @@ -162,6 +166,7 @@ + @@ -182,6 +187,7 @@ + @@ -260,7 +266,8 @@ - + + @@ -278,8 +285,10 @@ + + diff --git a/YRpp b/YRpp index 66e688c83b..29beb677bd 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit 66e688c83b9df8be78b3500ac3563fb1f6cd846f +Subproject commit 29beb677bd2375ae0125a3eb04882914922c0ac3 diff --git a/docs/Miscellanous.md b/docs/Miscellanous.md index f953b34bb8..3433d2d542 100644 --- a/docs/Miscellanous.md +++ b/docs/Miscellanous.md @@ -49,20 +49,22 @@ 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. - -| *Alias* | *CLSID* | -|--------:|:----------------------------------------:| -|Drive | `{4A582741-9839-11d1-B709-00A024DDAFD1}` | -|Hover | `{4A582742-9839-11d1-B709-00A024DDAFD1}` | -|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}` | -|Jumpjet | `{92612C46-F71F-11d1-AC9F-006008055BB5}` | -|Rocket | `{B7B49766-E576-11d3-9BD9-00104B972FE8}` | + - The feature is also supported for Phobos locomotors. + +| *Alias* | *CLSID* | +|----------:|:----------------------------------------:| +|Drive | `{4A582741-9839-11d1-B709-00A024DDAFD1}` | +|Hover | `{4A582742-9839-11d1-B709-00A024DDAFD1}` | +|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}` | +|Jumpjet | `{92612C46-F71F-11d1-AC9F-006008055BB5}` | +|Rocket | `{B7B49766-E576-11d3-9BD9-00104B972FE8}` | +|Attachment | `{C5D54B98-8C98-4275-8CE4-EF75CB0CBE3E}` | ```{note} `Chrono` is not a standard Alias, but since the default behavior of using `Teleport` will be triggered when the value of `Locomotor` is incorrect, the result of the operation will appear as if `Chrono` has taken effect. diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index cd0b6e4ac1..b1542e1383 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -189,6 +189,69 @@ SuppressReflectDamage.Types= ; List of AttachEffectTypes SuppressReflectDamage.Groups= ; comma-separated list of strings (group IDs) ``` +### 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). +- Attachment slots can now be assigned an `AttachmentX.ID` string. This enables child techno preservation across type conversions (e.g. Ares `UpdateType`) and deploys between building and unit. Each TechnoType owns its own set of attachment slots, and the following happens when a techno is type-converted: + 1. The current type's slots (and any children inside them) are placed in a per-type dormant storage. + 2. The new type's slots are set up - restored from dormant storage if the unit previously had this type, or created fresh otherwise. + 3. For each new slot that has an `AttachmentX.ID`, the old type's dormant slots are searched for a matching ID. If found, the child and its attachment state (e.g. the respawn timer, scaled proportionally to the new slot's `RespawnDelay`) are transferred into the new slot. + - If the new slot specifies an `AttachmentX.TechnoType` that differs from the child's current type, the child is automatically converted to the new slot's TechnoType. + - If the new slot has no `AttachmentX.TechnoType`, or if the old slot had `AttachmentX.TechnoType` set and the child's type does not match it, the child is transferred as-is regardless of its type. + - Slots without an `AttachmentX.ID` are never matched and their children are not transferred. + 4. Unmatched old dormant slots have their children placed in limbo. New slots that ended up empty are unlimboed (unless the parent is currently in limbo). + +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 +PassSelection=true ; boolean, whether the child selection propagates to parent +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] ; TechnoType +; 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 +AttachmentX.ID= ; string, max 32 chars - ID for child transfer on type conversion; must be unique per TechnoType + +[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 9d0ded6b6f..43829517de 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -553,6 +553,7 @@ New: - [Implement `CurleyShuffle` for AircraftTypes](Fixed-or-Improved-Logics.md#implement-curleyshuffle-for-aircrafttypes) (ported from Vinifera by Noble_Fish) - Customize which parasite can remove by warhead (by NetsuNegi) - Add toggle of whether shield use ArmorMultiplier or not (by NetsuNegi) +- [Techno attachment system](New-or-Enhanced-Logics.md#attachments) (by Kerbiter) Vanilla fixes: - Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya) diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 0405c71501..372e75eb66 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -371,6 +371,10 @@ void BuildingExt::KickOutStuckUnits(BuildingClass* pThis) if (pThis->Owner != pUnit->Owner || pUnit->Locomotor->Destination() != CoordStruct::Empty) continue; + // Skip attachments - they shouldn't be treated as stuck units + if (TechnoExt::IsAttached(pUnit)) + continue; + const auto height = pUnit->GetHeight(); if (height < 0 || height > Unsorted::CellHeight) diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 79739ebb49..c5c5bce94e 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -19,6 +19,9 @@ DEFINE_HOOK(0x43FE69, BuildingClass_AI, 0xA) const auto pTechnoExt = pBuildingExt->TechnoExtData; pTechnoExt->UpdateLaserTrails(); // Mainly for on turret trails + for (auto const& attachment : pTechnoExt->ChildAttachments) + attachment->AI(); + // Force airstrike targets to redraw every frame to account for tint intensity fluctuations. if (pTechnoExt->AirstrikeTargetingMe) pThis->Mark(MarkType::Change); @@ -752,6 +755,7 @@ DEFINE_HOOK(0x44EFD8, BuildingClass_FindExitCell_BarracksExitCell, 0x6) return 0; } + DEFINE_HOOK(0x444B83, BuildingClass_ExitObject_BarracksExitCell, 0x7) { enum { SkipGameCode = 0x444C7C }; diff --git a/src/Ext/Cell/Body.cpp b/src/Ext/Cell/Body.cpp index f5c5c3eda8..d55bbbeec0 100644 --- a/src/Ext/Cell/Body.cpp +++ b/src/Ext/Cell/Body.cpp @@ -12,6 +12,8 @@ void CellExt::ExtData::Serialize(T& Stm) .Process(this->RadSites) .Process(this->RadLevels) .Process(this->InfantryCount) + .Process(this->IncomingUnit) + .Process(this->IncomingUnitAlt) ; } @@ -28,7 +30,19 @@ void CellExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) } void CellExt::ExtData::InvalidatePointer(void* ptr, bool removed) -{ } +{ + if (ptr == static_cast(this->IncomingUnit)) + { + this->OwnerObject()->OccupationFlags &= ~0x20; + this->IncomingUnit = nullptr; + } + + if (ptr == static_cast(this->IncomingUnitAlt)) + { + this->OwnerObject()->AltOccupationFlags &= ~0x20; + this->IncomingUnitAlt = nullptr; + } +} bool CellExt::RadLevel::Load(PhobosStreamReader& stm, bool registerForChange) { diff --git a/src/Ext/Cell/Body.h b/src/Ext/Cell/Body.h index 5947c207e3..de1f690040 100644 --- a/src/Ext/Cell/Body.h +++ b/src/Ext/Cell/Body.h @@ -35,13 +35,15 @@ class CellExt std::vector RadSites {}; std::vector RadLevels { }; int InfantryCount{ 0 }; + UnitClass* IncomingUnit {}; + UnitClass* IncomingUnitAlt {}; ExtData(CellClass* OwnerObject) : Extension(OwnerObject) { } virtual ~ExtData() = default; - virtual void InvalidatePointer(void* ptr, bool removed) override; + virtual void InvalidatePointer(void* ptr, bool bRemoved) override; virtual void LoadFromStream(PhobosStreamReader& Stm) override; virtual void SaveToStream(PhobosStreamWriter& Stm) override; diff --git a/src/Ext/Cell/Hooks.cpp b/src/Ext/Cell/Hooks.cpp index 3472a5f499..6c5b869c6a 100644 --- a/src/Ext/Cell/Hooks.cpp +++ b/src/Ext/Cell/Hooks.cpp @@ -1,6 +1,7 @@ #include "Body.h" #include +#include DEFINE_HOOK(0x480EA8, CellClass_DamageWall_AdjacentWallDamage, 0x7) { @@ -9,3 +10,75 @@ DEFINE_HOOK(0x480EA8, CellClass_DamageWall_AdjacentWallDamage, 0x7) pThis->DamageWall(RulesExt::Global()->AdjacentWallDamage); return SkipGameCode; } + +// because 🦅💣 takes over, we have to do reimpl even more of the func and replicate Ares code + +void __fastcall UnitClass_SetOccupyBit_Reimpl(UnitClass* pThis, void*, CoordStruct* pCrd) +{ + if (TechnoExt::DoesntOccupyCellAsChild(pThis)) + return; + + CellClass* pCell = MapClass::Instance.GetCellAt(*pCrd); + auto pCellExt = CellExt::ExtMap.Find(pCell); + int height = MapClass::Instance.GetCellFloorHeight(*pCrd) + CellClass::BridgeHeight; + bool alt = (pCrd->Z >= height && pCell->ContainsBridge()); + + // remember which occupation bit we set + auto pExt = TechnoExt::ExtMap.Find(pThis); + pExt->AltOccupation = alt; + + if (alt) + { + pCell->AltOccupationFlags |= 0x20; + // Phobos addition: set incoming unit tracker + pCellExt->IncomingUnitAlt = pThis; + } + else + { + pCell->OccupationFlags |= 0x20; + // Phobos addition: set incoming unit tracker + pCellExt->IncomingUnit = pThis; + } +} + +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5D60, UnitClass_SetOccupyBit_Reimpl); + +void __fastcall UnitClass_ClearOccupyBit_Reimpl(UnitClass* pThis, void*, CoordStruct* pCrd) +{ + if (TechnoExt::DoesntOccupyCellAsChild(pThis)) + return; + + enum { obNormal = 1, obAlt = 2 }; + + CellClass* pCell = MapClass::Instance.GetCellAt(*pCrd); + auto pCellExt = CellExt::ExtMap.Find(pCell); + int height = MapClass::Instance.GetCellFloorHeight(*pCrd) + CellClass::BridgeHeight; + int alt = (pCrd->Z >= height) ? obAlt : obNormal; + + // also clear the last occupation bit, if set + auto pExt = TechnoExt::ExtMap.Find(pThis); + if(pExt->AltOccupation.has_value()) + { + int lastAlt = pExt->AltOccupation.value() ? obAlt : obNormal; + alt |= lastAlt; + pExt->AltOccupation.reset(); + } + + if (alt & obAlt) + { + pCell->AltOccupationFlags &= ~0x20; + // Phobos addition: clear incoming unit tracker + pCellExt->IncomingUnitAlt = nullptr; + } + + if (alt & obNormal) + { + pCell->OccupationFlags &= ~0x20; + // Phobos addition: clear incoming unit tracker + pCellExt->IncomingUnit = nullptr; + } +} + +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5D64, UnitClass_ClearOccupyBit_Reimpl); + +// TODO ^ same for TA for non-UnitClass, not needed so cba for now diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 0feba814bd..34dd1e2e65 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; @@ -37,6 +38,7 @@ void RulesExt::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) AttachEffectTypeClass::LoadFromINIList(pINI); BannerTypeClass::LoadFromINIList(pINI); InsigniaTypeClass::LoadFromINIList(pINI); + AttachmentTypeClass::LoadFromINIList(pINI); Data->LoadBeforeTypeData(pThis, pINI); } @@ -372,6 +374,9 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->UnitsUnsellable.Read(exINI, GameStrings::General, "UnitsUnsellable"); + 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) @@ -677,6 +682,8 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->CylinderRangefinding) .Process(this->PenetratesTransport_Level) .Process(this->UnitsUnsellable) + .Process(this->AttachmentTopLayerMinHeight) + .Process(this->AttachmentUndergroundLayerMaxHeight) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 813b7e5cdc..a6a3efae2c 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -322,6 +322,10 @@ class RulesExt Valueable UnitsUnsellable; + + Valueable AttachmentTopLayerMinHeight; + Valueable AttachmentUndergroundLayerMaxHeight; + ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } , HarvesterDumpAmount { 0.0f } @@ -587,6 +591,8 @@ class RulesExt , PenetratesTransport_Level { 10 } , UnitsUnsellable { false } + , AttachmentTopLayerMinHeight { 500 } + , AttachmentUndergroundLayerMaxHeight { -256 } { } virtual ~ExtData() = default; diff --git a/src/Ext/Techno/Body.TechnoAttachment.cpp b/src/Ext/Techno/Body.TechnoAttachment.cpp new file mode 100644 index 0000000000..5f50eab273 --- /dev/null +++ b/src/Ext/Techno/Body.TechnoAttachment.cpp @@ -0,0 +1,290 @@ +#include "Body.h" + +#include + +#include + +// 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) +{ + if (TechnoExt::DeployTransferSource) + return; // we handle that as part of the "conversion" + + 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.emplace_back(std::make_unique(&entry, pThis, nullptr))->OnCreated(); +} + +void TechnoExt::DestroyAttachments(TechnoClass* pThis, TechnoClass* pSource) +{ + // During deploy transfer the source object goes through Remove_This -> KillCargo after + // attachments were moved. The vector is empty so this is normally a no-op, but guard for safety. + if (TechnoExt::DeployTransferSource == pThis) + return; + + 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) +{ + // During deploy transfer the source goes through Remove_This which would notify the parent + // that the child was destroyed. The source is being replaced, not destroyed. + if (TechnoExt::DeployTransferSource == pThis) + return; + + 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) +{ + // During deploy transfer the source object is Limbo'd before the transfer hook fires + // (building->unit direction). Skip limbo-ing children - they will be moved to the new object. + if (TechnoExt::DeployTransferSource == pThis) + return; + + 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(); +} + +void TechnoExt::HandleAttachmentConversion(TechnoClass* pThis, TechnoTypeClass* pOldType, TechnoTypeClass* pNewType) +{ + auto const pThisExt = TechnoExt::ExtMap.Find(pThis); + auto const pNewTypeExt = TechnoTypeExt::ExtMap.Find(pNewType); + + const int oldTypeIndex = TechnoTypeClass::Array.FindItemIndex(pOldType); + const int newTypeIndex = TechnoTypeClass::Array.FindItemIndex(pNewType); + + // Helper to resolve a NullableIdx to a TechnoTypeClass pointer + auto resolveChildType = [](const NullableIdx& idx) -> TechnoTypeClass* + { + return idx.isset() ? TechnoTypeClass::Array[idx] : nullptr; + }; + + // Step 1: Store current (old type) mount points as dormant (without limboing yet) + // We preserve old attachments as is so we can then restore them as is + pThisExt->DormantAttachments[oldTypeIndex] = std::move(pThisExt->ChildAttachments); + + bool areNewAttachments = false; + // Step 2: Establish new mount points - restore from dormant or create fresh + if (auto node = pThisExt->DormantAttachments.extract(newTypeIndex)) + { + pThisExt->ChildAttachments = std::move(node.mapped()); + } + else + { + // Do NOT call OnCreated() here - we do not consider all of "technically" new attachments as new. + for (auto& entry : pNewTypeExt->AttachmentData) + pThisExt->ChildAttachments.emplace_back(std::make_unique(&entry, pThis, nullptr)); + + areNewAttachments = true; + } + + // Step 3: Match old mount points to new active ones by ID; transfer children and synchronize timers. + // Assumes each attachment ID is unique per TechnoType, which is enforced on parsing. + auto& oldMounts = pThisExt->DormantAttachments[oldTypeIndex]; + + // Shallow copy of old mount pointers for consumed-entry tracking; originals remain in DormantAttachments. + std::vector oldMountsCopy; + std::ranges::transform(oldMounts, std::back_inserter(oldMountsCopy), [](const auto& p) { return p.get(); }); + + for (auto& pNewMount : pThisExt->ChildAttachments) + { + const auto& newID = pNewMount->Data->ID; + if (!newID) + continue; + + bool gotConverted = false; + for (auto it = oldMountsCopy.begin(); it != oldMountsCopy.end(); ++it) + { + const auto& oldID = (*it)->Data->ID; + if (!oldID || _strcmpi(oldID, newID) != 0) + continue; + + // Transfer child techno if present + assert(!pNewMount->Child && "ID-matched new attachment mount already has a child before conversion illegally!"); + if (TechnoClass* pChild = (*it)->Child) + { + auto* oldChildType = resolveChildType((*it)->Data->TechnoType); + auto* newChildType = resolveChildType(pNewMount->Data->TechnoType); + + (*it)->DetachChildCore(); + + bool childMatchesType = pChild->GetTechnoType() == oldChildType; + bool typesDiffer = oldChildType != newChildType; + if (childMatchesType && typesDiffer && newChildType) + { + if (auto* pChildAsFoot = abstract_cast(pChild)) + TechnoExt::ConvertToType(pChildAsFoot, newChildType); + } + + pNewMount->AttachChildCore(pChild); + } + + // Synchronize respawn timer if both attachment types have respawn enabled. + // Preserves the completion percentage: newRemaining/newDelay == oldRemaining/oldDelay. + int oldDelay = (*it)->GetType()->RespawnDelay; + int newDelay = pNewMount->GetType()->RespawnDelay; + if (oldDelay > 0 && newDelay > 0 && (*it)->RespawnTimer.HasStarted()) + { + int oldRemaining = (*it)->RespawnTimer.GetTimeLeft(); + int newRemaining = (oldRemaining * newDelay) / oldDelay; + pNewMount->RespawnTimer.TimeLeft = newDelay; + pNewMount->RespawnTimer.StartTime = static_cast(Unsorted::CurrentFrame) - (newDelay - newRemaining); + + (*it)->RespawnTimer.Stop(); // old must giveth teh state to teh new + } + + oldMountsCopy.erase(it); + gotConverted = true; + break; + } + + // now that's what I call new - Kerbiter + if (!gotConverted && areNewAttachments) + pNewMount->OnCreated(); + } + + // Step 4: Limbo all old mount points (matched ones have no child, so Limbo is a no-op for them); + // unlimbo the new active ones + for (auto& pOldMount : oldMounts) + pOldMount->Limbo(); + + if (pThis->InLimbo) + return; // if parent is in limbo, leave new attachments in limbo as well and skip unlimboing + + for (auto& pNewMount : pThisExt->ChildAttachments) + pNewMount->Unlimbo(); +} + +void TechnoExt::HandleAttachmentDeployTransfer(TechnoClass* pFrom, TechnoClass* pTo) +{ + auto const pFromExt = TechnoExt::ExtMap.Find(pFrom); + auto const pToExt = TechnoExt::ExtMap.Find(pTo); + + // The flag is consumed here - clear it now that the transfer is happening. + TechnoExt::DeployTransferSource = nullptr; + assert(pToExt->ChildAttachments.empty() && "pTo should have no mounts before deploy transfer"); + + // Move pFrom's active and dormant attachments into pTo so they live on the surviving object. + pToExt->ChildAttachments = std::move(pFromExt->ChildAttachments); + pToExt->DormantAttachments = std::move(pFromExt->DormantAttachments); + + // Re-parent all active mounts to point at the new parent techno. + for (auto& pAttachment : pToExt->ChildAttachments) + pAttachment->Parent = pTo; + + // Re-parent dormant mounts as well, since they may be restored on a future conversion. + for (auto& [typeIdx, mounts] : pToExt->DormantAttachments) + { + for (auto& pAttachment : mounts) + pAttachment->Parent = pTo; + } + + // Now handle conversion from pFrom's type to pTo's type on the new object. + HandleAttachmentConversion(pTo, pFrom->GetTechnoType(), pTo->GetTechnoType()); +} + +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; +} diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index fc737093c4..5d0337d594 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -695,6 +695,9 @@ void TechnoExt::ExtData::UpdateTypeData(TechnoTypeClass* pCurrentType) auto const pNewTypeExt = TechnoTypeExt::ExtMap.Find(pCurrentType); this->TypeExtData = pNewTypeExt; + // Handle attachment slot matching and conversion + TechnoExt::HandleAttachmentConversion(pThis, pOldType, pCurrentType); + this->UpdateSelfOwnedAttachEffects(); if (auto const pShield = this->Shield.get()) @@ -1686,19 +1689,29 @@ void TechnoExt::KillSelf(TechnoClass* pThis, AutoDeathBehavior deathOption, cons } default: //must be AutoDeathBehavior::Kill - if (AresFunctions::SpawnSurvivors) + TechnoExt::Kill(pThis, nullptr, pThis->Owner); + } +} + +void TechnoExt::Kill(TechnoClass* pThis, ObjectClass* pAttacker, HouseClass* pAttackingHouse) +{ + if (AresFunctions::SpawnSurvivors) + { + switch (pThis->WhatAmI()) { - switch (pThis->WhatAmI()) - { - case AbstractType::Unit: - case AbstractType::Aircraft: - AresFunctions::SpawnSurvivors(static_cast(pThis), nullptr, false, false); - default:; - } + 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, nullptr); - 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 dfc6d58c79..e28663fe2a 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -12,6 +12,7 @@ TechnoExt::ExtContainer TechnoExt::ExtMap; UnitClass* TechnoExt::Deployer = nullptr; +TechnoClass* TechnoExt::DeployTransferSource = nullptr; TechnoExt::ExtData::~ExtData() { @@ -1066,12 +1067,20 @@ void TechnoExt::ExtData::Serialize(T& Stm) .Process(this->JumpjetStraightAscend) .Process(this->OnParachuted) .Process(this->HoverShutdown) + .Process(this->AltOccupation) ; } void TechnoExt::ExtData::InvalidatePointer(void* ptr, bool bRemoved) { AnnounceInvalidPointer(this->AirstrikeTargetingMe, ptr); + + for (auto const& pAttachment : ChildAttachments) + pAttachment->InvalidatePointer(ptr); + + for (auto& [key, vec] : DormantAttachments) + for (auto const& pAttachment : vec) + pAttachment->InvalidatePointer(ptr); } void TechnoExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 57d9f650dd..dff1c7fb08 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -1,11 +1,14 @@ #pragma once +#include + #include #include #include #include #include #include +#include class BulletClass; @@ -103,6 +106,13 @@ class TechnoExt bool OnParachuted; // This is just a temporary patch. TODO: fully check HasParachuted and correct its maintenance method. bool HoverShutdown; + AttachmentClass* ParentAttachment; + ValueableVector> ChildAttachments; + std::map>> DormantAttachments; + + // Ares + std::optional AltOccupation; // if the unit marks cell occupation flags, this is set to whether it uses the "high" occupation members + ExtData(TechnoClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } , Shield {} @@ -171,6 +181,10 @@ class TechnoExt , JumpjetStraightAscend { false } , OnParachuted { false } , HoverShutdown { false } + , ParentAttachment {} + , ChildAttachments {} + , DormantAttachments {} + , AltOccupation {} { } void OnEarlyUpdate(); @@ -214,6 +228,7 @@ class TechnoExt virtual ~ExtData() override; virtual void InvalidatePointer(void* ptr, bool bRemoved) override; + virtual void LoadFromStream(PhobosStreamReader& Stm) override; virtual void SaveToStream(PhobosStreamWriter& Stm) override; @@ -245,6 +260,7 @@ class TechnoExt static ExtContainer ExtMap; static UnitClass* Deployer; + static TechnoClass* DeployTransferSource; // Set before deploy-target construction to skip InitializeAttachments and possibly other things static bool LoadGlobals(PhobosStreamReader& Stm); static bool SaveGlobals(PhobosStreamWriter& Stm); @@ -257,12 +273,35 @@ class TechnoExt static bool HasRadioLinkWithDock(TechnoClass* pThis); static CoordStruct GetFLHAbsoluteCoords(TechnoClass* pThis, CoordStruct flh, bool turretFLH = false); + static void InitializeShield(TechnoClass* pThis); + 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 void HandleAttachmentConversion(TechnoClass* pThis, TechnoTypeClass* pOldType, TechnoTypeClass* pNewType); + static void HandleAttachmentDeployTransfer(TechnoClass* pFrom, TechnoClass* pTo); + + 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, const std::vector& pVanishAnimation, bool isInLimbo = false); + static void Kill(TechnoClass* pThis, ObjectClass* pAttacker, HouseClass* pAttackingHouse); + static void Kill(TechnoClass* pThis, TechnoClass* pAttacker); static void ObjectKilledBy(TechnoClass* pThis, TechnoClass* pKiller); static void UpdateSharedAmmo(TechnoClass* pThis); static double GetCurrentSpeedMultiplier(FootClass* pThis); diff --git a/src/Ext/Techno/Hooks.Misc.cpp b/src/Ext/Techno/Hooks.Misc.cpp index c924f694ff..995ac134cb 100644 --- a/src/Ext/Techno/Hooks.Misc.cpp +++ b/src/Ext/Techno/Hooks.Misc.cpp @@ -720,16 +720,20 @@ DEFINE_HOOK_AGAIN(0x5F4718, ObjectClass_Select, 0x7) DEFINE_HOOK(0x5F46AE, ObjectClass_Select, 0x7) { GET(ObjectClass*, pThis, ESI); - pThis->IsSelected = true; - if (!Phobos::Config::ShowFlashOnSelecting) - return 0; + if (Phobos::Config::ShowFlashOnSelecting) + { + const int duration = RulesExt::Global()->SelectionFlashDuration; - auto const duration = RulesExt::Global()->SelectionFlashDuration; + if (duration > 0) + { + const auto pFlashTarget = abstract_cast(pThis); - if (duration > 0 && pThis->GetOwningHouse()->IsControlledByCurrentPlayer()) - pThis->Flash(duration); + if (pFlashTarget && pFlashTarget->Owner->IsControlledByCurrentPlayer()) + pFlashTarget->Flash(duration); + } + } return 0; } diff --git a/src/Ext/Techno/Hooks.TargetEvaluation.cpp b/src/Ext/Techno/Hooks.TargetEvaluation.cpp index 67fde96409..0d5eac2d0b 100644 --- a/src/Ext/Techno/Hooks.TargetEvaluation.cpp +++ b/src/Ext/Techno/Hooks.TargetEvaluation.cpp @@ -341,7 +341,7 @@ class AresScheme } }; -static FireError __fastcall UnitClass__GetFireError_Wrapper(UnitClass* pThis, void* _, ObjectClass* pObj, int nWeaponIndex, bool ignoreRange) +FireError __fastcall UnitClass__GetFireError_Wrapper(UnitClass* pThis, void* _, ObjectClass* pObj, int nWeaponIndex, bool ignoreRange) { AresScheme::Prefix(pThis, pObj, nWeaponIndex, false); auto const result = pThis->UnitClass::GetFireError(pObj, nWeaponIndex, ignoreRange); @@ -362,8 +362,37 @@ DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB418, InfantryClass__GetFireError_Wrapper) static Action __fastcall UnitClass__WhatAction_Wrapper(UnitClass* pThis, void* _, 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_FUNCTION_JUMP(VTABLE, 0x7F5CE4, UnitClass__WhatAction_Wrapper) @@ -373,6 +402,7 @@ static Action __fastcall InfantryClass__WhatAction_Wrapper(InfantryClass* pThis, AresScheme::Prefix(pThis, pObj, -1, pThis->Type->Engineer); auto const result = pThis->InfantryClass::MouseOverObject(pObj, ignoreForce); AresScheme::Suffix(); + return result; } DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB0CC, 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..36a5248614 --- /dev/null +++ b/src/Ext/Techno/Hooks.TechnoAttachment.cpp @@ -0,0 +1,901 @@ +#include "Body.h" + +#include +#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 + +// see hooks for CellExt + +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 = (occupyFlags & 0x20) != 0; + } + + return false; +} + +DEFINE_HOOK(0x73F528, UnitClass_CanEnterCell_SkipChildren, 0x0) +{ + enum { SkipToNextOccupier = 0x73FA87, ContinueCheck = 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) + ? SkipToNextOccupier : ContinueCheck; +} + +void AccountForMovingInto(CellClass* into, bool isAlt, TechnoClass* pThis, byte& occupyFlags, bool& isVehicleFlagSet) +{ + auto const pCellExt = CellExt::ExtMap.Find(into); + auto const& pIncoming = isAlt ? pCellExt->IncomingUnitAlt : pCellExt->IncomingUnit; + + // Non-occupiers shouldn't be inserted as incoming units anyways so don't check that + if (pIncoming && pIncoming != pThis && + !TechnoExt::IsChildOf(pIncoming, pThis)) + { + occupyFlags |= TechnoAttachmentTemp::storedVehicleFlag; + isVehicleFlagSet = (occupyFlags & 0x20) != 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, void*, 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_FUNCTION_JUMP(CALL, 0x47C805, CellTechno_NoVirtual); +DEFINE_FUNCTION_JUMP(CALL, 0x47C738, CellTechno_NoVirtual); + +// skip building attachments in bib check +DEFINE_FUNCTION_JUMP(CALL, 0x4495F2, CellTechno_NoVirtualOrRelatives); +DEFINE_FUNCTION_JUMP(CALL, 0x44964E, 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; +} + +DEFINE_HOOK(0x51A0DA, InfantryClass_PerCellProcess_EntryLoopTechnos, 0x0) +{ + enum { SkipEntry = 0x51A4BF, TryEnterTarget = 0x51A258 }; + + GET(InfantryClass*, pThis, ESI); + + 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); + + // TODO additional priority checks (original code gets technos in certain order) because may backfire + + if (pEntryTarget && pEntryTarget != pThis + && (pThis->Target == pEntryTarget || pThis->Destination == pEntryTarget + || pThis->OnBridge && pCell == pEntryTarget->GetCell())) + { + R->EDI(pEntryTarget); + R->EBP(0); + return TryEnterTarget; + } + } + + return SkipEntry; +} + +enum class AttachCargoMode +{ + SingleObject, + ObjectChain, + + DefaultBehavior = ObjectChain, +}; + +namespace TechnoAttachmentTemp +{ + AttachCargoMode currentAttachMode = AttachCargoMode::DefaultBehavior; +} + +#define DEFINE_ATTACH_WRAPPER(mode) \ +void __fastcall CargoClass_Attach_##mode(PassengersClass* pThis, void*, FootClass* pThat) \ +{ \ + TechnoAttachmentTemp::currentAttachMode = AttachCargoMode::mode; \ + pThis->AddPassenger(pThat); \ + TechnoAttachmentTemp::currentAttachMode = AttachCargoMode::DefaultBehavior; \ +} + +DEFINE_ATTACH_WRAPPER(SingleObject); +DEFINE_ATTACH_WRAPPER(ObjectChain); + +DEFINE_FUNCTION_JUMP(CALL, 0x41729E, CargoClass_Attach_SingleObject); // AircraftClass::MissionMoveCarryall +DEFINE_FUNCTION_JUMP(CALL, 0x41A048, CargoClass_Attach_SingleObject); // AircraftClass::MissionEnter +DEFINE_FUNCTION_JUMP(CALL, 0x51A38A, CargoClass_Attach_SingleObject); // InfrantryClass::PerCellProcess +DEFINE_FUNCTION_JUMP(CALL, 0x710682, CargoClass_Attach_SingleObject); // TechnoClass::AttachCargo +DEFINE_FUNCTION_JUMP(CALL, 0x73A2E4, CargoClass_Attach_SingleObject); // UnitClass::PerCellProcess + +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_FUNCTION_JUMP(VTABLE, 0x7F49B0, TechnoClass_OnGround); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F49B4, TechnoClass_InAir); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F49DC, TechnoClass_IsSurfaced); + +// BuildingClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E3F0C, TechnoClass_OnGround); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E3F10, TechnoClass_InAir); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E3F38, TechnoClass_IsSurfaced); + +// FootClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E8CE4, TechnoClass_OnGround); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E8CE8, TechnoClass_InAir); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E8D10, TechnoClass_IsSurfaced); + +// UnitClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5CC0, TechnoClass_OnGround); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5CC4, TechnoClass_InAir); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5CEC, TechnoClass_IsSurfaced); + +// InfantryClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB0A8, TechnoClass_OnGround); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB0AC, TechnoClass_InAir); +DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB0D4, TechnoClass_IsSurfaced); + +// AircraftClass has it's own logic, who would want to attach aircrafts anyways + +#pragma endregion + +#pragma region Select & Flash + +bool __fastcall TechnoClass_Select(TechnoClass* pThis) +{ + const auto pExt = TechnoExt::ExtMap.Find(pThis); + const auto pAttachment = pExt->ParentAttachment; + return pAttachment && pAttachment->GetType()->PassSelection + ? pAttachment->Parent->Select() + : pThis->TechnoClass::Select(); +} +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5DBC, TechnoClass_Select) // UnitClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB1A4, TechnoClass_Select) // InfantryClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E4008, TechnoClass_Select) // BuildingClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E23F0, TechnoClass_Select) // AircraftClass + +void __fastcall TechnoClass_Flash(TechnoClass* pThis, void*, int duration) +{ + const auto pExt = TechnoExt::ExtMap.Find(pThis); + + for (const auto& pAttachment : pExt->ChildAttachments) + { + if (pAttachment->GetType()->InheritStateEffects && pAttachment->Child) + pAttachment->Child->Flash(duration); + } + + return pThis->TechnoClass::Flash(duration); +} +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5DB8, TechnoClass_Flash) // UnitClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB1A0, TechnoClass_Flash) // InfantryClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E23EC, TechnoClass_Flash) // AircraftClass + +void __fastcall BuildingClass_Flash(BuildingClass* pThis, void*, int duration) +{ + const auto pExt = TechnoExt::ExtMap.Find(pThis); + + for (const auto& pAttachment : pExt->ChildAttachments) + { + if (pAttachment->GetType()->InheritStateEffects && pAttachment->Child) + pAttachment->Child->Flash(duration); + } + + return pThis->BuildingClass::Flash(duration); +} +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E4004, BuildingClass_Flash) // BuildingClass + +#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) + { + 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) + { + 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 (TechnoAttachmentTemp::stopPressed && pAttachment->GetType()->InheritCommands_StopCommand) + pAttachment->Child->ClickedEvent(EventType::Idle); + + if (TechnoAttachmentTemp::deployPressed && 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, void*, 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_FUNCTION_JUMP(VTABLE, 0x7F5CE0, 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_FUNCTION_JUMP(CALL, 0x449413, TechnoClass_SortY_Wrapper) // BuildingClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7E235C, TechnoClass_SortY_Wrapper) // AircraftClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB110, TechnoClass_SortY_Wrapper) // InfantryClass +DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5D28, 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.Transport.cpp b/src/Ext/Techno/Hooks.Transport.cpp index 8416ebf863..2259408683 100644 --- a/src/Ext/Techno/Hooks.Transport.cpp +++ b/src/Ext/Techno/Hooks.Transport.cpp @@ -312,6 +312,7 @@ static inline void DoEnterNow(UnitClass* pTransport, FootClass* pPassenger, Tech } // Update after unit location update +// why is this hook in this file of all places? DEFINE_HOOK(0x4DA8A0, FootClass_Update_AfterLocomotorProcess, 0x6) { GET(FootClass* const, pThis, ESI); @@ -320,6 +321,9 @@ DEFINE_HOOK(0x4DA8A0, FootClass_Update_AfterLocomotorProcess, 0x6) const auto pExt = TechnoExt::ExtMap.Find(pThis); pExt->UpdateLaserTrails(); + for (auto const& attachment : pExt->ChildAttachments) + attachment->AI(); + // The core part of the fast enter action if (const auto pDest = abstract_cast(pThis->CurrentMission == Mission::Enter ? pThis->GetNthLink() : pThis->QueueUpToEnter)) { diff --git a/src/Ext/Techno/Hooks.cpp b/src/Ext/Techno/Hooks.cpp index 229aa31f93..c447bd8a36 100644 --- a/src/Ext/Techno/Hooks.cpp +++ b/src/Ext/Techno/Hooks.cpp @@ -31,7 +31,7 @@ DEFINE_HOOK(0x7363C9, UnitClass_AI_AnimationPaused, 0x6) enum { SkipGameCode = 0x7363DE }; GET(UnitClass*, pThis, ESI); - + if (TechnoExt::ExtMap.Find(pThis)->DelayedFireSequencePaused) return SkipGameCode; @@ -83,6 +83,8 @@ DEFINE_HOOK(0x71A88D, TemporalClass_AI, 0x0) { GET(TemporalClass*, pThis, ESI); + // TODO maybe handle attachments on temporal? + if (auto const pTarget = pThis->Target) { pTarget->IsMouseHovering = false; @@ -260,6 +262,8 @@ DEFINE_HOOK(0x6F42F7, TechnoClass_Init, 0x2) if (!pType) // Critical sanity check in s/l return 0; + TechnoExt::InitializeAttachments(pThis); + auto const pExt = TechnoExt::ExtMap.Find(pThis); auto const pTypeExt = TechnoTypeExt::ExtMap.Find(pType); pExt->TypeExtData = pTypeExt; @@ -1347,7 +1351,7 @@ DEFINE_HOOK(0x71A8BD, TemporalClass_Update_WarpAwayAnim, 0x5) AnimExt::CreateRandomAnim(pExt->WarpAway, pTarget->Location, nullptr, pTarget->Owner); return 0x71A90E; } - + return 0; } diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 5081f73487..38c4365dca 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -872,6 +872,64 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->AutoTargetOwnPosition_Self.Read(exINI, pSection, "AutoFire.TargetSelf"); // Temporary solution for the INI tags renaming issue, see #2093 this->AutoTargetOwnPosition_Self.Read(exINI, pSection, "AutoTargetOwnPosition.Self"); + 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) + { + char tempBuffer[32]; + 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); + + PhobosFixedString<32> id; + _snprintf_s(tempBuffer, sizeof(tempBuffer), "Attachment%d.ID", i); + id.Read(pINI, pSection, tempBuffer); + + AttachmentDataEntry const entry { ValueableIdx(type), technoType, flh, isOnTurret, rotationAdjust, id }; + if (i == AttachmentData.size()) + this->AttachmentData.push_back(entry); + else + this->AttachmentData[i] = entry; + } + + // Validate attachment ID uniqueness + std::set> usedIds; + for (size_t i = 0; i < this->AttachmentData.size(); ++i) + { + const auto& id = this->AttachmentData[i].ID; + + if (!id) + continue; + + if (!usedIds.insert(id).second) + { + Debug::FatalErrorAndExit("[%s] Duplicate Attachment ID '%s'\n", + pSection, id); + } + } + this->NoSecondaryWeaponFallback.Read(exINI, pSection, "NoSecondaryWeaponFallback"); this->NoSecondaryWeaponFallback_AllowAA.Read(exINI, pSection, "NoSecondaryWeaponFallback.AllowAA"); this->AllowWeaponSelectAgainstWalls.Read(exINI, pSection, "AllowWeaponSelectAgainstWalls"); @@ -1901,6 +1959,10 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->Unsellable) .Process(this->TurretShape) + + .Process(this->AttachmentTopLayerMinHeight) + .Process(this->AttachmentUndergroundLayerMaxHeight) + .Process(this->AttachmentData) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) @@ -1915,6 +1977,33 @@ void TechnoTypeExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) this->Serialize(Stm); } +#pragma region Data entry save/load + +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) + .Process(this->ID) + .Success(); +} + +#pragma endregion + // ============================= // container diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index f5b512f4a3..f711d546ad 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -12,6 +12,7 @@ #include #include #include +#include class Matrix3D; class ParticleSystemTypeClass; @@ -380,6 +381,28 @@ class TechnoTypeExt Nullable RadarInvisibleToHouse; + Valueable AttachmentTopLayerMinHeight; + Valueable AttachmentUndergroundLayerMaxHeight; + + struct AttachmentDataEntry + { + ValueableIdx Type; + NullableIdx TechnoType; + Valueable FLH; + Valueable IsOnTurret; + Valueable RotationAdjust; + PhobosFixedString<32> ID; + + bool Load(PhobosStreamReader& stm, bool registerForChange); + bool Save(PhobosStreamWriter& stm) const; + + private: + template + bool Serialize(T& stm); + }; + + ValueableVector AttachmentData; + struct LaserTrailDataEntry { ValueableIdx idxType; @@ -943,6 +966,10 @@ class TechnoTypeExt , Unsellable {} , TurretShape { nullptr } + + , AttachmentTopLayerMinHeight { RulesExt::Global()->AttachmentTopLayerMinHeight } + , AttachmentUndergroundLayerMaxHeight { RulesExt::Global()->AttachmentUndergroundLayerMaxHeight } + , AttachmentData {} { } virtual ~ExtData() = default; diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index 98815fdf2f..2f27b1cc52 100644 --- a/src/Ext/Unit/Hooks.DeploysInto.cpp +++ b/src/Ext/Unit/Hooks.DeploysInto.cpp @@ -92,6 +92,7 @@ DEFINE_HOOK(0x739956, UnitClass_Deploy_Transfer, 0x6) ShieldClass::SyncShieldToAnother(pUnit, pStructure); TechnoExt::SyncInvulnerability(pUnit, pStructure); AttachEffectClass::TransferAttachedEffects(pUnit, pStructure); + TechnoExt::HandleAttachmentDeployTransfer(pUnit, pStructure); return 0; } @@ -105,6 +106,7 @@ DEFINE_HOOK(0x44A03C, BuildingClass_Mi_Selling_Transfer, 0x6) ShieldClass::SyncShieldToAnother(pStructure, pUnit); TechnoExt::SyncInvulnerability(pStructure, pUnit); AttachEffectClass::TransferAttachedEffects(pStructure, pUnit); + TechnoExt::HandleAttachmentDeployTransfer(pStructure, pUnit); // This line will break the bahavior of UnDeploysInto buildings. However, it might serve a purpose that no one knows yet // Comment out the line instead of removing it for now, so we can turn to it if something related goes wrong in the future @@ -117,6 +119,8 @@ DEFINE_HOOK(0x449E2E, BuildingClass_Mi_Selling_CreateUnit, 0x6) GET(BuildingClass*, pStructure, EBP); R->ECX(pStructure->GetOriginalOwner()); + TechnoExt::DeployTransferSource = pStructure; + // Remember MC ring animation. if (pStructure->IsMindControlled()) { @@ -132,6 +136,8 @@ DEFINE_HOOK(0x7396AD, UnitClass_Deploy_CreateBuilding, 0x6) GET(UnitClass*, pUnit, EBP); R->EDX(pUnit->GetOriginalOwner()); + TechnoExt::DeployTransferSource = pUnit; + return 0x7396B3; } diff --git a/src/Ext/Unit/Hooks.DisallowMoving.cpp b/src/Ext/Unit/Hooks.DisallowMoving.cpp index 42f629e90b..addd7b2e64 100644 --- a/src/Ext/Unit/Hooks.DisallowMoving.cpp +++ b/src/Ext/Unit/Hooks.DisallowMoving.cpp @@ -5,23 +5,52 @@ DEFINE_HOOK(0x740A93, UnitClass_Mission_Move_DisallowMoving, 0x6) { + enum { QueueGuardInstead = 0x740AEF, ReturnTrue = 0x740AFD, ContinueCheck = 0x0 }; + GET(UnitClass*, pThis, ESI); - return TechnoExt::CannotMove(pThis) ? 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 TechnoExt::CannotMove(pThis) + ? QueueGuardInstead + : ContinueCheck; } DEFINE_HOOK(0x741AA7, UnitClass_Assign_Destination_DisallowMoving, 0x6) { + enum { ClearNavComsAndReturn = 0x743173, ContinueCheck = 0x0 }; + GET(UnitClass*, pThis, EBP); - return TechnoExt::CannotMove(pThis) ? 0x743173 : 0; + return TechnoExt::CannotMove(pThis) || TechnoExt::HasAttachmentLoco(pThis) + ? ClearNavComsAndReturn + : ContinueCheck; } DEFINE_HOOK(0x743B4B, UnitClass_Scatter_DisallowMoving, 0x6) { + enum { ReleaseReturn = 0x74408E, ContinueCheck = 0x0 }; + GET(UnitClass*, pThis, EBP); - return TechnoExt::CannotMove(pThis) ? 0x74408E : 0; + return TechnoExt::CannotMove(pThis) || TechnoExt::HasAttachmentLoco(pThis) + ? ReleaseReturn : ContinueCheck; } DEFINE_HOOK(0x74038F, UnitClass_What_Action_ObjectClass_DisallowMoving_1, 0x6) @@ -99,13 +128,13 @@ DEFINE_HOOK(0x73EFC4, UnitClass_Mission_Hunt_DisallowMoving, 0x6) // 3 Sep, 2025 - Starkku: Separated from above, do not change to guard mission // and only handle the target acquisition part of area guard for immobile units. -DEFINE_HOOK(0x744103, UnitClass_Mission_AreaGuard_DisallowMoving, 0x6) +DEFINE_HOOK(0x744103, UnitClass_Mission_AreaGuard_DisallowMoving, 0x6) { GET(UnitClass*, pThis, ESI); if (TechnoExt::CannotMove(pThis)) { - if (pThis->CanPassiveAcquireTargets() && pThis->TargetingTimer.Completed()) + if (pThis->CanPassiveAcquireTargets() && pThis->TargetingTimer.Completed()) pThis->TargetAndEstimateDamage(pThis->Location, ThreatType::Range); int delay = 1; diff --git a/src/Ext/Unit/Hooks.Jumpjet.cpp b/src/Ext/Unit/Hooks.Jumpjet.cpp index 280a9082ca..470147f907 100644 --- a/src/Ext/Unit/Hooks.Jumpjet.cpp +++ b/src/Ext/Unit/Hooks.Jumpjet.cpp @@ -12,6 +12,9 @@ DEFINE_HOOK(0x736F78, UnitClass_UpdateFiring_FireErrorIsFACING, 0x6) { GET(UnitClass* const, pThis, ESI); + if (TechnoExt::HasAttachmentLoco(pThis)) + return 0; + const auto pType = pThis->Type; CoordStruct& source = pThis->Location; const CoordStruct target = pThis->Target->GetCoords(); // Target checked so it's not null here diff --git a/src/Locomotion/AttachmentLocomotionClass.cpp b/src/Locomotion/AttachmentLocomotionClass.cpp new file mode 100644 index 0000000000..4c2a02b8e9 --- /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; + } + + // sight is handled in FootClass::AI + + 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..7e60e45535 --- /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/Selection.cpp b/src/Misc/Selection.cpp index 3018260c0c..d19fe78ad4 100644 --- a/src/Misc/Selection.cpp +++ b/src/Misc/Selection.cpp @@ -63,7 +63,11 @@ class ExtSelection { if ((selected.Object->AbstractFlags & AbstractFlags::Techno) != AbstractFlags::None) { - if (!TechnoExt::ExtMap.Find(static_cast(selected.Object))->TypeExtData->LowSelectionPriority) + auto const& pExt = TechnoExt::ExtMap.Find(static_cast(selected.Object)); + auto const& pTypeExt = TechnoTypeExt::ExtMap.Find(selected.Object->GetTechnoType()); + + bool isLowPriorityByAttachment = pExt->ParentAttachment && pExt->ParentAttachment->GetType()->LowSelectionPriority; + if (!pTypeExt->LowSelectionPriority && !isLowPriorityByAttachment) return true; } } @@ -89,8 +93,16 @@ class ExtSelection if (auto const pTypeExt = TechnoTypeExt::ExtMap.TryFind(pTechnoType)) // If pTechnoType is nullptr so will be pTypeExt { - if (bPriorityFiltering && pTypeExt->LowSelectionPriority) - continue; + if (bPriorityFiltering) + { + auto const& pExt = TechnoExt::ExtMap.Find(static_cast(pObject)); + // Attached units shouldn't be selected regardless of the setting + bool isLowPriorityByAttachment = pExt->ParentAttachment && pExt->ParentAttachment->GetType()->LowSelectionPriority; + bool isLowPriorityByTechno = Phobos::Config::PrioritySelectionFiltering && pTypeExt->LowSelectionPriority; + + if (isLowPriorityByAttachment || isLowPriorityByTechno) + continue; + } if (Game::IsTypeSelecting()) { @@ -169,7 +181,7 @@ class ExtSelection LTRBStruct rect { nLeft , nTop, nRight - nLeft + 1, nBottom - nTop + 1 }; - const bool bPriorityFiltering = Phobos::Config::PrioritySelectionFiltering && Tactical_IsHighPriorityInRect(pThis, &rect); + bool bPriorityFiltering = Tactical_IsHighPriorityInRect(pThis, &rect); Tactical_SelectFiltered(pThis, &rect, check_callback, bPriorityFiltering); pThis->Band.Left = 0; diff --git a/src/New/Entity/AttachmentClass.cpp b/src/New/Entity/AttachmentClass.cpp new file mode 100644 index 0000000000..2efd4f4457 --- /dev/null +++ b/src/New/Entity/AttachmentClass.cpp @@ -0,0 +1,321 @@ +#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[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::OnCreated() +{ + 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::ScenarioInit; + this->Child->Unlimbo(childCoord, childDir.GetDir()); + --Unsorted::ScenarioInit; + } +} + +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->AttachChildCore(pChild); + + // 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); + + this->DetachChildCore(); + + return true; + } + + return false; +} + + +void AttachmentClass::AttachChildCore(TechnoClass* pChild) +{ + this->Child = pChild; + TechnoExt::ExtMap.Find(pChild)->ParentAttachment = this; +} + +void AttachmentClass::DetachChildCore() +{ + if (this->Child) + { + TechnoExt::ExtMap.Find(this->Child)->ParentAttachment = nullptr; + this->Child = nullptr; + } +} + +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..51693f0ea7 --- /dev/null +++ b/src/New/Entity/AttachmentClass.h @@ -0,0 +1,72 @@ +#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 OnCreated(); + void CreateChild(); + void AI(); + void Destroy(TechnoClass* pSource); + void ChildDestroyed(); + + void Unlimbo(); + void Limbo(); + + bool AttachChild(TechnoClass* pChild); + bool DetachChild(); + + // Core link/unlink without side effects (locomotor changes, missions, owner resets) + void AttachChildCore(TechnoClass* pChild); + void DetachChildCore(); + + 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..4165f89bb2 --- /dev/null +++ b/src/New/Type/AttachmentTypeClass.cpp @@ -0,0 +1,69 @@ +#include "AttachmentTypeClass.h" + +#include + +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->PassSelection.Read(exINI, section, "PassSelection"); + 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->PassSelection) + .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..eb519e8d4d --- /dev/null +++ b/src/New/Type/AttachmentTypeClass.h @@ -0,0 +1,61 @@ +#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 PassSelection; + 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 } + , PassSelection { true } + , TransparentToMouse { false } + , YSortPosition { AttachmentYSortPosition::Default } + , DestructionWeapon_Child { } + , DestructionWeapon_Parent { } + , ParentDestructionMission { } + , ParentDetachmentMission { } + { } + + virtual ~AttachmentTypeClass() = default; + + virtual void LoadFromINI(CCINIClass* pINI); + 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.COM.h b/src/Phobos.COM.h index ef98e297fe..6838b4a3cf 100644 --- a/src/Phobos.COM.h +++ b/src/Phobos.COM.h @@ -13,7 +13,7 @@ void RegisterFactoryForClass(IClassFactory* pFactory) else Debug::Log("Class factory for %s registered.\n", typeid(T).name()); - Game::COMClasses->AddItem((ULONG)dwRegister); + Game::COMClasses.AddItem((ULONG)dwRegister); } // Registers an automatically created factory for a class. diff --git a/src/Phobos.Ext.cpp b/src/Phobos.Ext.cpp index ac02d779bf..7d2c54331a 100644 --- a/src/Phobos.Ext.cpp +++ b/src/Phobos.Ext.cpp @@ -242,7 +242,10 @@ using PhobosTypeRegistry = TypeRegistry < AttachEffectTypeClass, AttachEffectClass, NewSWType, - SelectBoxTypeClass + SelectBoxTypeClass, + AttachEffectClass, + AttachmentClass, + AttachmentTypeClass // other classes > ; diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h index 2109fdf943..b64d7bf412 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -243,6 +243,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/TemplateDef.h b/src/Utilities/TemplateDef.h index 856355209f..4161affbc7 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -59,6 +59,7 @@ #include #include #include +#include namespace detail { @@ -537,14 +538,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 + 1 - 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)."); } } @@ -1181,6 +1185,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 @@ -1476,6 +1481,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) {