From 33920746b5075b111a33accb4dbc63e38c9a3f3b Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 12 May 2025 12:16:10 +0200 Subject: [PATCH 01/19] Initial commit --- Phobos.vcxproj | 2 + src/Ext/BuildingType/Body.cpp | 3 + src/Ext/BuildingType/Body.h | 3 + src/Ext/House/Body.cpp | 1 + src/Ext/House/Body.h | 3 + src/Ext/HouseType/Body.cpp | 141 +++++++++++++++++++++++++++++++ src/Ext/HouseType/Body.h | 55 ++++++++++++ src/Ext/Rules/Body.cpp | 3 + src/Ext/Rules/Body.h | 4 + src/Ext/SWType/Body.cpp | 3 + src/Ext/SWType/Body.h | 3 + src/Ext/Techno/Body.Internal.cpp | 36 ++++++++ src/Ext/TechnoType/Body.cpp | 4 + src/Ext/TechnoType/Body.h | 4 + src/Misc/PhobosToolTip.cpp | 12 +++ src/Phobos.INI.cpp | 4 + src/Phobos.h | 1 + 17 files changed, 282 insertions(+) create mode 100644 src/Ext/HouseType/Body.cpp create mode 100644 src/Ext/HouseType/Body.h diff --git a/Phobos.vcxproj b/Phobos.vcxproj index e036e4902c..e29834675f 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -21,6 +21,7 @@ + @@ -197,6 +198,7 @@ + diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 632b0e1887..d3efa406fa 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -201,6 +201,8 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) } } + this->BattlePointsGenerator.Read(exINI, pSection, "BattlePointsGenerator"); + // Ares tag this->SpyEffect_Custom.Read(exINI, pSection, "SpyEffect.Custom"); if (SuperWeaponTypeClass::Array.Count > 0) @@ -305,6 +307,7 @@ void BuildingTypeExt::ExtData::Serialize(T& Stm) .Process(this->BarracksExitCell) .Process(this->Overpower_KeepOnline) .Process(this->Overpower_ChargeWeapon) + .Process(this->BattlePointsGenerator) ; } diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 74d0e27e4c..c2091201c6 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -84,6 +84,8 @@ class BuildingTypeExt Valueable Overpower_KeepOnline; Valueable Overpower_ChargeWeapon; + Nullable BattlePointsGenerator; + ExtData(BuildingTypeClass* OwnerObject) : Extension(OwnerObject) , PowersUp_Owner { AffectedHouse::Owner } , PowersUp_Buildings {} @@ -135,6 +137,7 @@ class BuildingTypeExt , BarracksExitCell {} , Overpower_KeepOnline { 2 } , Overpower_ChargeWeapon { 1 } + , BattlePointsGenerator {} { } // Ares 0.A functions diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index e4fd2d70cf..dcb85de7a0 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -653,6 +653,7 @@ void HouseExt::ExtData::Serialize(T& Stm) .Process(this->SuspendedEMPulseSWs) .Process(this->SuperExts) .Process(this->ForceEnemyIndex) + .Process(this->BattlePoints) ; } diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 8f99bffc31..dbffccb077 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -66,6 +66,8 @@ class HouseExt int ForceEnemyIndex; + int BattlePoints; + ExtData(HouseClass* OwnerObject) : Extension(OwnerObject) , PowerPlantEnhancers {} , OwnedLimboDeliveredBuildings {} @@ -94,6 +96,7 @@ class HouseExt , SuspendedEMPulseSWs {} , SuperExts(SuperWeaponTypeClass::Array.Count) , ForceEnemyIndex(-1) + , BattlePoints(0) { } bool OwnsLimboDeliveredBuilding(BuildingClass* pBuilding); diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp new file mode 100644 index 0000000000..e3b54bac6c --- /dev/null +++ b/src/Ext/HouseType/Body.cpp @@ -0,0 +1,141 @@ +#include "Body.h" + +#include + +static constexpr DWORD Canary = 0x1111111A; +HouseTypeExt::ExtContainer HouseTypeExt::ExtMap; + +void HouseTypeExt::ExtData::Initialize() +{ +} + +// ============================= +// load / save + +void HouseTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) +{ + auto pThis = this->OwnerObject(); + const char* pSection = pThis->ID; + + if (!pINI->GetSection(pSection)) + return; + + INI_EX exINI(pINI); + + this->BattlePoints.Read(exINI, pSection, "BattlePoints"); + this->BattlePoints_CanReuseStandardPoints.Read(exINI, pSection, "BattlePoints.CanUseStandardPoints"); +} + +void HouseTypeExt::ExtData::CompleteInitialization() +{ + auto const pThis = this->OwnerObject(); + UNREFERENCED_PARAMETER(pThis); +} + +template +void HouseTypeExt::ExtData::Serialize(T& Stm) +{ + Stm + .Process(this->BattlePoints) + .Process(this->BattlePoints_CanReuseStandardPoints) + ; +} + +void HouseTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) +{ + Extension::LoadFromStream(Stm); + this->Serialize(Stm); +} + +void HouseTypeExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) +{ + Extension::SaveToStream(Stm); + this->Serialize(Stm); +} + +bool HouseTypeExt::ExtContainer::Load(HouseTypeClass* pThis, IStream* pStm) +{ + HouseTypeExt::ExtData* pData = this->LoadKey(pThis, pStm); + return pData != nullptr; +}; + +bool HouseTypeExt::LoadGlobals(PhobosStreamReader& Stm) +{ + return Stm.Success(); +} + +bool HouseTypeExt::SaveGlobals(PhobosStreamWriter& Stm) +{ + return Stm.Success(); +} + +// ============================= +// container + +HouseTypeExt::ExtContainer::ExtContainer() : Container("HouseTypeClass") { } + +HouseTypeExt::ExtContainer::~ExtContainer() = default; + +// ============================= +// container hooks + +DEFINE_HOOK(0x511635, HouseTypeClass_CTOR_1, 0x5) +{ + GET(HouseTypeClass*, pItem, EAX); + + HouseTypeExt::ExtMap.Allocate(pItem); + + return 0; +} + +DEFINE_HOOK(0x511643, HouseTypeClass_CTOR_2, 0x5) +{ + GET(HouseTypeClass*, pItem, EAX); + + HouseTypeExt::ExtMap.Allocate(pItem); + + return 0; +} + +DEFINE_HOOK(0x5127CF, HouseTypeClass_DTOR, 0x6) +{ + GET(HouseTypeClass*, pItem, ESI); + + HouseTypeExt::ExtMap.Remove(pItem); + + return 0; +} + +DEFINE_HOOK_AGAIN(0x512480, HouseTypeClass_SaveLoad_Prefix, 0x5) +DEFINE_HOOK(0x512290, HouseTypeClass_SaveLoad_Prefix, 0x5) +{ + GET_STACK(HouseTypeClass*, pItem, 0x4); + GET_STACK(IStream*, pStm, 0x8); + + HouseTypeExt::ExtMap.PrepareStream(pItem, pStm); + + return 0; +} + +DEFINE_HOOK(0x51246D, HouseTypeClass_Load_Suffix, 0x5) +{ + HouseTypeExt::ExtMap.LoadStatic(); + return 0; +} + +DEFINE_HOOK(0x51255C, HouseTypeClass_Save_Suffix, 0x5) +{ + HouseTypeExt::ExtMap.SaveStatic(); + return 0; +} + +DEFINE_HOOK_AGAIN(0x51215A, HouseTypeClass_LoadFromINI, 0x5) +DEFINE_HOOK(0x51214F, HouseTypeClass_LoadFromINI, 0x5) +{ + GET(HouseTypeClass*, pItem, EBX); + GET_BASE(CCINIClass*, pINI, 0x8); + + HouseTypeExt::ExtMap.LoadFromINI(pItem, pINI); + + return 0; +} diff --git a/src/Ext/HouseType/Body.h b/src/Ext/HouseType/Body.h new file mode 100644 index 0000000000..5940d6843c --- /dev/null +++ b/src/Ext/HouseType/Body.h @@ -0,0 +1,55 @@ +#pragma once +#include + +#include +#include +#include + +class HouseTypeExt +{ +public: + using base_type = HouseTypeClass; + static constexpr DWORD Canary = 0x11112222; + + class ExtData final : public Extension + { + public: + Valueable BattlePoints; + Valueable BattlePoints_CanReuseStandardPoints; + + ExtData(HouseTypeClass* OwnerObject) : Extension(OwnerObject) + , BattlePoints { false } + , BattlePoints_CanReuseStandardPoints { false } + { } + + virtual ~ExtData() = default; + + virtual void LoadFromINIFile(CCINIClass* pINI) override; + virtual void Initialize() override; + virtual void CompleteInitialization(); + + virtual void InvalidatePointer(void* ptr, bool bRemoved) override + { + } + + virtual void LoadFromStream(PhobosStreamReader& Stm) override; + virtual void SaveToStream(PhobosStreamWriter& Stm) override; + + private: + template + void Serialize(T& Stm); + }; + + class ExtContainer final : public Container + { + public: + ExtContainer(); + ~ExtContainer(); + + virtual bool Load(HouseTypeClass* pThis, IStream* pStm) override; + }; + + static ExtContainer ExtMap; + static bool LoadGlobals(PhobosStreamReader& Stm); + static bool SaveGlobals(PhobosStreamWriter& Stm); +}; diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 7542f826b8..53004199e7 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -267,6 +267,8 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->HarvesterScanAfterUnload.Read(exINI, GameStrings::General, "HarvesterScanAfterUnload"); + this->BattlePoints.Read(exINI, GameStrings::General, "BattlePoints"); + // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); for (int i = 0; i < itemsCount; ++i) @@ -489,6 +491,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->ProneSpeed_NoCrawls) .Process(this->DamagedSpeed) .Process(this->HarvesterScanAfterUnload) + .Process(this->BattlePoints) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 89f523fdf3..4e500f11af 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -220,6 +220,8 @@ class RulesExt Valueable HarvesterScanAfterUnload; + Nullable BattlePoints; + ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } , HarvesterDumpAmount { 0.0f } @@ -385,6 +387,8 @@ class RulesExt , DamagedSpeed { 0.75 } , HarvesterScanAfterUnload { false } + + , BattlePoints {} { } virtual ~ExtData() = default; diff --git a/src/Ext/SWType/Body.cpp b/src/Ext/SWType/Body.cpp index c009999ab2..ea415e29e5 100644 --- a/src/Ext/SWType/Body.cpp +++ b/src/Ext/SWType/Body.cpp @@ -61,6 +61,7 @@ void SWTypeExt::ExtData::Serialize(T& Stm) .Process(this->EMPulse_SuspendOthers) .Process(this->EMPulse_Cannons) .Process(this->EMPulse_TargetSelf) + .Process(this->BattlePoints_Amount) ; } @@ -195,6 +196,8 @@ void SWTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) pNewSWType->Initialize(const_cast(this), OwnerObject()); pNewSWType->LoadFromINI(const_cast(this), OwnerObject(), pINI); } + + this->BattlePoints_Amount.Read(exINI, pSection, "BattlePoints.Amount"); } void SWTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/SWType/Body.h b/src/Ext/SWType/Body.h index aae1713b62..674ada61e4 100644 --- a/src/Ext/SWType/Body.h +++ b/src/Ext/SWType/Body.h @@ -82,6 +82,8 @@ class SWTypeExt ValueableVector EMPulse_Cannons; Valueable EMPulse_TargetSelf; + Valueable BattlePoints_Amount; + ExtData(SuperWeaponTypeClass* OwnerObject) : Extension(OwnerObject) , TypeID { "" } , Money_Amount { 0 } @@ -131,6 +133,7 @@ class SWTypeExt , EMPulse_SuspendOthers { false } , EMPulse_Cannons {} , EMPulse_TargetSelf { false } + , BattlePoints_Amount { 0 } { } // Ares 0.A functions diff --git a/src/Ext/Techno/Body.Internal.cpp b/src/Ext/Techno/Body.Internal.cpp index 120cb1f8c8..f042e5fd05 100644 --- a/src/Ext/Techno/Body.Internal.cpp +++ b/src/Ext/Techno/Body.Internal.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include // Unsorted methods @@ -25,6 +28,39 @@ void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller) TechnoClass* pObjectKiller = ((pKiller->GetTechnoType()->Spawned || pKiller->GetTechnoType()->MissileSpawn) && pKiller->SpawnOwner) ? pKiller->SpawnOwner : pKiller; + if (pObjectKiller && pKiller->Owner != pVictim->Owner && !pKiller->Owner->IsAlliedWith(pVictim)) + { + auto pOwner = pKiller->Owner; + const auto pOwnerTypeExt = HouseTypeExt::ExtMap.Find(pOwner->Type); + //bool hasBattlePointsGenerator = false; + bool enabledBattlePoints = false; + + //RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get() + for (auto const pBuilding : pOwner->Buildings) + { + const auto pBuildingTypeExt = BuildingTypeExt::ExtMap.Find(pBuilding->Type); + if (pBuildingTypeExt->BattlePointsGenerator.isset() && pBuildingTypeExt->BattlePointsGenerator.Get()) + { + enabledBattlePoints = true; + break; + } + } + + enabledBattlePoints |= pOwnerTypeExt->BattlePoints; + enabledBattlePoints |= RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get(); + + if (enabledBattlePoints) + { + auto pOwnerExt = HouseExt::ExtMap.Find(pOwner); + const auto pVictimTypeExt = TechnoTypeExt::ExtMap.Find(pVictim->GetTechnoType()); + + int points = pVictimTypeExt->BattlePoints.isset() ? pVictimTypeExt->BattlePoints.Get() : 0; + points = points == 0 && pOwnerTypeExt->BattlePoints_CanReuseStandardPoints ? pVictim->GetTechnoType()->Points : points; + pOwnerExt->BattlePoints += points; + pOwnerExt->BattlePoints = pOwnerExt->BattlePoints < 0 ? 0 : pOwnerExt->BattlePoints; + } + } + if (pObjectKiller && pObjectKiller->BelongsToATeam()) { if (auto const pFootKiller = generic_cast(pObjectKiller)) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index ecdd782c05..b7f1e069cb 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -570,6 +570,8 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->Harvester_CanGuardArea.Read(exINI, pSection, "Harvester.CanGuardArea"); this->HarvesterScanAfterUnload.Read(exINI, pSection, "HarvesterScanAfterUnload"); + this->BattlePoints.Read(exINI, pSection, "BattlePoints"); + // Ares 0.2 this->RadarJamRadius.Read(exINI, pSection, "RadarJamRadius"); @@ -1114,6 +1116,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->FallingDownDamage) .Process(this->FallingDownDamage_Water) + + .Process(this->BattlePoints) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 68dda0e8e1..e808175e67 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -355,6 +355,8 @@ class TechnoTypeExt Valueable FallingDownDamage; Nullable FallingDownDamage_Water; + Nullable BattlePoints; + ExtData(TechnoTypeClass* OwnerObject) : Extension(OwnerObject) , HealthBar_Hide { false } , UIDescription {} @@ -662,6 +664,8 @@ class TechnoTypeExt , FallingDownDamage { 1.0 } , FallingDownDamage_Water {} + + , BattlePoints {} { } virtual ~ExtData() = default; diff --git a/src/Misc/PhobosToolTip.cpp b/src/Misc/PhobosToolTip.cpp index 73bf7c06c0..e94e2d6261 100644 --- a/src/Misc/PhobosToolTip.cpp +++ b/src/Misc/PhobosToolTip.cpp @@ -181,6 +181,18 @@ void PhobosToolTip::HelpText_Super(int swidx) showSth = true; } + if (int nPoints = std::abs(pData->BattlePoints_Amount)) + { + oss << L"\n"; + + if (pData->BattlePoints_Amount > 0) + oss << Phobos::UI::BattlePointsLabel << L"-" << nPoints; + else if (pData->BattlePoints_Amount < 0) + oss << Phobos::UI::BattlePointsLabel << L"+" << nPoints; + + showSth = true; + } + int rechargeTime = TickTimeToSeconds(pSuper->GetRechargeTime()); if (rechargeTime > 0) { diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index e22bdedde8..81a1575713 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -27,6 +27,7 @@ const wchar_t* Phobos::UI::TimeLabel = L""; const wchar_t* Phobos::UI::HarvesterLabel = L""; const wchar_t* Phobos::UI::ShowBriefingResumeButtonLabel = L""; const wchar_t* Phobos::UI::SWShotsFormat = L""; +const wchar_t* Phobos::UI::BattlePointsLabel = L""; char Phobos::UI::ShowBriefingResumeButtonStatusLabel[32]; bool Phobos::UI::PowerDelta_Show = false; double Phobos::UI::PowerDelta_ConditionYellow = 0.75; @@ -132,6 +133,9 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) ini_uimd.ReadString(GameStrings::ToolTips, "SWShotsFormat", NONE_STR, Phobos::readBuffer); Phobos::UI::SWShotsFormat = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"Shots: %d"); // ⌚ + + ini_uimd.ReadString(GameStrings::ToolTips, "BattlePointsLabel", NONE_STR, Phobos::readBuffer); + Phobos::UI::BattlePointsLabel = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"BP: "); } // Sidebar diff --git a/src/Phobos.h b/src/Phobos.h index d7e993fd7e..6930ea2754 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -65,6 +65,7 @@ class Phobos static const wchar_t* ShowBriefingResumeButtonLabel; static const wchar_t* SWShotsFormat; static char ShowBriefingResumeButtonStatusLabel[0x20]; + static const wchar_t* BattlePointsLabel; }; class Config From 54a141e0be9ff843a4f9bb60f4f459fe0b83ac1c Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 12 May 2025 23:27:59 +0200 Subject: [PATCH 02/19] More code Improved & splitted the code in auxiliar functions. Now the SW can modify the house points. Now a new General tag is added for default value when the tag isn't established in the techno. Added hook SuperClass_IsReady_BattlePoints for disabling the use of the SW if it doesn't have enough points (in reality is a rewrite of that IsReady() method adding this check) . --- src/Ext/House/Body.cpp | 45 ++++++++++++++++++++++++++++++ src/Ext/House/Body.h | 4 +++ src/Ext/Rules/Body.cpp | 2 ++ src/Ext/Rules/Body.h | 2 ++ src/Ext/SWType/Body.h | 2 ++ src/Ext/SWType/FireSuperWeapon.cpp | 9 ++++++ src/Ext/SWType/Hooks.cpp | 28 +++++++++++++++++++ src/Ext/Techno/Body.Internal.cpp | 29 +++---------------- 8 files changed, 96 insertions(+), 25 deletions(-) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index dcb85de7a0..e5f71ddde9 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -1085,3 +1086,47 @@ bool HouseExt::ReachedBuildLimit(const HouseClass* pHouse, const TechnoTypeClass return false; } #pragma endregion + +void HouseExt::ExtData::UpdateBattlePoints(int modifier) +{ + this->BattlePoints += modifier; + this->BattlePoints = this->BattlePoints < 0 ? 0 : this->BattlePoints; +} + +bool HouseExt::ExtData::AreBattlePointsEnabled() +{ + auto const pThis = this->OwnerObject(); + const auto pOwnerTypeExt = HouseTypeExt::ExtMap.Find(pThis->Type); + bool enabledBattlePoints = false; + + for (auto const pBuilding : pThis->Buildings) + { + const auto pBuildingTypeExt = BuildingTypeExt::ExtMap.Find(pBuilding->Type); + if (pBuildingTypeExt->BattlePointsGenerator.isset() && pBuildingTypeExt->BattlePointsGenerator.Get()) + { + enabledBattlePoints = true; + break; + } + } + + enabledBattlePoints |= pOwnerTypeExt->BattlePoints; + enabledBattlePoints |= RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get(); + + return enabledBattlePoints; +} + +int HouseExt::ExtData::CalculateBattlePoints(TechnoClass* pTechno) +{ + if (!pTechno) + return 0; + + auto const pThis = this->OwnerObject(); + const auto pThisTypeExt = HouseTypeExt::ExtMap.Find(pThis->Type); + const auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(pTechno->GetTechnoType()); + + int points = RulesExt::Global()->BattlePoints_DefaultKillValue.isset() ? RulesExt::Global()->BattlePoints_DefaultKillValue.Get() : 0; + points = points == 0 && pTechnoTypeExt->BattlePoints.isset() ? pTechnoTypeExt->BattlePoints.Get() : points; + points = points == 0 && pThisTypeExt->BattlePoints_CanReuseStandardPoints ? pTechno->GetTechnoType()->Points : points; + + return points; +} diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index dbffccb077..1dd361e8a3 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -118,6 +118,10 @@ class HouseExt void UpdateVehicleProduction(); + void UpdateBattlePoints(int modifier); + bool AreBattlePointsEnabled(); + int CalculateBattlePoints(TechnoClass* pTechno); + virtual void LoadFromStream(PhobosStreamReader& Stm) override; virtual void SaveToStream(PhobosStreamWriter& Stm) override; diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 53004199e7..6e7ace41e5 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -268,6 +268,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->HarvesterScanAfterUnload.Read(exINI, GameStrings::General, "HarvesterScanAfterUnload"); this->BattlePoints.Read(exINI, GameStrings::General, "BattlePoints"); + this->BattlePoints_DefaultKillValue.Read(exINI, GameStrings::General, "BattlePoints.DefaultKillValue"); // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); @@ -492,6 +493,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->DamagedSpeed) .Process(this->HarvesterScanAfterUnload) .Process(this->BattlePoints) + .Process(this->BattlePoints_DefaultKillValue) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 4e500f11af..04a5781c74 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -221,6 +221,7 @@ class RulesExt Valueable HarvesterScanAfterUnload; Nullable BattlePoints; + Nullable BattlePoints_DefaultKillValue; ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } @@ -389,6 +390,7 @@ class RulesExt , HarvesterScanAfterUnload { false } , BattlePoints {} + , BattlePoints_DefaultKillValue {} { } virtual ~ExtData() = default; diff --git a/src/Ext/SWType/Body.h b/src/Ext/SWType/Body.h index 674ada61e4..9b6a392211 100644 --- a/src/Ext/SWType/Body.h +++ b/src/Ext/SWType/Body.h @@ -157,6 +157,8 @@ class SWTypeExt std::vector GetEMPulseCannons(HouseClass* pOwner, const CellStruct& cell) const; std::pair GetEMPulseCannonRange(BuildingClass* pBuilding) const; + void ApplyBattlePoints(SuperClass* pSW); + virtual void LoadFromINIFile(CCINIClass* pINI) override; virtual ~ExtData() = default; diff --git a/src/Ext/SWType/FireSuperWeapon.cpp b/src/Ext/SWType/FireSuperWeapon.cpp index f467873c64..59e6ebd828 100644 --- a/src/Ext/SWType/FireSuperWeapon.cpp +++ b/src/Ext/SWType/FireSuperWeapon.cpp @@ -37,6 +37,9 @@ void SWTypeExt::FireSuperWeaponExt(SuperClass* pSW, const CellStruct& cell) if (static_cast(pSW->Type->Type) == 28 && !pTypeExt->EMPulse_TargetSelf) // Ares' Type=EMPulse SW pTypeExt->HandleEMPulseLaunch(pSW, cell); + if (pTypeExt->BattlePoints_Amount != 0) + pTypeExt->ApplyBattlePoints(pSW); + auto& sw_ext = HouseExt::ExtMap.Find(pSW->Owner)->SuperExts[pSW->Type->ArrayIndex]; sw_ext.ShotCount++; } @@ -345,3 +348,9 @@ void SWTypeExt::ExtData::HandleEMPulseLaunch(SuperClass* pSW, const CellStruct& } } } + +void SWTypeExt::ExtData::ApplyBattlePoints(SuperClass* pSW) +{ + auto pOwnerExt = HouseExt::ExtMap.Find(pSW->Owner); + pOwnerExt->UpdateBattlePoints(this->BattlePoints_Amount * (-1)); +} diff --git a/src/Ext/SWType/Hooks.cpp b/src/Ext/SWType/Hooks.cpp index d107695abd..9fbb94c3f7 100644 --- a/src/Ext/SWType/Hooks.cpp +++ b/src/Ext/SWType/Hooks.cpp @@ -266,3 +266,31 @@ DEFINE_HOOK(0x6AC67A, SidebarClass_6AC5F0_TabIndex, 0x5) DEFINE_JUMP(LJMP, 0x6A8D07, 0x6A8D17) // Skip tabIndex check #pragma endregion + +DEFINE_HOOK(0x6CC367, SuperClass_IsReady_BattlePoints, 0xD) +{ + GET(SuperClass*, pSuper, ECX); + + enum{ ReturnIsReady = 0x6CC37D, ReturnZero = 0x6CC381, SkipAll = 0x6CC383}; + + if (pSuper->IsSuspended) + return ReturnZero; + + if (pSuper->Type->UseChargeDrain) + { + R->AL(pSuper->ChargeDrainState != ChargeDrainState::Charging); + return SkipAll; + } + + const auto pOwner = pSuper->Owner; + auto pOwnerExt = HouseExt::ExtMap.Find(pOwner); + auto pExt = SWTypeExt::ExtMap.Find(pSuper->Type); + + if (pExt->BattlePoints_Amount > 0) + { + if (pOwnerExt->BattlePoints < pExt->BattlePoints_Amount) + return ReturnZero; + } + + return ReturnIsReady; +} diff --git a/src/Ext/Techno/Body.Internal.cpp b/src/Ext/Techno/Body.Internal.cpp index f042e5fd05..b27da9b18a 100644 --- a/src/Ext/Techno/Body.Internal.cpp +++ b/src/Ext/Techno/Body.Internal.cpp @@ -31,33 +31,12 @@ void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller) if (pObjectKiller && pKiller->Owner != pVictim->Owner && !pKiller->Owner->IsAlliedWith(pVictim)) { auto pOwner = pKiller->Owner; - const auto pOwnerTypeExt = HouseTypeExt::ExtMap.Find(pOwner->Type); - //bool hasBattlePointsGenerator = false; - bool enabledBattlePoints = false; + auto pOwnerExt = HouseExt::ExtMap.Find(pOwner); - //RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get() - for (auto const pBuilding : pOwner->Buildings) + if (pOwnerExt->AreBattlePointsEnabled()) { - const auto pBuildingTypeExt = BuildingTypeExt::ExtMap.Find(pBuilding->Type); - if (pBuildingTypeExt->BattlePointsGenerator.isset() && pBuildingTypeExt->BattlePointsGenerator.Get()) - { - enabledBattlePoints = true; - break; - } - } - - enabledBattlePoints |= pOwnerTypeExt->BattlePoints; - enabledBattlePoints |= RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get(); - - if (enabledBattlePoints) - { - auto pOwnerExt = HouseExt::ExtMap.Find(pOwner); - const auto pVictimTypeExt = TechnoTypeExt::ExtMap.Find(pVictim->GetTechnoType()); - - int points = pVictimTypeExt->BattlePoints.isset() ? pVictimTypeExt->BattlePoints.Get() : 0; - points = points == 0 && pOwnerTypeExt->BattlePoints_CanReuseStandardPoints ? pVictim->GetTechnoType()->Points : points; - pOwnerExt->BattlePoints += points; - pOwnerExt->BattlePoints = pOwnerExt->BattlePoints < 0 ? 0 : pOwnerExt->BattlePoints; + int points = pOwnerExt->CalculateBattlePoints(pVictim); + pOwnerExt->UpdateBattlePoints(points); } } From 2398e3116ea86f00898ecb19f21fab39067f0f8a Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 14 May 2025 12:55:48 +0200 Subject: [PATCH 03/19] Small additions - Now friendly objects can have it's own default value. - Renamed the default value tag, - Realtime sidebar counter indicator. - New tag: Sidebar.BattlePoints.Offset - New tag: Sidebar.BattlePoints.Color - New tag: Sidebar.BattlePoints.Align - Now Chronoshere tele-kill will register the points (why not the nuke... :@ ). --- src/Ext/House/Body.cpp | 5 ++++- src/Ext/HouseType/Body.cpp | 2 +- src/Ext/Rules/Body.cpp | 6 ++++-- src/Ext/Rules/Body.h | 6 ++++-- src/Ext/Side/Body.cpp | 6 ++++++ src/Ext/Side/Body.h | 6 ++++++ src/Ext/Techno/Body.Internal.cpp | 31 ++++++++++++++++++++----------- src/Ext/Techno/Body.h | 2 +- src/Ext/Techno/Hooks.cpp | 12 ++++++++++++ src/Misc/Hooks.UI.cpp | 26 ++++++++++++++++++++++++++ src/Phobos.INI.cpp | 15 ++++++++++++++- src/Phobos.h | 4 ++++ 12 files changed, 102 insertions(+), 19 deletions(-) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index e5f71ddde9..374515ac70 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -1124,7 +1124,10 @@ int HouseExt::ExtData::CalculateBattlePoints(TechnoClass* pTechno) const auto pThisTypeExt = HouseTypeExt::ExtMap.Find(pThis->Type); const auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(pTechno->GetTechnoType()); - int points = RulesExt::Global()->BattlePoints_DefaultKillValue.isset() ? RulesExt::Global()->BattlePoints_DefaultKillValue.Get() : 0; + int defaultValue = RulesExt::Global()->BattlePoints_DefaultValue.isset() ? RulesExt::Global()->BattlePoints_DefaultValue.Get() : 0; + int defaultFriendlyValue = RulesExt::Global()->BattlePoints_DefaultFriendlyValue.isset() ? RulesExt::Global()->BattlePoints_DefaultFriendlyValue.Get() : 0; + + int points = pThis->IsAlliedWith(pTechno)? defaultFriendlyValue : defaultValue; points = points == 0 && pTechnoTypeExt->BattlePoints.isset() ? pTechnoTypeExt->BattlePoints.Get() : points; points = points == 0 && pThisTypeExt->BattlePoints_CanReuseStandardPoints ? pTechno->GetTechnoType()->Points : points; diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp index e3b54bac6c..6d3b961e6b 100644 --- a/src/Ext/HouseType/Body.cpp +++ b/src/Ext/HouseType/Body.cpp @@ -23,7 +23,7 @@ void HouseTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) INI_EX exINI(pINI); this->BattlePoints.Read(exINI, pSection, "BattlePoints"); - this->BattlePoints_CanReuseStandardPoints.Read(exINI, pSection, "BattlePoints.CanUseStandardPoints"); + this->BattlePoints_CanReuseStandardPoints.Read(exINI, pSection, "BattlePoints.CanReuseStandardPoints"); } void HouseTypeExt::ExtData::CompleteInitialization() diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 6e7ace41e5..8d09c5f393 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -268,7 +268,8 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->HarvesterScanAfterUnload.Read(exINI, GameStrings::General, "HarvesterScanAfterUnload"); this->BattlePoints.Read(exINI, GameStrings::General, "BattlePoints"); - this->BattlePoints_DefaultKillValue.Read(exINI, GameStrings::General, "BattlePoints.DefaultKillValue"); + this->BattlePoints_DefaultValue.Read(exINI, GameStrings::General, "BattlePoints.DefaultValue"); + this->BattlePoints_DefaultFriendlyValue.Read(exINI, GameStrings::General, "BattlePoints.DefaultFriendlyValue"); // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); @@ -493,7 +494,8 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->DamagedSpeed) .Process(this->HarvesterScanAfterUnload) .Process(this->BattlePoints) - .Process(this->BattlePoints_DefaultKillValue) + .Process(this->BattlePoints_DefaultValue) + .Process(this->BattlePoints_DefaultFriendlyValue) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 04a5781c74..be32fe23d6 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -221,7 +221,8 @@ class RulesExt Valueable HarvesterScanAfterUnload; Nullable BattlePoints; - Nullable BattlePoints_DefaultKillValue; + Nullable BattlePoints_DefaultValue; + Nullable BattlePoints_DefaultFriendlyValue; ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } @@ -390,7 +391,8 @@ class RulesExt , HarvesterScanAfterUnload { false } , BattlePoints {} - , BattlePoints_DefaultKillValue {} + , BattlePoints_DefaultValue {} + , BattlePoints_DefaultFriendlyValue {} { } virtual ~ExtData() = default; diff --git a/src/Ext/Side/Body.cpp b/src/Ext/Side/Body.cpp index d763574cb1..a4dcfd3458 100644 --- a/src/Ext/Side/Body.cpp +++ b/src/Ext/Side/Body.cpp @@ -38,6 +38,9 @@ void SideExt::ExtData::LoadFromINIFile(CCINIClass* pINI) this->Sidebar_PowerDelta_Red.Read(exINI, pSection, "Sidebar.PowerDelta.ColorRed"); this->Sidebar_PowerDelta_Grey.Read(exINI, pSection, "Sidebar.PowerDelta.ColorGrey"); this->Sidebar_PowerDelta_Align.Read(exINI, pSection, "Sidebar.PowerDelta.Align"); + this->Sidebar_BattlePoints_Offset.Read(exINI, pSection, "Sidebar.BattlePoints.Offset"); + this->Sidebar_BattlePoints_Color.Read(exINI, pSection, "Sidebar.BattlePoints.Color"); + this->Sidebar_BattlePoints_Align.Read(exINI, pSection, "Sidebar.BattlePoints.Align"); this->ToolTip_Background_Color.Read(exINI, pSection, "ToolTip.Background.Color"); this->ToolTip_Background_Opacity.Read(exINI, pSection, "ToolTip.Background.Opacity"); this->ToolTip_Background_BlurSize.Read(exINI, pSection, "ToolTip.Background.BlurSize"); @@ -65,6 +68,9 @@ void SideExt::ExtData::Serialize(T& Stm) .Process(this->Sidebar_PowerDelta_Red) .Process(this->Sidebar_PowerDelta_Grey) .Process(this->Sidebar_PowerDelta_Align) + .Process(this->Sidebar_BattlePoints_Offset) + .Process(this->Sidebar_BattlePoints_Color) + .Process(this->Sidebar_BattlePoints_Align) .Process(this->ToolTip_Background_Color) .Process(this->ToolTip_Background_Opacity) .Process(this->ToolTip_Background_BlurSize) diff --git a/src/Ext/Side/Body.h b/src/Ext/Side/Body.h index 88e694aa0c..2271225cbe 100644 --- a/src/Ext/Side/Body.h +++ b/src/Ext/Side/Body.h @@ -32,6 +32,9 @@ class SideExt Valueable Sidebar_PowerDelta_Red; Valueable Sidebar_PowerDelta_Grey; Valueable Sidebar_PowerDelta_Align; + Valueable Sidebar_BattlePoints_Offset; + Nullable Sidebar_BattlePoints_Color; + Valueable Sidebar_BattlePoints_Align; Nullable ToolTip_Background_Color; Nullable ToolTip_Background_Opacity; Nullable ToolTip_Background_BlurSize; @@ -54,6 +57,9 @@ class SideExt , Sidebar_PowerDelta_Red { { 255, 0, 0 } } , Sidebar_PowerDelta_Grey { { 0x80,0x80,0x80 } } , Sidebar_PowerDelta_Align { TextAlign::Left } + , Sidebar_BattlePoints_Offset { { 0, 0 } } + , Sidebar_BattlePoints_Color {} + , Sidebar_BattlePoints_Align { TextAlign::Left } , ToolTip_Background_Color { } , ToolTip_Background_Opacity { } , ToolTip_Background_BlurSize { } diff --git a/src/Ext/Techno/Body.Internal.cpp b/src/Ext/Techno/Body.Internal.cpp index b27da9b18a..37b0519d43 100644 --- a/src/Ext/Techno/Body.Internal.cpp +++ b/src/Ext/Techno/Body.Internal.cpp @@ -23,21 +23,17 @@ void TechnoExt::ExtData::InitializeLaserTrails() } } -void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller) +void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller, HouseClass* pHouseKiller) { - TechnoClass* pObjectKiller = ((pKiller->GetTechnoType()->Spawned || pKiller->GetTechnoType()->MissileSpawn) && pKiller->SpawnOwner) ? - pKiller->SpawnOwner : pKiller; + TechnoClass* pObjectKiller = nullptr; - if (pObjectKiller && pKiller->Owner != pVictim->Owner && !pKiller->Owner->IsAlliedWith(pVictim)) + if (pKiller) { - auto pOwner = pKiller->Owner; - auto pOwnerExt = HouseExt::ExtMap.Find(pOwner); + pObjectKiller = ((pKiller->GetTechnoType()->Spawned || pKiller->GetTechnoType()->MissileSpawn) && pKiller->SpawnOwner) ? + pKiller->SpawnOwner : pKiller; - if (pOwnerExt->AreBattlePointsEnabled()) - { - int points = pOwnerExt->CalculateBattlePoints(pVictim); - pOwnerExt->UpdateBattlePoints(points); - } + if (!pObjectKiller) + return; } if (pObjectKiller && pObjectKiller->BelongsToATeam()) @@ -48,6 +44,19 @@ void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller) pKillerTechnoData->LastKillWasTeamTarget = pFootKiller->Team->Focus == pVictim; } } + + HouseClass* pHouse = pKiller ? pKiller->Owner : pHouseKiller; + + if (pHouse != pVictim->Owner) + { + auto pHouseExt = HouseExt::ExtMap.Find(pHouse); + + if (pHouseExt->AreBattlePointsEnabled()) + { + int points = pHouseExt->CalculateBattlePoints(pVictim); + pHouseExt->UpdateBattlePoints(points); + } + } } // reversed from 6F3D60 diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 11e4c189a7..323114069f 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -190,7 +190,7 @@ class TechnoExt static void ChangeOwnerMissionFix(FootClass* pThis); static void KillSelf(TechnoClass* pThis, AutoDeathBehavior deathOption, AnimTypeClass* pVanishAnimation, bool isInLimbo = false); - static void ObjectKilledBy(TechnoClass* pThis, TechnoClass* pKiller); + static void ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller = nullptr, HouseClass* pHouseKiller = nullptr); static void UpdateSharedAmmo(TechnoClass* pThis); static double GetCurrentSpeedMultiplier(FootClass* pThis); static void DrawSelfHealPips(TechnoClass* pThis, Point2D* pLocation, RectangleStruct* pBounds); diff --git a/src/Ext/Techno/Hooks.cpp b/src/Ext/Techno/Hooks.cpp index 8185d49a08..7da9071a13 100644 --- a/src/Ext/Techno/Hooks.cpp +++ b/src/Ext/Techno/Hooks.cpp @@ -484,12 +484,24 @@ DEFINE_HOOK(0x702E4E, TechnoClass_RegisterDestruction_SaveKillerInfo, 0x6) GET(TechnoClass*, pKiller, EDI); GET(TechnoClass*, pVictim, ECX); + // Note: Some SW never had a "killer" or a "house" (hello "NukeSpecial"), probably never scored to the killer? if (pKiller && pVictim) TechnoExt::ObjectKilledBy(pVictim, pKiller); return 0; } +// AFAIK, only used by the teleport of the Chronoshift SW +DEFINE_HOOK(0x70337D, HouseClass_RegisterDestruction_SaveKillerInfo, 0x6) +{ + GET(HouseClass*, pHouse, EDI); + GET(TechnoClass*, pVictim, ESI); + + TechnoExt::ObjectKilledBy(pVictim, nullptr, pHouse); + + return 0; +} + DEFINE_HOOK(0x71067B, TechnoClass_EnterTransport_LaserTrails, 0x7) { GET(TechnoClass*, pTechno, EDI); diff --git a/src/Misc/Hooks.UI.cpp b/src/Misc/Hooks.UI.cpp index 5516f7f22a..8e0364ce4a 100644 --- a/src/Misc/Hooks.UI.cpp +++ b/src/Misc/Hooks.UI.cpp @@ -162,6 +162,32 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) TextPrintType::UseGradPal | TextPrintType::Center | TextPrintType::Metal12); } + if (Phobos::UI::BattlePoints_Show && Phobos::Config::ShowBattlePoints) + { + auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.GetItem(pPlayer->SideIndex)); + wchar_t counter[0x20]; + + ColorStruct clrToolTip = pSideExt->Sidebar_BattlePoints_Color.Get(Drawing::TooltipColor); + + auto pHouseExt = HouseExt::ExtMap.Find(pPlayer); + int points = pHouseExt->BattlePoints; + + if (Phobos::UI::BattlePointsSidebarLabel_InvertPosition) + swprintf_s(counter, L"%d %ls", points, Phobos::UI::BattlePointsSidebarLabel); + else + swprintf_s(counter, L"%ls %d", Phobos::UI::BattlePointsSidebarLabel, points); + + Point2D vPos = { + DSurface::Sidebar->GetWidth() / 2 - 70 + pSideExt->Sidebar_BattlePoints_Offset.Get().X, + 2 + pSideExt->Sidebar_BattlePoints_Offset.Get().Y + }; + + auto const TextFlags = static_cast(static_cast(TextPrintType::UseGradPal | TextPrintType::Metal12) + | static_cast(pSideExt->Sidebar_BattlePoints_Align.Get())); + + DSurface::Sidebar->DrawText(counter, &vRect, &vPos, Drawing::RGB_To_Int(clrToolTip), 0, TextFlags); + } + return 0; } diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index 81a1575713..ec1f13c737 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -28,6 +28,8 @@ const wchar_t* Phobos::UI::HarvesterLabel = L""; const wchar_t* Phobos::UI::ShowBriefingResumeButtonLabel = L""; const wchar_t* Phobos::UI::SWShotsFormat = L""; const wchar_t* Phobos::UI::BattlePointsLabel = L""; +const wchar_t* Phobos::UI::BattlePointsSidebarLabel = L""; +bool Phobos::UI::BattlePointsSidebarLabel_InvertPosition = false; char Phobos::UI::ShowBriefingResumeButtonStatusLabel[32]; bool Phobos::UI::PowerDelta_Show = false; double Phobos::UI::PowerDelta_ConditionYellow = 0.75; @@ -35,6 +37,7 @@ double Phobos::UI::PowerDelta_ConditionRed = 1.0; bool Phobos::UI::CenterPauseMenuBackground = false; bool Phobos::UI::WeedsCounter_Show = false; bool Phobos::UI::AnchoredToolTips = false; +bool Phobos::UI::BattlePoints_Show = true; bool Phobos::Config::ToolTipDescriptions = true; bool Phobos::Config::ToolTipBlur = false; @@ -59,6 +62,7 @@ bool Phobos::Config::ShowWeedsCounter = false; bool Phobos::Config::HideLightFlashEffects = true; bool Phobos::Config::ShowFlashOnSelecting = false; bool Phobos::Config::UnitPowerDrain = false; +bool Phobos::Config::ShowBattlePoints = false; bool Phobos::Misc::CustomGS = false; int Phobos::Misc::CustomGS_ChangeInterval[7] = { -1, -1, -1, -1, -1, -1, -1 }; @@ -82,6 +86,7 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::Config::ShowWeedsCounter = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowWeedsCounter", true); Phobos::Config::HideLightFlashEffects = CCINIClass::INI_RA2MD.ReadBool("Phobos", "HideLightFlashEffects", false); Phobos::Config::ShowFlashOnSelecting = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowFlashOnSelecting", false); + Phobos::Config::ShowBattlePoints = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowBattlePoints", true); // Custom game speeds, 6 - i so that GS6 is index 0, just like in the engine Phobos::Config::CampaignDefaultGameSpeed = 6 - CCINIClass::INI_RA2MD.ReadInteger("Phobos", "CampaignDefaultGameSpeed", 4); @@ -135,7 +140,7 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::UI::SWShotsFormat = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"Shots: %d"); // ⌚ ini_uimd.ReadString(GameStrings::ToolTips, "BattlePointsLabel", NONE_STR, Phobos::readBuffer); - Phobos::UI::BattlePointsLabel = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"BP: "); + Phobos::UI::BattlePointsLabel = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"\u2605: "); // ★: } // Sidebar @@ -169,6 +174,14 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::UI::CenterPauseMenuBackground = ini_uimd.ReadBool(SIDEBAR_SECTION, "CenterPauseMenuBackground", Phobos::UI::CenterPauseMenuBackground); + + Phobos::UI::BattlePoints_Show = + ini_uimd.ReadBool(SIDEBAR_SECTION, "BattlePoints.Show", true); + + Phobos::UI::BattlePointsSidebarLabel_InvertPosition = ini_uimd.ReadBool(SIDEBAR_SECTION, "BattlePointsSidebarLabel.InvertPosition", false); + + ini_uimd.ReadString(SIDEBAR_SECTION, "BattlePointsSidebar.Label", NONE_STR, Phobos::readBuffer); + Phobos::UI::BattlePointsSidebarLabel = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"\u2605"); // %d ★ } // UISettings diff --git a/src/Phobos.h b/src/Phobos.h index 6930ea2754..173c588784 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -56,6 +56,7 @@ class Phobos static bool CenterPauseMenuBackground; static bool WeedsCounter_Show; static bool AnchoredToolTips; + static bool BattlePoints_Show; static const wchar_t* CostLabel; static const wchar_t* PowerLabel; @@ -66,6 +67,8 @@ class Phobos static const wchar_t* SWShotsFormat; static char ShowBriefingResumeButtonStatusLabel[0x20]; static const wchar_t* BattlePointsLabel; + static const wchar_t* BattlePointsSidebarLabel; + static bool BattlePointsSidebarLabel_InvertPosition; }; class Config @@ -95,6 +98,7 @@ class Phobos static bool HideLightFlashEffects; static bool ShowFlashOnSelecting; static bool UnitPowerDrain; + static bool ShowBattlePoints; }; class Misc From c69bb45bf5961bc1b4c740174e0edb2733b87c6b Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 15 May 2025 10:02:55 +0200 Subject: [PATCH 04/19] - Now AI-controlled super weapons managed by Ares dll will obey the points requisite. - Removed code used for optionally show this value. Now depends on the activation of the feature, since is weird to hide the current gathered points having a requisite in a SW that needs it. --- src/Commands/ObjectInfo.cpp | 5 ++++ src/Ext/SWType/Hooks.cpp | 33 ++++++++++++++++++----- src/Misc/Hooks.UI.cpp | 52 ++++++++++++++++++------------------- src/Misc/PhobosToolTip.cpp | 17 +++++++----- src/Phobos.INI.cpp | 6 ----- src/Phobos.h | 2 -- 6 files changed, 67 insertions(+), 48 deletions(-) diff --git a/src/Commands/ObjectInfo.cpp b/src/Commands/ObjectInfo.cpp index 58db77f544..d3fa9e1ba3 100644 --- a/src/Commands/ObjectInfo.cpp +++ b/src/Commands/ObjectInfo.cpp @@ -13,6 +13,7 @@ #include #include +#include const char* ObjectInfoCommandClass::GetName() const { @@ -152,6 +153,8 @@ void ObjectInfoCommandClass::Execute(WWKey eInput) const append(", Ammo = (%d / %d)", pFoot->Ammo, pType->Ammo); append("\n"); + const auto pOwnerExt = HouseExt::ExtMap.Find(pFoot->Owner); + append("BP: %d\n", pOwnerExt->BattlePoints); display(); }; @@ -217,6 +220,8 @@ void ObjectInfoCommandClass::Execute(WWKey eInput) const if (pTechnoExt->CurrentShieldType && pShieldData) append("Current Shield HP = (%d / %d)\n", pShieldData->GetHP(), pTechnoExt->CurrentShieldType->Strength); + const auto pOwnerExt = HouseExt::ExtMap.Find(pBuilding->Owner); + append("BP: %d\n", pOwnerExt->BattlePoints); display(); }; diff --git a/src/Ext/SWType/Hooks.cpp b/src/Ext/SWType/Hooks.cpp index 9fbb94c3f7..fa80c94dd3 100644 --- a/src/Ext/SWType/Hooks.cpp +++ b/src/Ext/SWType/Hooks.cpp @@ -267,6 +267,7 @@ DEFINE_HOOK(0x6AC67A, SidebarClass_6AC5F0_TabIndex, 0x5) DEFINE_JUMP(LJMP, 0x6A8D07, 0x6A8D17) // Skip tabIndex check #pragma endregion +// Full rewrite DEFINE_HOOK(0x6CC367, SuperClass_IsReady_BattlePoints, 0xD) { GET(SuperClass*, pSuper, ECX); @@ -282,15 +283,33 @@ DEFINE_HOOK(0x6CC367, SuperClass_IsReady_BattlePoints, 0xD) return SkipAll; } - const auto pOwner = pSuper->Owner; - auto pOwnerExt = HouseExt::ExtMap.Find(pOwner); - auto pExt = SWTypeExt::ExtMap.Find(pSuper->Type); - - if (pExt->BattlePoints_Amount > 0) + if (RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get()) { - if (pOwnerExt->BattlePoints < pExt->BattlePoints_Amount) - return ReturnZero; + auto pOwnerExt = HouseExt::ExtMap.Find(pSuper->Owner); + auto pExt = SWTypeExt::ExtMap.Find(pSuper->Type); + + if (pExt->BattlePoints_Amount > 0) + { + if (pOwnerExt->BattlePoints < pExt->BattlePoints_Amount) + return ReturnZero; + } } return ReturnIsReady; } + +// Executed before the Ares hook for launching AI super weapons, SW->IsReady property won't be updated anymore +DEFINE_HOOK(0x4FD77C, ExpertAI_SuperWeaponAI_RecheckIsReady, 0x5) +{ + GET(HouseClass*, pHouse, EBX); + if (!SessionClass::IsCampaign() || pHouse->IQLevel2 >= RulesClass::Instance->SuperWeapons) + { + for (auto const& pSuper : pHouse->Supers) + { + if (pSuper->IsReady) + pSuper->IsReady = pSuper->GetIsReady(); + } + } + + return 0; +} diff --git a/src/Misc/Hooks.UI.cpp b/src/Misc/Hooks.UI.cpp index 8e0364ce4a..eeae360794 100644 --- a/src/Misc/Hooks.UI.cpp +++ b/src/Misc/Hooks.UI.cpp @@ -84,6 +84,32 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) RectangleStruct vRect = DSurface::Sidebar->GetRect(); + if (RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get()) + { + auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.GetItem(pPlayer->SideIndex)); + wchar_t counter[0x20]; + + ColorStruct clrToolTip = pSideExt->Sidebar_BattlePoints_Color.Get(Drawing::TooltipColor); + + auto pHouseExt = HouseExt::ExtMap.Find(pPlayer); + int points = pHouseExt->BattlePoints; + + if (Phobos::UI::BattlePointsSidebarLabel_InvertPosition) + swprintf_s(counter, L"%d %ls", points, Phobos::UI::BattlePointsSidebarLabel); + else + swprintf_s(counter, L"%ls %d", Phobos::UI::BattlePointsSidebarLabel, points); + + Point2D vPos = { + DSurface::Sidebar->GetWidth() / 2 - 70 + pSideExt->Sidebar_BattlePoints_Offset.Get().X, + 2 + pSideExt->Sidebar_BattlePoints_Offset.Get().Y + }; + + auto const TextFlags = static_cast(static_cast(TextPrintType::UseGradPal | TextPrintType::Metal12) + | static_cast(pSideExt->Sidebar_BattlePoints_Align.Get())); + + DSurface::Sidebar->DrawText(counter, &vRect, &vPos, Drawing::RGB_To_Int(clrToolTip), 0, TextFlags); + } + if (Phobos::UI::HarvesterCounter_Show && Phobos::Config::ShowHarvesterCounter) { auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.GetItem(pPlayer->SideIndex)); @@ -162,32 +188,6 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) TextPrintType::UseGradPal | TextPrintType::Center | TextPrintType::Metal12); } - if (Phobos::UI::BattlePoints_Show && Phobos::Config::ShowBattlePoints) - { - auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.GetItem(pPlayer->SideIndex)); - wchar_t counter[0x20]; - - ColorStruct clrToolTip = pSideExt->Sidebar_BattlePoints_Color.Get(Drawing::TooltipColor); - - auto pHouseExt = HouseExt::ExtMap.Find(pPlayer); - int points = pHouseExt->BattlePoints; - - if (Phobos::UI::BattlePointsSidebarLabel_InvertPosition) - swprintf_s(counter, L"%d %ls", points, Phobos::UI::BattlePointsSidebarLabel); - else - swprintf_s(counter, L"%ls %d", Phobos::UI::BattlePointsSidebarLabel, points); - - Point2D vPos = { - DSurface::Sidebar->GetWidth() / 2 - 70 + pSideExt->Sidebar_BattlePoints_Offset.Get().X, - 2 + pSideExt->Sidebar_BattlePoints_Offset.Get().Y - }; - - auto const TextFlags = static_cast(static_cast(TextPrintType::UseGradPal | TextPrintType::Metal12) - | static_cast(pSideExt->Sidebar_BattlePoints_Align.Get())); - - DSurface::Sidebar->DrawText(counter, &vRect, &vPos, Drawing::RGB_To_Int(clrToolTip), 0, TextFlags); - } - return 0; } diff --git a/src/Misc/PhobosToolTip.cpp b/src/Misc/PhobosToolTip.cpp index e94e2d6261..1179ebfce9 100644 --- a/src/Misc/PhobosToolTip.cpp +++ b/src/Misc/PhobosToolTip.cpp @@ -181,16 +181,19 @@ void PhobosToolTip::HelpText_Super(int swidx) showSth = true; } - if (int nPoints = std::abs(pData->BattlePoints_Amount)) + if (RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get()) { - oss << L"\n"; + if (int nPoints = std::abs(pData->BattlePoints_Amount)) + { + oss << L"\n"; - if (pData->BattlePoints_Amount > 0) - oss << Phobos::UI::BattlePointsLabel << L"-" << nPoints; - else if (pData->BattlePoints_Amount < 0) - oss << Phobos::UI::BattlePointsLabel << L"+" << nPoints; + if (pData->BattlePoints_Amount > 0) + oss << Phobos::UI::BattlePointsLabel << L"-" << nPoints; + else if (pData->BattlePoints_Amount < 0) + oss << Phobos::UI::BattlePointsLabel << L"+" << nPoints; - showSth = true; + showSth = true; + } } int rechargeTime = TickTimeToSeconds(pSuper->GetRechargeTime()); diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index ec1f13c737..d7987a85d4 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -37,7 +37,6 @@ double Phobos::UI::PowerDelta_ConditionRed = 1.0; bool Phobos::UI::CenterPauseMenuBackground = false; bool Phobos::UI::WeedsCounter_Show = false; bool Phobos::UI::AnchoredToolTips = false; -bool Phobos::UI::BattlePoints_Show = true; bool Phobos::Config::ToolTipDescriptions = true; bool Phobos::Config::ToolTipBlur = false; @@ -62,7 +61,6 @@ bool Phobos::Config::ShowWeedsCounter = false; bool Phobos::Config::HideLightFlashEffects = true; bool Phobos::Config::ShowFlashOnSelecting = false; bool Phobos::Config::UnitPowerDrain = false; -bool Phobos::Config::ShowBattlePoints = false; bool Phobos::Misc::CustomGS = false; int Phobos::Misc::CustomGS_ChangeInterval[7] = { -1, -1, -1, -1, -1, -1, -1 }; @@ -86,7 +84,6 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::Config::ShowWeedsCounter = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowWeedsCounter", true); Phobos::Config::HideLightFlashEffects = CCINIClass::INI_RA2MD.ReadBool("Phobos", "HideLightFlashEffects", false); Phobos::Config::ShowFlashOnSelecting = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowFlashOnSelecting", false); - Phobos::Config::ShowBattlePoints = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowBattlePoints", true); // Custom game speeds, 6 - i so that GS6 is index 0, just like in the engine Phobos::Config::CampaignDefaultGameSpeed = 6 - CCINIClass::INI_RA2MD.ReadInteger("Phobos", "CampaignDefaultGameSpeed", 4); @@ -175,9 +172,6 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::UI::CenterPauseMenuBackground = ini_uimd.ReadBool(SIDEBAR_SECTION, "CenterPauseMenuBackground", Phobos::UI::CenterPauseMenuBackground); - Phobos::UI::BattlePoints_Show = - ini_uimd.ReadBool(SIDEBAR_SECTION, "BattlePoints.Show", true); - Phobos::UI::BattlePointsSidebarLabel_InvertPosition = ini_uimd.ReadBool(SIDEBAR_SECTION, "BattlePointsSidebarLabel.InvertPosition", false); ini_uimd.ReadString(SIDEBAR_SECTION, "BattlePointsSidebar.Label", NONE_STR, Phobos::readBuffer); diff --git a/src/Phobos.h b/src/Phobos.h index 173c588784..45bd0d5377 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -56,7 +56,6 @@ class Phobos static bool CenterPauseMenuBackground; static bool WeedsCounter_Show; static bool AnchoredToolTips; - static bool BattlePoints_Show; static const wchar_t* CostLabel; static const wchar_t* PowerLabel; @@ -98,7 +97,6 @@ class Phobos static bool HideLightFlashEffects; static bool ShowFlashOnSelecting; static bool UnitPowerDrain; - static bool ShowBattlePoints; }; class Misc From d440d4e3d7d3da17a8b94e046f15aec30612a929 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 15 May 2025 14:07:36 +0200 Subject: [PATCH 05/19] Documentation and bugfixes - Fixed bugs - Added documentation --- docs/New-or-Enhanced-Logics.md | 48 ++++++++++++++++++++++++++++++++++ src/Ext/House/Body.cpp | 6 +++-- src/Ext/HouseType/Body.cpp | 4 +-- src/Ext/HouseType/Body.h | 4 +-- src/Ext/SWType/Hooks.cpp | 6 ++--- src/Misc/Hooks.UI.cpp | 10 +++---- src/Misc/PhobosToolTip.cpp | 17 +++++------- src/Phobos.INI.cpp | 14 +++++----- src/Phobos.h | 6 ++--- 9 files changed, 81 insertions(+), 34 deletions(-) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 7d3f228f5f..6991c066b1 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -983,6 +983,54 @@ In `rulesmd.ini`: AISuperWeaponDelay= ; integer, game frames ``` +### Battle Points counter +- This system displayes a new currency to be used (optionally) in Super Weapons. +- The new currency will be modified by the kills against other players and the system is enabled. +- If set `BattlePointsGenerator` in any structure then the system will be enabled if the structure is built. +- If set `BattlePoints` in houses they will have the system enabled by default. +- If set [General] -> `BattlePoints` enables/disables the system for every player in the scenario, overriding the country preferences & `BattlePointsGenerator` setting. +- If set [General] -> `BattlePoints.DefaultValue` and the enemy destroyed object doesn't have a value then this generic value will be used instead. +- If set [General] -> `BattlePoints.DefaultFriendlyValue` and the friendly destroyed object doesn't have a value then this generic value will be used instead. This tag doesn't work with the own objects of the player. +- If is set `BattlePoints.CanUseStandardPoints` and the destroyed object doesn't have the `BattlePoints` value then the points are obtained from the `Points` tag of the destroyed object. If `BattlePoints.DefaultValue` is present then this will be ignored. +- If set `BattlePoints` in the destroyed object the calculation is made with this value. Self-Kills done by the own player doesn't count in this system. +- If `BattlePoints.Amount` is set with a value different from `0` then the super weapon is influenced by this system. If the value is positive then this super weapon won't be launched until the affected player gathered the required ammount. + +In `uimd.ini`: +```ini +[Sidebar] +BattlePointsSidebar.Label= ; CSF entry key `★ `, code U+2605 +BattlePointsSidebar.Label.InvertPosition=false ; bool, ` ★`, code U+2605 + +[ToolTips] +BattlePoints.Label= ; CSF entry key, default to `★: `, code U+2605 +``` + +In `rulesmd.ini`: +```ini +[General] +BattlePoints= ; bool +BattlePoints.DefaultValue= ; int +BattlePoints.DefaultFriendlyValue= ; int + +[SOMESIDE] ; Side +Sidebar.BattlePoints.Offset=0,0 ; X,Y, pixels relative to default +Sidebar.BattlePoints.Color= ; integer - R,G,B +Sidebar.BattlePoints.Align=Left ; Left, Right, Center/Centre + +[SOMECOUNTRY] ; HouseType +BattlePoints=false ; bool +BattlePoints.CanUseStandardPoints=false ; bool + +[SOMEBUILDING] ; BuildingType +BattlePointsGenerator= ; bool + +[SOMETECHNO] ; TechnoType +BattlePoints= ; int + +[SOMESW] +BattlePoints.Amount=0 ; int +``` + ### Convert TechnoType - Warheads can now change TechnoTypes of affected units to other Types in the same category (infantry to infantry, vehicles to vehicles, aircraft to aircraft). diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 374515ac70..ba6ace341d 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -1110,7 +1110,9 @@ bool HouseExt::ExtData::AreBattlePointsEnabled() } enabledBattlePoints |= pOwnerTypeExt->BattlePoints; - enabledBattlePoints |= RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get(); + + if (RulesExt::Global()->BattlePoints.isset()) + enabledBattlePoints = RulesExt::Global()->BattlePoints.Get(); return enabledBattlePoints; } @@ -1129,7 +1131,7 @@ int HouseExt::ExtData::CalculateBattlePoints(TechnoClass* pTechno) int points = pThis->IsAlliedWith(pTechno)? defaultFriendlyValue : defaultValue; points = points == 0 && pTechnoTypeExt->BattlePoints.isset() ? pTechnoTypeExt->BattlePoints.Get() : points; - points = points == 0 && pThisTypeExt->BattlePoints_CanReuseStandardPoints ? pTechno->GetTechnoType()->Points : points; + points = points == 0 && pThisTypeExt->BattlePoints_CanUseStandardPoints ? pTechno->GetTechnoType()->Points : points; return points; } diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp index 6d3b961e6b..030bf86454 100644 --- a/src/Ext/HouseType/Body.cpp +++ b/src/Ext/HouseType/Body.cpp @@ -23,7 +23,7 @@ void HouseTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) INI_EX exINI(pINI); this->BattlePoints.Read(exINI, pSection, "BattlePoints"); - this->BattlePoints_CanReuseStandardPoints.Read(exINI, pSection, "BattlePoints.CanReuseStandardPoints"); + this->BattlePoints_CanUseStandardPoints.Read(exINI, pSection, "BattlePoints.CanUseStandardPoints"); } void HouseTypeExt::ExtData::CompleteInitialization() @@ -37,7 +37,7 @@ void HouseTypeExt::ExtData::Serialize(T& Stm) { Stm .Process(this->BattlePoints) - .Process(this->BattlePoints_CanReuseStandardPoints) + .Process(this->BattlePoints_CanUseStandardPoints) ; } diff --git a/src/Ext/HouseType/Body.h b/src/Ext/HouseType/Body.h index 5940d6843c..fbe78a919c 100644 --- a/src/Ext/HouseType/Body.h +++ b/src/Ext/HouseType/Body.h @@ -15,11 +15,11 @@ class HouseTypeExt { public: Valueable BattlePoints; - Valueable BattlePoints_CanReuseStandardPoints; + Valueable BattlePoints_CanUseStandardPoints; ExtData(HouseTypeClass* OwnerObject) : Extension(OwnerObject) , BattlePoints { false } - , BattlePoints_CanReuseStandardPoints { false } + , BattlePoints_CanUseStandardPoints { false } { } virtual ~ExtData() = default; diff --git a/src/Ext/SWType/Hooks.cpp b/src/Ext/SWType/Hooks.cpp index fa80c94dd3..677736e753 100644 --- a/src/Ext/SWType/Hooks.cpp +++ b/src/Ext/SWType/Hooks.cpp @@ -283,10 +283,10 @@ DEFINE_HOOK(0x6CC367, SuperClass_IsReady_BattlePoints, 0xD) return SkipAll; } - if (RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get()) + const auto pExt = SWTypeExt::ExtMap.Find(pSuper->Type); + if (pExt->BattlePoints_Amount != 0) { - auto pOwnerExt = HouseExt::ExtMap.Find(pSuper->Owner); - auto pExt = SWTypeExt::ExtMap.Find(pSuper->Type); + const auto pOwnerExt = HouseExt::ExtMap.Find(pSuper->Owner); if (pExt->BattlePoints_Amount > 0) { diff --git a/src/Misc/Hooks.UI.cpp b/src/Misc/Hooks.UI.cpp index eeae360794..fd5daaf01c 100644 --- a/src/Misc/Hooks.UI.cpp +++ b/src/Misc/Hooks.UI.cpp @@ -83,21 +83,21 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) return 0; RectangleStruct vRect = DSurface::Sidebar->GetRect(); + auto pHouseExt = HouseExt::ExtMap.Find(pPlayer); - if (RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get()) + if (pHouseExt->AreBattlePointsEnabled()) { auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.GetItem(pPlayer->SideIndex)); wchar_t counter[0x20]; ColorStruct clrToolTip = pSideExt->Sidebar_BattlePoints_Color.Get(Drawing::TooltipColor); - auto pHouseExt = HouseExt::ExtMap.Find(pPlayer); int points = pHouseExt->BattlePoints; - if (Phobos::UI::BattlePointsSidebarLabel_InvertPosition) - swprintf_s(counter, L"%d %ls", points, Phobos::UI::BattlePointsSidebarLabel); + if (Phobos::UI::BattlePointsSidebar_Label_InvertPosition) + swprintf_s(counter, L"%d %ls", points, Phobos::UI::BattlePointsSidebar_Label); else - swprintf_s(counter, L"%ls %d", Phobos::UI::BattlePointsSidebarLabel, points); + swprintf_s(counter, L"%ls %d", Phobos::UI::BattlePointsSidebar_Label, points); Point2D vPos = { DSurface::Sidebar->GetWidth() / 2 - 70 + pSideExt->Sidebar_BattlePoints_Offset.Get().X, diff --git a/src/Misc/PhobosToolTip.cpp b/src/Misc/PhobosToolTip.cpp index 1179ebfce9..acda70b954 100644 --- a/src/Misc/PhobosToolTip.cpp +++ b/src/Misc/PhobosToolTip.cpp @@ -181,19 +181,16 @@ void PhobosToolTip::HelpText_Super(int swidx) showSth = true; } - if (RulesExt::Global()->BattlePoints.isset() && RulesExt::Global()->BattlePoints.Get()) + if (int nPoints = std::abs(pData->BattlePoints_Amount)) { - if (int nPoints = std::abs(pData->BattlePoints_Amount)) - { - oss << L"\n"; + oss << L"\n"; - if (pData->BattlePoints_Amount > 0) - oss << Phobos::UI::BattlePointsLabel << L"-" << nPoints; - else if (pData->BattlePoints_Amount < 0) - oss << Phobos::UI::BattlePointsLabel << L"+" << nPoints; + if (pData->BattlePoints_Amount > 0) + oss << Phobos::UI::BattlePoints_Label << L"-" << nPoints; + else if (pData->BattlePoints_Amount < 0) + oss << Phobos::UI::BattlePoints_Label << L"+" << nPoints; - showSth = true; - } + showSth = true; } int rechargeTime = TickTimeToSeconds(pSuper->GetRechargeTime()); diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index d7987a85d4..1baf244003 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -27,9 +27,9 @@ const wchar_t* Phobos::UI::TimeLabel = L""; const wchar_t* Phobos::UI::HarvesterLabel = L""; const wchar_t* Phobos::UI::ShowBriefingResumeButtonLabel = L""; const wchar_t* Phobos::UI::SWShotsFormat = L""; -const wchar_t* Phobos::UI::BattlePointsLabel = L""; -const wchar_t* Phobos::UI::BattlePointsSidebarLabel = L""; -bool Phobos::UI::BattlePointsSidebarLabel_InvertPosition = false; +const wchar_t* Phobos::UI::BattlePoints_Label = L""; +const wchar_t* Phobos::UI::BattlePointsSidebar_Label = L""; +bool Phobos::UI::BattlePointsSidebar_Label_InvertPosition = false; char Phobos::UI::ShowBriefingResumeButtonStatusLabel[32]; bool Phobos::UI::PowerDelta_Show = false; double Phobos::UI::PowerDelta_ConditionYellow = 0.75; @@ -136,8 +136,8 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) ini_uimd.ReadString(GameStrings::ToolTips, "SWShotsFormat", NONE_STR, Phobos::readBuffer); Phobos::UI::SWShotsFormat = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"Shots: %d"); // ⌚ - ini_uimd.ReadString(GameStrings::ToolTips, "BattlePointsLabel", NONE_STR, Phobos::readBuffer); - Phobos::UI::BattlePointsLabel = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"\u2605: "); // ★: + ini_uimd.ReadString(GameStrings::ToolTips, "BattlePoints.Label", NONE_STR, Phobos::readBuffer); + Phobos::UI::BattlePoints_Label = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"\u2605: "); // ★: } // Sidebar @@ -172,10 +172,10 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::UI::CenterPauseMenuBackground = ini_uimd.ReadBool(SIDEBAR_SECTION, "CenterPauseMenuBackground", Phobos::UI::CenterPauseMenuBackground); - Phobos::UI::BattlePointsSidebarLabel_InvertPosition = ini_uimd.ReadBool(SIDEBAR_SECTION, "BattlePointsSidebarLabel.InvertPosition", false); + Phobos::UI::BattlePointsSidebar_Label_InvertPosition = ini_uimd.ReadBool(SIDEBAR_SECTION, "BattlePointsSidebar.Label.InvertPosition", false); ini_uimd.ReadString(SIDEBAR_SECTION, "BattlePointsSidebar.Label", NONE_STR, Phobos::readBuffer); - Phobos::UI::BattlePointsSidebarLabel = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"\u2605"); // %d ★ + Phobos::UI::BattlePointsSidebar_Label = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"\u2605"); // %d ★ } // UISettings diff --git a/src/Phobos.h b/src/Phobos.h index 45bd0d5377..3153ba2584 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -65,9 +65,9 @@ class Phobos static const wchar_t* ShowBriefingResumeButtonLabel; static const wchar_t* SWShotsFormat; static char ShowBriefingResumeButtonStatusLabel[0x20]; - static const wchar_t* BattlePointsLabel; - static const wchar_t* BattlePointsSidebarLabel; - static bool BattlePointsSidebarLabel_InvertPosition; + static const wchar_t* BattlePoints_Label; + static const wchar_t* BattlePointsSidebar_Label; + static bool BattlePointsSidebar_Label_InvertPosition; }; class Config From 6f78cd09eea36411f0442d22b4c818a813c3f7ea Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 16 May 2025 20:58:44 +0200 Subject: [PATCH 06/19] small fix --- src/Commands/ObjectInfo.cpp | 5 ----- src/Ext/SWType/Hooks.cpp | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Commands/ObjectInfo.cpp b/src/Commands/ObjectInfo.cpp index d3fa9e1ba3..58db77f544 100644 --- a/src/Commands/ObjectInfo.cpp +++ b/src/Commands/ObjectInfo.cpp @@ -13,7 +13,6 @@ #include #include -#include const char* ObjectInfoCommandClass::GetName() const { @@ -153,8 +152,6 @@ void ObjectInfoCommandClass::Execute(WWKey eInput) const append(", Ammo = (%d / %d)", pFoot->Ammo, pType->Ammo); append("\n"); - const auto pOwnerExt = HouseExt::ExtMap.Find(pFoot->Owner); - append("BP: %d\n", pOwnerExt->BattlePoints); display(); }; @@ -220,8 +217,6 @@ void ObjectInfoCommandClass::Execute(WWKey eInput) const if (pTechnoExt->CurrentShieldType && pShieldData) append("Current Shield HP = (%d / %d)\n", pShieldData->GetHP(), pTechnoExt->CurrentShieldType->Strength); - const auto pOwnerExt = HouseExt::ExtMap.Find(pBuilding->Owner); - append("BP: %d\n", pOwnerExt->BattlePoints); display(); }; diff --git a/src/Ext/SWType/Hooks.cpp b/src/Ext/SWType/Hooks.cpp index 677736e753..f52991b70f 100644 --- a/src/Ext/SWType/Hooks.cpp +++ b/src/Ext/SWType/Hooks.cpp @@ -307,7 +307,7 @@ DEFINE_HOOK(0x4FD77C, ExpertAI_SuperWeaponAI_RecheckIsReady, 0x5) for (auto const& pSuper : pHouse->Supers) { if (pSuper->IsReady) - pSuper->IsReady = pSuper->GetIsReady(); + pSuper->IsReady = pSuper->CanFire(); } } From fb8f5f0265aaa6fc8bd513c4f0f72579971ad4a4 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 16 May 2025 22:40:11 +0200 Subject: [PATCH 07/19] doc tweaks --- CREDITS.md | 1 + docs/New-or-Enhanced-Logics.md | 2 +- docs/Whats-New.md | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CREDITS.md b/CREDITS.md index 7ec395a368..326e1835a3 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -138,6 +138,7 @@ This page lists all the individual contributions to the project by their author. - Map Events 604 & 605 for checking if a specific Techno enters in a cell - Warhead that can not kill - `Pips.HideIfNoStrength` and `SelfHealing.EnabledBy` additions for shields + - Battle Points economy for super weapons - **Starkku**: - Misc. minor bugfixes & improvements - AI script actions: diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 6991c066b1..4cc676594b 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -983,7 +983,7 @@ In `rulesmd.ini`: AISuperWeaponDelay= ; integer, game frames ``` -### Battle Points counter +### Battle Points economy for super weapons - This system displayes a new currency to be used (optionally) in Super Weapons. - The new currency will be modified by the kills against other players and the system is enabled. - If set `BattlePointsGenerator` in any structure then the system will be enabled if the structure is built. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 9ec4651894..8a83316b7d 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -376,6 +376,7 @@ New: - [Use `InsigniaType` to set the properties of insignia in a batch](Miscellanous.md#insignia-type) (by Ollerus) - [Tiberium eater logic](New-or-Enhanced-Logics.md#tiberium-eater) (by NetsuNegi) - [Customize the damage taken when falling from a bridge](Fixed-or-Improved-Logics.md#customize-bridge-falling-down-damage) (by FlyStar) +- Battle Points economy for super weapons (by FS-21) Vanilla fixes: - Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya) From b65fb81cd54eb3cfc3e0206609551eb3d001984c Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 17 May 2025 00:50:20 +0200 Subject: [PATCH 08/19] tweak --- src/Ext/House/Body.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index ba6ace341d..6cd11afe5b 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -1095,11 +1095,11 @@ void HouseExt::ExtData::UpdateBattlePoints(int modifier) bool HouseExt::ExtData::AreBattlePointsEnabled() { - auto const pThis = this->OwnerObject(); + const auto pThis = this->OwnerObject(); const auto pOwnerTypeExt = HouseTypeExt::ExtMap.Find(pThis->Type); bool enabledBattlePoints = false; - for (auto const pBuilding : pThis->Buildings) + for (const auto pBuilding : pThis->Buildings) { const auto pBuildingTypeExt = BuildingTypeExt::ExtMap.Find(pBuilding->Type); if (pBuildingTypeExt->BattlePointsGenerator.isset() && pBuildingTypeExt->BattlePointsGenerator.Get()) @@ -1122,7 +1122,7 @@ int HouseExt::ExtData::CalculateBattlePoints(TechnoClass* pTechno) if (!pTechno) return 0; - auto const pThis = this->OwnerObject(); + const auto pThis = this->OwnerObject(); const auto pThisTypeExt = HouseTypeExt::ExtMap.Find(pThis->Type); const auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(pTechno->GetTechnoType()); From bc96d8f4badefcd7f5f6e6350a2fba2e2144a3e6 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 17 May 2025 11:09:58 +0200 Subject: [PATCH 09/19] renamed tag From BattlePointsGenerator to BattlePointsCollector --- docs/New-or-Enhanced-Logics.md | 6 +++--- src/Ext/BuildingType/Body.cpp | 4 ++-- src/Ext/BuildingType/Body.h | 4 ++-- src/Ext/House/Body.cpp | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index b40f225808..b6f4865a5b 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -970,9 +970,9 @@ AISuperWeaponDelay= ; integer, game frames ### Battle Points economy for super weapons - This system displayes a new currency to be used (optionally) in Super Weapons. - The new currency will be modified by the kills against other players and the system is enabled. -- If set `BattlePointsGenerator` in any structure then the system will be enabled if the structure is built. +- If set `BattlePointsCollector` in any structure then the system will be enabled if the structure is built. - If set `BattlePoints` in houses they will have the system enabled by default. -- If set [General] -> `BattlePoints` enables/disables the system for every player in the scenario, overriding the country preferences & `BattlePointsGenerator` setting. +- If set [General] -> `BattlePoints` enables/disables the system for every player in the scenario, overriding the country preferences & `BattlePointsCollector` setting. - If set [General] -> `BattlePoints.DefaultValue` and the enemy destroyed object doesn't have a value then this generic value will be used instead. - If set [General] -> `BattlePoints.DefaultFriendlyValue` and the friendly destroyed object doesn't have a value then this generic value will be used instead. This tag doesn't work with the own objects of the player. - If is set `BattlePoints.CanUseStandardPoints` and the destroyed object doesn't have the `BattlePoints` value then the points are obtained from the `Points` tag of the destroyed object. If `BattlePoints.DefaultValue` is present then this will be ignored. @@ -1006,7 +1006,7 @@ BattlePoints=false ; bool BattlePoints.CanUseStandardPoints=false ; bool [SOMEBUILDING] ; BuildingType -BattlePointsGenerator= ; bool +BattlePointsCollector= ; bool [SOMETECHNO] ; TechnoType BattlePoints= ; int diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index d3efa406fa..0499a5cafc 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -201,7 +201,7 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) } } - this->BattlePointsGenerator.Read(exINI, pSection, "BattlePointsGenerator"); + this->BattlePointsCollector.Read(exINI, pSection, "BattlePointsCollector"); // Ares tag this->SpyEffect_Custom.Read(exINI, pSection, "SpyEffect.Custom"); @@ -307,7 +307,7 @@ void BuildingTypeExt::ExtData::Serialize(T& Stm) .Process(this->BarracksExitCell) .Process(this->Overpower_KeepOnline) .Process(this->Overpower_ChargeWeapon) - .Process(this->BattlePointsGenerator) + .Process(this->BattlePointsCollector) ; } diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index c2091201c6..da3d3cf354 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -84,7 +84,7 @@ class BuildingTypeExt Valueable Overpower_KeepOnline; Valueable Overpower_ChargeWeapon; - Nullable BattlePointsGenerator; + Nullable BattlePointsCollector; ExtData(BuildingTypeClass* OwnerObject) : Extension(OwnerObject) , PowersUp_Owner { AffectedHouse::Owner } @@ -137,7 +137,7 @@ class BuildingTypeExt , BarracksExitCell {} , Overpower_KeepOnline { 2 } , Overpower_ChargeWeapon { 1 } - , BattlePointsGenerator {} + , BattlePointsCollector {} { } // Ares 0.A functions diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 6cd11afe5b..21c7f7b0b6 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -1102,7 +1102,7 @@ bool HouseExt::ExtData::AreBattlePointsEnabled() for (const auto pBuilding : pThis->Buildings) { const auto pBuildingTypeExt = BuildingTypeExt::ExtMap.Find(pBuilding->Type); - if (pBuildingTypeExt->BattlePointsGenerator.isset() && pBuildingTypeExt->BattlePointsGenerator.Get()) + if (pBuildingTypeExt->BattlePointsCollector.isset() && pBuildingTypeExt->BattlePointsCollector.Get()) { enabledBattlePoints = true; break; From 384ee394e1818c48c84ff66f0e475e944bd3bed0 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 17 May 2025 17:09:58 +0200 Subject: [PATCH 10/19] bugfix Battlepoints of the killed object was ignored dif a default value was established. --- src/Ext/House/Body.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 21c7f7b0b6..a3b2171e9f 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -1130,7 +1130,7 @@ int HouseExt::ExtData::CalculateBattlePoints(TechnoClass* pTechno) int defaultFriendlyValue = RulesExt::Global()->BattlePoints_DefaultFriendlyValue.isset() ? RulesExt::Global()->BattlePoints_DefaultFriendlyValue.Get() : 0; int points = pThis->IsAlliedWith(pTechno)? defaultFriendlyValue : defaultValue; - points = points == 0 && pTechnoTypeExt->BattlePoints.isset() ? pTechnoTypeExt->BattlePoints.Get() : points; + points = pTechnoTypeExt->BattlePoints.isset() ? pTechnoTypeExt->BattlePoints.Get() : points; points = points == 0 && pThisTypeExt->BattlePoints_CanUseStandardPoints ? pTechno->GetTechnoType()->Points : points; return points; From a58c346eed34c29ac97676a0095443b20cdb3336 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 17 May 2025 23:56:41 +0200 Subject: [PATCH 11/19] By mistake I interted the sign of the operations Now the +/- vaalues work like the tag Money.Amount --- src/Ext/SWType/FireSuperWeapon.cpp | 2 +- src/Ext/SWType/Hooks.cpp | 4 ++-- src/Misc/PhobosToolTip.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Ext/SWType/FireSuperWeapon.cpp b/src/Ext/SWType/FireSuperWeapon.cpp index e829f966fc..08c7a77558 100644 --- a/src/Ext/SWType/FireSuperWeapon.cpp +++ b/src/Ext/SWType/FireSuperWeapon.cpp @@ -350,5 +350,5 @@ void SWTypeExt::ExtData::HandleEMPulseLaunch(SuperClass* pSW, const CellStruct& void SWTypeExt::ExtData::ApplyBattlePoints(SuperClass* pSW) { auto pOwnerExt = HouseExt::ExtMap.Find(pSW->Owner); - pOwnerExt->UpdateBattlePoints(this->BattlePoints_Amount * (-1)); + pOwnerExt->UpdateBattlePoints(this->BattlePoints_Amount); } diff --git a/src/Ext/SWType/Hooks.cpp b/src/Ext/SWType/Hooks.cpp index f52991b70f..e295e14fd7 100644 --- a/src/Ext/SWType/Hooks.cpp +++ b/src/Ext/SWType/Hooks.cpp @@ -288,9 +288,9 @@ DEFINE_HOOK(0x6CC367, SuperClass_IsReady_BattlePoints, 0xD) { const auto pOwnerExt = HouseExt::ExtMap.Find(pSuper->Owner); - if (pExt->BattlePoints_Amount > 0) + if (pExt->BattlePoints_Amount < 0) { - if (pOwnerExt->BattlePoints < pExt->BattlePoints_Amount) + if (pOwnerExt->BattlePoints < std::abs(pExt->BattlePoints_Amount)) return ReturnZero; } } diff --git a/src/Misc/PhobosToolTip.cpp b/src/Misc/PhobosToolTip.cpp index acda70b954..7849e9be33 100644 --- a/src/Misc/PhobosToolTip.cpp +++ b/src/Misc/PhobosToolTip.cpp @@ -186,9 +186,9 @@ void PhobosToolTip::HelpText_Super(int swidx) oss << L"\n"; if (pData->BattlePoints_Amount > 0) - oss << Phobos::UI::BattlePoints_Label << L"-" << nPoints; - else if (pData->BattlePoints_Amount < 0) oss << Phobos::UI::BattlePoints_Label << L"+" << nPoints; + else if (pData->BattlePoints_Amount < 0) + oss << Phobos::UI::BattlePoints_Label << L"-" << nPoints; showSth = true; } From 655e2ecd9eb2f66494916c2758c900745454b340 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 19 May 2025 12:17:30 +0200 Subject: [PATCH 12/19] Code improvements --- src/Ext/House/Body.cpp | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index be2df162bd..9f99ac7fd7 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -1097,24 +1097,26 @@ bool HouseExt::ExtData::AreBattlePointsEnabled() { const auto pThis = this->OwnerObject(); const auto pOwnerTypeExt = HouseTypeExt::ExtMap.Find(pThis->Type); - bool enabledBattlePoints = false; - for (const auto pBuilding : pThis->Buildings) + // Global setting + if (RulesExt::Global()->BattlePoints.isset()) + return RulesExt::Global()->BattlePoints.Get(); + + // House specific setting + if (!pOwnerTypeExt->BattlePoints) { - const auto pBuildingTypeExt = BuildingTypeExt::ExtMap.Find(pBuilding->Type); - if (pBuildingTypeExt->BattlePointsCollector.isset() && pBuildingTypeExt->BattlePointsCollector.Get()) + // Structures can enable this logic overwriting the house's setting + for (const auto pBuilding : pThis->Buildings) { - enabledBattlePoints = true; - break; + const auto pBuildingTypeExt = BuildingTypeExt::ExtMap.Find(pBuilding->Type); + if (pBuildingTypeExt->BattlePointsCollector.Get(false)) + return true; } - } - enabledBattlePoints |= pOwnerTypeExt->BattlePoints; - - if (RulesExt::Global()->BattlePoints.isset()) - enabledBattlePoints = RulesExt::Global()->BattlePoints.Get(); + return false; + } - return enabledBattlePoints; + return true; } int HouseExt::ExtData::CalculateBattlePoints(TechnoClass* pTechno) @@ -1130,7 +1132,7 @@ int HouseExt::ExtData::CalculateBattlePoints(TechnoClass* pTechno) int defaultFriendlyValue = RulesExt::Global()->BattlePoints_DefaultFriendlyValue.isset() ? RulesExt::Global()->BattlePoints_DefaultFriendlyValue.Get() : 0; int points = pThis->IsAlliedWith(pTechno)? defaultFriendlyValue : defaultValue; - points = pTechnoTypeExt->BattlePoints.isset() ? pTechnoTypeExt->BattlePoints.Get() : points; + points = pTechnoTypeExt->BattlePoints.Get(points); points = points == 0 && pThisTypeExt->BattlePoints_CanUseStandardPoints ? pTechno->GetTechnoType()->Points : points; return points; From fd57aef5f1caa550f2b62fe539424cbd28e0d998 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 19 May 2025 12:22:46 +0200 Subject: [PATCH 13/19] Code tweaks Applied feedback --- src/Ext/House/Body.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 9f99ac7fd7..a7d8b3e877 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -1128,8 +1128,8 @@ int HouseExt::ExtData::CalculateBattlePoints(TechnoClass* pTechno) const auto pThisTypeExt = HouseTypeExt::ExtMap.Find(pThis->Type); const auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(pTechno->GetTechnoType()); - int defaultValue = RulesExt::Global()->BattlePoints_DefaultValue.isset() ? RulesExt::Global()->BattlePoints_DefaultValue.Get() : 0; - int defaultFriendlyValue = RulesExt::Global()->BattlePoints_DefaultFriendlyValue.isset() ? RulesExt::Global()->BattlePoints_DefaultFriendlyValue.Get() : 0; + int defaultValue = RulesExt::Global()->BattlePoints_DefaultValue.Get(0); + int defaultFriendlyValue = RulesExt::Global()->BattlePoints_DefaultFriendlyValue.Get(0); int points = pThis->IsAlliedWith(pTechno)? defaultFriendlyValue : defaultValue; points = pTechnoTypeExt->BattlePoints.Get(points); From 3882e7389ce557e1e36beeffe7eda209f40e8025 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 26 Aug 2026 13:47:15 +0200 Subject: [PATCH 14/19] rewrite --- CREDITS.md | 2 +- Phobos.vcxproj | 2 + docs/New-or-Enhanced-Logics.md | 141 ++++++++++++------- docs/Whats-New.md | 2 +- src/Ext/Building/Body.cpp | 130 +++++++++++++++-- src/Ext/Building/Body.h | 2 + src/Ext/Building/Hooks.Selling.cpp | 32 ++++- src/Ext/Building/Hooks.cpp | 3 + src/Ext/BuildingType/Body.cpp | 4 - src/Ext/BuildingType/Body.h | 4 - src/Ext/House/Body.cpp | 150 +++++++++++++++----- src/Ext/House/Body.h | 16 ++- src/Ext/House/Hooks.cpp | 156 +++++++++++++++++++- src/Ext/HouseType/Body.cpp | 4 - src/Ext/HouseType/Body.h | 5 - src/Ext/Rules/Body.cpp | 10 +- src/Ext/Rules/Body.h | 7 - src/Ext/SWType/Body.cpp | 21 ++- src/Ext/SWType/Body.h | 8 +- src/Ext/SWType/FireSuperWeapon.cpp | 45 +++++- src/Ext/SWType/Hooks.cpp | 12 +- src/Ext/Side/Body.cpp | 17 ++- src/Ext/Side/Body.h | 15 +- src/Ext/TAction/Body.cpp | 51 +++++++ src/Ext/TAction/Body.h | 5 + src/Ext/TEvent/Body.cpp | 29 ++++ src/Ext/TEvent/Body.h | 3 + src/Ext/Techno/Body.Internal.cpp | 23 ++- src/Ext/Techno/Body.Update.cpp | 6 + src/Ext/Techno/Body.cpp | 219 +++++++++++++++++++++++++++++ src/Ext/Techno/Body.h | 12 ++ src/Ext/TechnoType/Body.cpp | 159 ++++++++++++++++++++- src/Ext/TechnoType/Body.h | 15 +- src/Ext/Tiberium/Body.cpp | 5 + src/Ext/Tiberium/Body.h | 5 + src/Ext/Unit/Hooks.Harvester.cpp | 2 + src/Misc/FlyingStrings.cpp | 65 ++++++++- src/Misc/FlyingStrings.h | 5 +- src/Misc/Hooks.BugFixes.cpp | 8 ++ src/Misc/Hooks.UI.cpp | 114 ++++++++++++--- src/Misc/PhobosToolTip.cpp | 57 +++++++- src/New/Type/ResourceTypeClass.cpp | 93 ++++++++++++ src/New/Type/ResourceTypeClass.h | 89 ++++++++++++ src/Phobos.Ext.cpp | 4 +- src/Phobos.INI.cpp | 11 -- src/Phobos.h | 3 - src/Utilities/Enum.h | 24 ++++ src/Utilities/TemplateDef.h | 88 ++++++++++++ 48 files changed, 1664 insertions(+), 219 deletions(-) create mode 100644 src/New/Type/ResourceTypeClass.cpp create mode 100644 src/New/Type/ResourceTypeClass.h diff --git a/CREDITS.md b/CREDITS.md index 6f121c9fc4..1a9d8269d6 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -158,7 +158,7 @@ This page lists all the individual contributions to the project by their author. - Warhead activation target health thresholds enhancements - Event 606: AttachEffect is attaching to a Techno - Linked superweapons - - Battle Points economy for super weapons + - Custom Resource Types system - Unit & infantry auto-conversion on ammo change - Restore the ScriptType action#24 `Play speech` from Tiberian Sun - Modify ammo on impact diff --git a/Phobos.vcxproj b/Phobos.vcxproj index c88bf74b41..9867961ef9 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -273,6 +273,7 @@ + @@ -415,6 +416,7 @@ + diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 2588f3cd02..b0b39d1353 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -280,6 +280,99 @@ Due to performance concerns, unless any radiation type has `RadApplicationDelay. Similarly, unless you really have a need, `UseGlobalRadApplicationDelay` should remain `true`, because not doing so will be very resource-intensive. ``` +### Custom Resource Types + +- Now you can define and customize arbitrary secondary/tertiary resources with their own HUD counters, production cycles, unit costs, bounties, and superweapon requirements. + - `[ResourceTypes]` defines the list of custom resource types available in the game. + - `[AudioVisual] -> Display.ResourceTypes.Orientation` controls whether multiple resource counters stack vertically or horizontally on the HUD (`Vertical` / `Horizontal`). + - `[AudioVisual] -> Display.ResourceTypes.Anchor` sets the reference HUD anchor corner (`TopRight`, `TopLeft`, `BottomRight`, `BottomLeft`, `Sidebar`). + - `[AudioVisual] -> Display.ResourceTypes.BaseOffset` provides an initial X,Y pixel offset for the resource display group. + - `[AudioVisual] -> Display.ResourceTypes.Spacing` specifies the spacing in pixels between consecutive resource counters. + - `[] -> Display.Label` specifies the CSF label or text symbol displayed alongside the counter. + - `[] -> Display.Label.InvertPosition` when set to true, places the label after the value (`100 ★` vs `★ 100`). + - `[] -> Display.Label.UseSpace` when set to false, omits the separating space between label and amount (`100★` or `★100` vs `100 ★` / `★ 100`). Defaults to `true`. + - `[] -> Display.Color` specifies the text color (R,G,B) for the resource on the HUD and floating strings. + - `[] -> Display.Condition` specifies when the counter is visible (`Always`, `GreaterThanZero`, `HasCollector`, `Never` / `Hidden` / `None`). + - `[] -> Display.Offset` optional explicit X,Y screen coordinates overriding auto-stacking. + - `[] -> InitialValue` starting resource amount granted to houses at match start. + - `[] -> RequiresCollector` when true, the house resource only activates once a structure with `ResourceCollector.=true` is built. + - `[] -> Bounty.Enabled` enables kill bounty rewards for this resource. + - `[] -> Bounty.DefaultValue` default bounty granted when destroying an enemy unit that lacks a specific bounty tag. + - `[] -> Bounty.DefaultFriendlyValue` bounty when destroying an allied unit. + - `[] -> Bounty.CanUseStandardPoints` fall back to the techno's `Points=` tag for bounty calculation. + - `[] -> Bounty.MoneyConversion` divides the destroyed unit's `Cost=` by this value to determine the bounty reward when not explicitly defined on the techno (e.g. 100 calculates 700 / 100 = 7). Falls back to `Bounty.DefaultValue` if result is 0. + - `[] -> Cost.` additional resource cost required to build the techno. + - `[] -> Bounty..Value` custom bounty awarded to the killer when this enemy unit is destroyed. + - `[] -> Bounty..FriendlyValue` custom friendly fire penalty applied if destroyed by an ally (e.g. -50; always evaluated as negative to prevent accidental rewards). + - `[] -> Production.`, `Production..Delay`, and `Production..Startup` configure periodic and initial resource generation. Works on all units, vehicles, infantry, aircraft, and buildings. `Startup` is optional (defaults to 0). Periodic production occurs every `Delay` frames. + - `[] -> Production..Display.Offset` optional X,Y pixel offset for the floating string text relative to the object render center. + - `[] -> Production..Display.Color` optional R,G,B text color override for the floating string (falls back to `Display.Color` from `[]`). + - `[] -> Production..Display.Houses` controls which players can see the floating string (`All`, `Owner`, `Allies`, `Enemies`, `None`, defaults to `All`). + - `[] -> ResourceCollector.` designates this unit or building as an active collector for this resource. + - `[] -> ResourceType..Amount` specifies resource cost (positive) or grant (negative) upon firing. + - `[] -> ResourceType` and `ResourceValue` configure Tiberium/Ore harvesting to yield custom resources. When `ResourceValue=` is omitted, `Value=` is used for the custom resource and no money is granted (replaces money). When `ResourceValue=` is defined, `ResourceValue` is used for the custom resource AND `Value=` is used to grant standard money (dual income). + - `[] -> Sidebar.ResourceTypes.Types` list of resources to show on this side's sidebar (if omitted, all non-hidden resources are shown). + - `[] -> Sidebar.ResourceTypes.Offset`, `Sidebar.ResourceTypes.Color`, and `Sidebar.ResourceTypes.Align` provide per-side HUD positioning and text styling fallbacks. + +In `rulesmd.ini`: +```ini +[ResourceTypes] +0=BattlePoints +1=OreAlloy + +[AudioVisual] +; Global HUD alignment and stacking for custom resources +Display.ResourceTypes.Orientation=Vertical ; Vertical, Horizontal +Display.ResourceTypes.Anchor=TopRight ; TopRight, TopLeft, BottomRight, BottomLeft, Sidebar +Display.ResourceTypes.BaseOffset=0,0 ; X,Y pixel offset +Display.ResourceTypes.Spacing=14 ; integer, pixels between stacked counters + +[BattlePoints] ; ResourceTypeClass +Display.Label=TXT_BP_SYMBOL ; CSF entry key (e.g. "★") +Display.Label.InvertPosition=false ; boolean +Display.Label.UseSpace=true ; boolean (false = "★100", true = "★ 100") +Display.Color=255,215,0 ; R,G,B ColorStruct +Display.Condition=GreaterThanZero ; Always, GreaterThanZero, HasCollector, Never +Display.Offset= ; X,Y pixels (optional, overrides auto-stacking) +InitialValue=0 ; integer +RequiresCollector=no ; boolean +Bounty.Enabled=yes ; boolean +Bounty.DefaultValue=1 ; integer +Bounty.DefaultFriendlyValue=-10 ; integer +Bounty.CanUseStandardPoints=no ; boolean +Bounty.MoneyConversion=100 ; integer (calculates Cost / 100) + +[SOMETECHNO] ; Works on InfantryType, UnitType, AircraftType, BuildingType +Cost.BattlePoints=50 ; integer +Bounty.BattlePoints.Value=15 ; integer +Bounty.BattlePoints.FriendlyValue=-50; integer (optional special penalty for friendly fire) +ResourceCollector.BattlePoints=yes ; boolean (enables resource for house if RequiresCollector=yes) +Production.BattlePoints=10 ; integer (periodic amount generated) +Production.BattlePoints.Delay=450 ; integer (frames between generation) +Production.BattlePoints.Startup=25 ; integer (optional one-time grant upon creation) +Production.BattlePoints.Display.Offset=0,-20 ; X,Y pixels offset for floating text (optional) +Production.BattlePoints.Display.Color=255,215,0 ; R,G,B color override for floating text (optional) +Production.BattlePoints.Display.Houses=All ; All, Owner, Allies, Enemies, None (optional) + +[Cruentus] ; TiberiumType +; Example 1: Replace money entirely with BattlePoints (uses Value= for points) +ResourceType=BattlePoints + +; Example 2: Dual income (gives $50 money via Value= AND 15 points via ResourceValue=) +; Value=50 +; ResourceType=BattlePoints +; ResourceValue=15 + +[SOMESW] ; SuperWeaponType +ResourceType.BattlePoints.Amount=100 ; integer (>0 required to launch, <0 grants on fire) + +[SOMESIDE] ; SideClass +Sidebar.ResourceTypes.Types=BattlePoints ; List of ResourceTypes to show on this side's sidebar +Sidebar.ResourceTypes.Offset=0,0 ; X,Y +Sidebar.ResourceTypes.Color= ; R,G,B +Sidebar.ResourceTypes.Align=Left ; Left, Right, Center/Centre +``` + ### Laser Trails ![Laser Trails](_static/images/lasertrails.gif) @@ -1183,54 +1276,6 @@ In `rulesmd.ini`: AISuperWeaponDelay= ; integer, game frames ``` -### Battle Points economy for super weapons -- This system displayes a new currency to be used (optionally) in Super Weapons. -- The new currency will be modified by the kills against other players and the system is enabled. -- If set `BattlePointsCollector` in any structure then the system will be enabled if the structure is built. -- If set `BattlePoints` in houses they will have the system enabled by default. -- If set [General] -> `BattlePoints` enables/disables the system for every player in the scenario, overriding the country preferences & `BattlePointsCollector` setting. -- If set [General] -> `BattlePoints.DefaultValue` and the enemy destroyed object doesn't have a value then this generic value will be used instead. -- If set [General] -> `BattlePoints.DefaultFriendlyValue` and the friendly destroyed object doesn't have a value then this generic value will be used instead. This tag doesn't work with the own objects of the player. -- If is set `BattlePoints.CanUseStandardPoints` and the destroyed object doesn't have the `BattlePoints` value then the points are obtained from the `Points` tag of the destroyed object. If `BattlePoints.DefaultValue` is present then this will be ignored. -- If set `BattlePoints` in the destroyed object the calculation is made with this value. Self-Kills done by the own player doesn't count in this system. -- If `BattlePoints.Amount` is set with a value different from `0` then the super weapon is influenced by this system. If the value is positive then this super weapon won't be launched until the affected player gathered the required ammount. - -In `uimd.ini`: -```ini -[Sidebar] -BattlePointsSidebar.Label= ; CSF entry key `★ `, code U+2605 -BattlePointsSidebar.Label.InvertPosition=false ; bool, ` ★`, code U+2605 - -[ToolTips] -BattlePoints.Label= ; CSF entry key, default to `★: `, code U+2605 -``` - -In `rulesmd.ini`: -```ini -[General] -BattlePoints= ; bool -BattlePoints.DefaultValue= ; int -BattlePoints.DefaultFriendlyValue= ; int - -[SOMESIDE] ; Side -Sidebar.BattlePoints.Offset=0,0 ; X,Y, pixels relative to default -Sidebar.BattlePoints.Color= ; integer - R,G,B -Sidebar.BattlePoints.Align=Left ; Left, Right, Center/Centre - -[SOMECOUNTRY] ; HouseType -BattlePoints=false ; bool -BattlePoints.CanUseStandardPoints=false ; bool - -[SOMEBUILDING] ; BuildingType -BattlePointsCollector= ; bool - -[SOMETECHNO] ; TechnoType -BattlePoints= ; int - -[SOMESW] -BattlePoints.Amount=0 ; int -``` - ### Aux technos and TechLevel requirement of superweapon - `SW.AuxTechnos` specifies the auxiliary technos without which this super weapon cannot become available. The player has to own at least one techno of any of these types to get access to this super weapon. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index db3a821dd2..89dd016d4d 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -421,7 +421,7 @@ HideShakeEffects=false ; boolean #### New: - [Customized transport plane for teams](AI-Scripting-and-Mapping.md#customized-transport-plane-for-teams) (by FlyStar) - [Modify ammo on impact](New-or-Enhanced-Logics.md#modify-ammo-on-impact) (by FS-21) -- [Battle Points economy for super weapons](New-or-Enhanced-Logics.md#battle-points-economy-for-super-weapons) (by FS-21) +- [Custom Resource Types](New-or-Enhanced-Logics.md#custom-resource-types) (by FS-21) - [Customize whether mind-controlled Insignificant technos can be auto-targeted](Fixed-or-Improved-Logics.md#customize-whether-mind-controlled-Insignificant-technos-can-be-auto-targeted) (by Noble_Fish) #### Vanilla fixes: diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 1bbc42eeb4..7fbb75e05e 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include BuildingExt::ExtContainer BuildingExt::ExtMap; @@ -11,7 +13,17 @@ BuildingExt::ExtContainer::~ExtContainer() = default; void BuildingExt::DisplayIncomeString() { - if (this->AccumulatedIncome) + bool hasResourceIncome = false; + for (float val : this->AccumulatedResources) + { + if (val >= 1.0f || val <= -1.0f) + { + hasResourceIncome = true; + break; + } + } + + if (this->AccumulatedIncome != 0 || hasResourceIncome) { int const delay = this->GetTypeExtData()->DisplayIncome_Delay.Get(RulesExt::Global()->DisplayIncome_Delay.Get()); if (Unsorted::CurrentFrame % delay == 0) @@ -22,15 +34,90 @@ void BuildingExt::DisplayIncomeString() if ((RulesExt::Global()->DisplayIncome_AllowAI || pThis->Owner->IsControlledByHuman()) && pTypeExt->DisplayIncome.Get(RulesExt::Global()->DisplayIncome)) { - FlyingStrings::AddMoneyString( - this->AccumulatedIncome, - pThis, - pThis->Owner, - pTypeExt->DisplayIncome_Houses.Get(RulesExt::Global()->DisplayIncome_Houses.Get()), - pThis->GetRenderCoords(), - pTypeExt->DisplayIncome_Offset - ); + std::wstring combinedStr; + ColorStruct displayColor = ColorStruct { 0, 255, 0 }; + bool hasCustomColor = false; + + if (this->AccumulatedIncome != 0) + { + const bool isPositive = this->AccumulatedIncome > 0; + wchar_t moneyBuf[32]; + swprintf_s(moneyBuf, L"%ls%ls%d", isPositive ? L"+" : L"-", Phobos::UI::CostLabel, std::abs(this->AccumulatedIncome)); + combinedStr += moneyBuf; + if (!isPositive) + displayColor = ColorStruct { 255, 0, 0 }; + } + + for (size_t i = 0; i < this->AccumulatedResources.size(); ++i) + { + const int amount = static_cast(this->AccumulatedResources[i]); + if (amount != 0 && i < ResourceTypeClass::Array.size()) + { + if (const auto pResource = ResourceTypeClass::Array[i].get()) + { + if (!combinedStr.empty()) + combinedStr += L" "; + + const bool isPositive = amount > 0; + wchar_t resBuf[32]; + const wchar_t* label = pResource->Display_Label.Get(); + const bool useSpace = pResource->Display_Label_UseSpace.Get(); + if (label && *label) + { + if (pResource->Display_Label_InvertPosition.Get()) + swprintf_s(resBuf, useSpace ? L"%ls%d %ls" : L"%ls%d%ls", isPositive ? L"+" : L"-", std::abs(amount), label); + else + swprintf_s(resBuf, useSpace ? L"%ls%ls %d" : L"%ls%ls%d", isPositive ? L"+" : L"-", label, std::abs(amount)); + } + else + { + swprintf_s(resBuf, L"%ls%d", isPositive ? L"+" : L"-", std::abs(amount)); + } + combinedStr += resBuf; + + if (this->AccumulatedIncome == 0 && !hasCustomColor) + { + const ColorStruct resColor = pResource->Display_Color.Get(); + if (resColor != ColorStruct { 0, 0, 0 }) + { + displayColor = resColor; + hasCustomColor = true; + } + else if (!isPositive) + { + displayColor = ColorStruct { 255, 0, 0 }; + } + } + } + } + } + + if (!combinedStr.empty() && !MapClass::Instance.IsLocationShrouded(pThis->GetCoords())) + { + const auto displayHouses = pTypeExt->DisplayIncome_Houses.Get(RulesExt::Global()->DisplayIncome_Houses.Get()); + if (displayHouses == AffectedHouse::All || EnumFunctions::CanTargetHouse(displayHouses, pThis->Owner, HouseClass::CurrentPlayer)) + { + if (pThis->VisualCharacter(false, nullptr) != VisualType::Hidden) + { + Point2D offset = pTypeExt->DisplayIncome_Offset; + int width = 0, height = 0; + if (BitFont::Instance) + { + BitFont::Instance->GetTextDimension(combinedStr.c_str(), &width, &height, 120); + offset.X -= (width / 2); + } + FlyingStrings::Add(combinedStr.c_str(), pThis->GetCoords(), displayColor, offset); + } + } + } } + + for (size_t i = 0; i < this->AccumulatedResources.size(); ++i) + { + const int amount = static_cast(this->AccumulatedResources[i]); + this->AccumulatedResources[i] -= static_cast(amount); + } + this->AccumulatedIncome = 0; } } @@ -270,6 +357,23 @@ bool BuildingExt::DoGrindingExtras(BuildingClass* pBuilding, TechnoClass* pTechn pExt->AccumulatedIncome += refund; pExt->GrindingWeapon_AccumulatedCredits += refund; + if (const auto pHouseExt = HouseExt::TryFetch(pBuilding->Owner)) + { + const size_t resCount = ResourceTypeClass::Array.size(); + for (size_t i = 0; i < resCount; ++i) + { + const int resRefund = TechnoExt::GetResourceRefund(pTechno, static_cast(i), true); + if (resRefund > 0) + { + if (i >= pExt->AccumulatedResources.size()) + pExt->AccumulatedResources.resize(i + 1, 0.0f); + + pExt->AccumulatedResources[i] += static_cast(resRefund); + pHouseExt->UpdateResourceAmount(static_cast(i), resRefund); + } + } + } + if (pTypeExt->Grinding_Weapon && Unsorted::CurrentFrame >= pExt->GrindingWeapon_LastFiredFrame + pTypeExt->Grinding_Weapon->ROF && pExt->GrindingWeapon_AccumulatedCredits >= pTypeExt->Grinding_Weapon_RequiredCredits) @@ -596,6 +700,7 @@ void BuildingExt::Serialize(T& Stm) .Process(this->GrindingWeapon_AccumulatedCredits) .Process(this->CurrentAirFactory) .Process(this->AccumulatedIncome) + .Process(this->AccumulatedResources) .Process(this->CurrentLaserWeaponIndex) .Process(this->PoweredUpToLevel) .Process(this->CurrentEMPulseSW) @@ -603,7 +708,7 @@ void BuildingExt::Serialize(T& Stm) .Process(this->TurretAnimIdleFrame) .Process(this->TurretAnimFiringFrame) .Process(this->TurretAnimRateTick) - .Process(this->ConstructionStartFacing) + .Process(this->ConstructionStartFacing) ; } @@ -675,6 +780,11 @@ DEFINE_HOOK(0x43C0B6, BuildingClass_DTOR, 0x9) { GET(BuildingClass*, pItem, ESI); + if (const auto pExt = BuildingExt::TryFetch(pItem)) + { + pExt->ApplyCollectorRegistration(false); + } + BuildingExt::ExtMap.Remove(pItem); return 0; diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index 47ad829fa3..2dfd71c9d7 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -21,6 +21,7 @@ class BuildingExt final : public TechnoExt, public Detach::Listener AccumulatedResources; std::optional CurrentLaserWeaponIndex; int PoweredUpToLevel; // Distinct from UpgradeLevel, and set to highest PowersUpToLevel out of applied upgrades regardless of how many are currently applied to this building. SuperClass* CurrentEMPulseSW; @@ -39,6 +40,7 @@ class BuildingExt final : public TechnoExt, public Detach::Listener +#include #include // SellSound and EVA dehardcode @@ -12,6 +13,19 @@ DEFINE_HOOK(0x4D9F7B, FootClass_Sell, 0x6) const auto pOwner = pThis->Owner; pOwner->GiveMoney(money); + if (const auto pHouseExt = HouseExt::TryFetch(pOwner)) + { + const size_t resCount = ResourceTypeClass::Array.size(); + for (size_t i = 0; i < resCount; ++i) + { + const int resRefund = TechnoExt::GetResourceRefund(pThis, static_cast(i), false); + if (resRefund > 0) + { + pHouseExt->UpdateResourceAmount(static_cast(i), resRefund); + } + } + } + if (pOwner->IsControlledByCurrentPlayer()) { const auto pTypeExt = TechnoExt::Fetch(pThis)->TypeExtData; @@ -73,6 +87,22 @@ DEFINE_HOOK(0x449CC1, BuildingClass_Mi_Selling_EVASold_UndeploysInto, 0x6) VoxClass::PlayIndex(pTypeExt->EVA_Sold.isset() ? pTypeExt->EVA_Sold.Get() : VoxClass::FindIndex(GameStrings::EVA_StructureSold)); } + if (!BuildingExt::CanUndeployOnSell(pThis)) + { + if (const auto pHouseExt = HouseExt::TryFetch(pThis->Owner)) + { + const size_t resCount = ResourceTypeClass::Array.size(); + for (size_t i = 0; i < resCount; ++i) + { + const int resRefund = TechnoExt::GetResourceRefund(pThis, static_cast(i), false); + if (resRefund > 0) + { + pHouseExt->UpdateResourceAmount(static_cast(i), resRefund); + } + } + } + } + return BuildingExt::CanUndeployOnSell(pThis) ? CreateUnit : SkipTheEntireShit; } diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 458987a6d3..5deaed38ce 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -393,6 +393,9 @@ DEFINE_HOOK(0x441553, BuildingClass_Unlimbo_AddOwned, 0x6) GET(BuildingClass*, pThis, ESI); const auto pTypeExt = BuildingTypeExt::Fetch(pThis->Type); const auto pOwnerExt = HouseExt::Fetch(pThis->Owner); + const auto pBuildingExt = BuildingExt::Fetch(pThis); + + pBuildingExt->InitializeResourceProductions(); if (!pTypeExt->PowerPlantEnhancer_Buildings.empty() && (pTypeExt->PowerPlantEnhancer_Amount != 0 || pTypeExt->PowerPlantEnhancer_Factor != 1.0f)) pOwnerExt->PowerPlantEnhancers.push_back(pThis); diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 55b482dcd1..e2b0e50d16 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -307,9 +307,6 @@ void BuildingTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->AircraftDockingDir_DefaultToPoseDir.Read(exArtINI, pArtSection, "AircraftDockingDir.DefaultToPoseDir"); this->Refinery_UseNormalActiveAnim.Read(exArtINI, pArtSection, "Refinery.UseNormalActiveAnim"); - - this->BattlePointsCollector.Read(exINI, pSection, "BattlePointsCollector"); - this->DeployFireDelay.Read(exINI, pSection, "DeployFireDelay"); auto& preProdAnim = pThis->GetBuildingAnim(BuildingAnimSlot::PreProduction); @@ -461,7 +458,6 @@ void BuildingTypeExt::Serialize(T& Stm) .Process(this->BuildingRepairedSound) .Process(this->Refinery_UseNormalActiveAnim) .Process(this->HasPowerUpAnim) - .Process(this->BattlePointsCollector) .Process(this->UndeploysInto_Sellable) .Process(this->BuildingRadioLink_SyncOwner) .Process(this->GuardRetryDelay) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index a14715e9db..7de3d25dbe 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -142,8 +142,6 @@ class BuildingTypeExt final : public TechnoTypeExt // Ares 3.0 Nullable UnitSell; - Nullable BattlePointsCollector; - BuildingTypeExt(BuildingTypeClass* OwnerObject) : TechnoTypeExt(OwnerObject) , PowersUp_Owner { AffectedHouse::Owner } , PowersUp_Buildings {} @@ -243,8 +241,6 @@ class BuildingTypeExt final : public TechnoTypeExt // Ares 3.0 , UnitSell {} - - , BattlePointsCollector {} { } // typed owner accessor (shadows the TechnoTypeClass one from the base) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 3315d99fae..686cffa32e 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include //Static init @@ -707,7 +709,8 @@ void HouseExt::Serialize(T& Stm) .Process(this->SuspendedEMPulseSWs) .Process(this->SuperExts) .Process(this->ForceEnemyIndex) - .Process(this->BattlePoints) + .Process(this->CustomResources) + .Process(this->ResourceCollectorCounts) .Process(this->ForceOnlyTargetHouseEnemy) .Process(this->ForceOnlyTargetHouseEnemyMode) .Process(this->TeamDelay) @@ -1190,60 +1193,132 @@ bool HouseExt::ReachedBuildLimit(const HouseClass* pHouse, const TechnoTypeClass } #pragma endregion -void HouseExt::UpdateBattlePoints(int modifier) +int HouseExt::GetResourceAmount(int resourceIdx) const { - this->BattlePoints += modifier; - this->BattlePoints = this->BattlePoints < 0 ? 0 : this->BattlePoints; + if (resourceIdx >= 0 && resourceIdx < static_cast(this->CustomResources.size())) + return this->CustomResources[resourceIdx]; + return 0; } -bool HouseExt::AreBattlePointsEnabled() const +void HouseExt::SetResourceAmount(int resourceIdx, int amount) { - const auto pThis = this->OwnerObject(); - const auto pOwnerTypeExt = HouseTypeExt::Fetch(pThis->Type); + if (resourceIdx < 0) + return; - // Global setting - if (RulesExt::Global()->BattlePoints.isset()) - return RulesExt::Global()->BattlePoints.Get(); + if (resourceIdx >= static_cast(this->CustomResources.size())) + const_cast(this)->CustomResources.resize(resourceIdx + 1, 0); - // House specific setting - if (!pOwnerTypeExt->BattlePoints) - { - // Structures can enable this logic overwriting the house's setting - for (const auto pBuilding : pThis->Buildings) - { - if (const auto pBuildingTypeExt = BuildingTypeExt::TryFetch(pBuilding->Type)) - { - if (pBuildingTypeExt->BattlePointsCollector.Get(false)) - return true; - } - } + this->CustomResources[resourceIdx] = std::max(0, amount); +} + +void HouseExt::UpdateResourceAmount(int resourceIdx, int delta) +{ + if (resourceIdx < 0) + return; + + if (resourceIdx >= static_cast(this->CustomResources.size())) + const_cast(this)->CustomResources.resize(resourceIdx + 1, 0); + this->CustomResources[resourceIdx] = std::max(0, this->CustomResources[resourceIdx] + delta); +} + +bool HouseExt::CanAffordResource(int resourceIdx, int amount) const +{ + return this->GetResourceAmount(resourceIdx) >= amount; +} + +bool HouseExt::IsResourceEnabled(int resourceIdx) const +{ + if (resourceIdx < 0 || resourceIdx >= static_cast(ResourceTypeClass::Array.size())) return false; - } - return true; + const auto pResource = ResourceTypeClass::Array[resourceIdx].get(); + if (!pResource) + return false; + + if (!pResource->RequiresCollector.Get()) + return true; + + if (resourceIdx < static_cast(this->ResourceCollectorCounts.size())) + return this->ResourceCollectorCounts[resourceIdx] > 0; + + return false; } -int HouseExt::CalculateBattlePoints(TechnoClass* pTechno) const +int HouseExt::CalculateResourceBounty(int resourceIdx, TechnoClass* pVictim) const { - if (!pTechno) + if (!pVictim || resourceIdx < 0 || resourceIdx >= static_cast(ResourceTypeClass::Array.size())) + return 0; + + const auto pResource = ResourceTypeClass::Array[resourceIdx].get(); + if (!pResource || !this->IsResourceEnabled(resourceIdx)) return 0; const auto pThis = this->OwnerObject(); - const auto pThisTypeExt = HouseTypeExt::Fetch(pThis->Type); - const auto pTechnoTypeExt = TechnoTypeExt::TryFetch(pTechno->GetTechnoType()); + const auto pVictimType = pVictim->GetTechnoType(); + const auto pVictimTypeExt = TechnoTypeExt::TryFetch(pVictimType); - int defaultValue = RulesExt::Global()->BattlePoints_DefaultValue.Get(0); - int defaultFriendlyValue = RulesExt::Global()->BattlePoints_DefaultFriendlyValue.Get(0); + const bool isAllied = pThis->IsAlliedWith(pVictim); - int points = pThis->IsAlliedWith(pTechno) ? defaultFriendlyValue : defaultValue; - if (pTechnoTypeExt) + if (isAllied) { - points = pTechnoTypeExt->BattlePoints.Get(points); + // Friendly fire always applies a friendly penalty (never enemy bounties). + // Clamped to negative/zero so positive accidental inputs cannot reward teamkilling. + if (pVictimTypeExt && resourceIdx < static_cast(pVictimTypeExt->ResourceFriendlyBounties.size())) + { + int customFriendlyVal = pVictimTypeExt->ResourceFriendlyBounties[resourceIdx]; + if (customFriendlyVal != 0) + return -std::abs(customFriendlyVal); + } + + return -std::abs(pResource->Bounty_DefaultFriendlyValue.Get()); + } + + int bounty = 0; + + // 1. Explicit per-techno bounty + if (pVictimTypeExt && resourceIdx < static_cast(pVictimTypeExt->ResourceBounties.size())) + { + int customVal = pVictimTypeExt->ResourceBounties[resourceIdx]; + if (customVal != 0) + bounty = customVal; } - points = points == 0 && pThisTypeExt->BattlePoints_CanUseStandardPoints ? pTechno->GetTechnoType()->Points : points; - return points; + // 2. Generic fallback / MoneyConversion / Points / DefaultValue + if (bounty == 0) + { + const int moneyConversion = pResource->Bounty_MoneyConversion.Get(); + if (moneyConversion > 0) + { + bounty = (pVictimType->Cost + (moneyConversion / 2)) / moneyConversion; + } + + if (bounty <= 0) + { + if (pResource->Bounty_CanUseStandardPoints.Get() && pVictimType->Points > 0) + { + bounty = pVictimType->Points; + } + else if (pResource->Bounty_DefaultValue.Get() > 0) + { + bounty = pResource->Bounty_DefaultValue.Get(); + } + } + } + + return bounty; +} + +void HouseExt::InitializeCustomResources() +{ + const size_t count = ResourceTypeClass::Array.size(); + this->CustomResources.resize(count, 0); + this->ResourceCollectorCounts.resize(count, 0); + + for (size_t i = 0; i < count; ++i) + { + this->CustomResources[i] = ResourceTypeClass::Array[i]->InitialValue.Get(); + } } // ============================= @@ -1261,7 +1336,10 @@ DEFINE_HOOK(0x4F6532, HouseClass_CTOR, 0x5) { GET(HouseClass*, pItem, EAX); - HouseExt::ExtMap.TryAllocate(pItem); + if (const auto pExt = HouseExt::ExtMap.TryAllocate(pItem)) + { + pExt->InitializeCustomResources(); + } HouseExt::CalculatePowerSurplus(pItem); return 0; diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 7e9f33c45e..befc2af7cd 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -65,7 +65,8 @@ class HouseExt final : public AbstractExt, public Detach::Listener> SuspendedEMPulseSWs; - int BattlePoints; + std::vector CustomResources; + std::vector ResourceCollectorCounts; // standalone? no need and not a good idea struct SWExt @@ -109,9 +110,10 @@ class HouseExt final : public AbstractExt, public Detach::Listener +#include +#include +#include #include @@ -68,7 +71,52 @@ DEFINE_HOOK(0x73E474, UnitClass_Unload_Storage, 0x6) auto const storageTiberiumIndex = RulesExt::Global()->Storage_TiberiumIndex; - if (pTypeExt->Refinery_UseStorage && storageTiberiumIndex >= 0) + if (idxTiberium >= 0 && idxTiberium < TiberiumClass::Array.Count) + { + const auto pTiberium = TiberiumClass::Array.GetItem(idxTiberium); + if (const auto pTibExt = TiberiumExt::TryFetch(pTiberium)) + { + if (pTibExt->ResourceType.isset() && pTibExt->ResourceType >= 0) + { + const int resIdx = pTibExt->ResourceType.Get(); + const bool hasCustomResourceValue = pTibExt->ResourceValue.isset(); + const int unitVal = hasCustomResourceValue ? pTibExt->ResourceValue.Get() : pTiberium->Value; + const float incomingPoints = amount * static_cast(unitVal); + + if (incomingPoints > 0.0f && pBuilding && pBuilding->Owner) + { + if (const auto pHouseExt = HouseExt::TryFetch(pBuilding->Owner)) + { + if (pHouseExt->IsResourceEnabled(resIdx)) + { + if (const auto pBldExt = BuildingExt::TryFetch(pBuilding)) + { + if (resIdx >= static_cast(pBldExt->AccumulatedResources.size())) + pBldExt->AccumulatedResources.resize(resIdx + 1, 0.0f); + + const int prevWhole = static_cast(pBldExt->AccumulatedResources[resIdx]); + pBldExt->AccumulatedResources[resIdx] += incomingPoints; + const int newWhole = static_cast(pBldExt->AccumulatedResources[resIdx]); + const int pointsToGrant = newWhole - prevWhole; + + if (pointsToGrant > 0) + { + pHouseExt->UpdateResourceAmount(resIdx, pointsToGrant); + } + } + } + } + } + + if (!hasCustomResourceValue) + { + amount = 0.0f; // Value was used for the custom resource, suppress money + } + } + } + } + + if (pTypeExt->Refinery_UseStorage && storageTiberiumIndex >= 0 && amount > 0.0f) { BuildingExt::StoreTiberium(pBuilding, amount, idxTiberium, storageTiberiumIndex); amount = 0.0f; @@ -856,3 +904,109 @@ DEFINE_HOOK(0x4AC9B2, MouseClass_ToggleBeaconMode_AllUsed, 0x6) } #pragma endregion + +#pragma region ResourceCosts + +DEFINE_HOOK(0x4FD590, HouseClass_CanBuild_CheckResources, 0x6) +{ + GET(HouseClass*, pHouse, ECX); + GET_STACK(TechnoTypeClass*, pItem, 0x4); + GET_STACK(bool, check_money, 0x8); + + if (check_money && pItem && pHouse) + { + if (const auto pTypeExt = TechnoTypeExt::TryFetch(pItem)) + { + if (const auto pHouseExt = HouseExt::TryFetch(pHouse)) + { + for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) + { + const int cost = pTypeExt->ResourceCosts[i]; + if (cost > 0 && !pHouseExt->CanAffordResource(static_cast(i), cost)) + { + R->AL(0); + return 0x4FD605; + } + } + } + } + } + + return 0; +} + +DEFINE_HOOK(0x4FA4B0, HouseClass_BeginProduction_CheckAndDeductResources, 0x6) +{ + GET(HouseClass*, pHouse, ECX); + GET_STACK(AbstractType, absType, 0x4); + GET_STACK(int, index, 0x8); + GET_STACK(int, count, 0xC); + + if (pHouse && count > 0) + { + if (const auto pType = TechnoTypeClass::GetByTypeAndIndex(absType, index)) + { + if (const auto pTypeExt = TechnoTypeExt::TryFetch(pType)) + { + if (const auto pHouseExt = HouseExt::TryFetch(pHouse)) + { + for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) + { + const int cost = pTypeExt->ResourceCosts[i]; + if (cost > 0 && !pHouseExt->CanAffordResource(static_cast(i), cost * count)) + { + R->AL(0); + return 0x4FA66C; + } + } + + for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) + { + const int cost = pTypeExt->ResourceCosts[i]; + if (cost > 0) + { + pHouseExt->UpdateResourceAmount(static_cast(i), -(cost * count)); + } + } + } + } + } + } + + return 0; +} + +DEFINE_HOOK(0x4FAA20, HouseClass_AbandonProduction_RefundResources, 0x6) +{ + GET(HouseClass*, pHouse, ECX); + GET_STACK(AbstractType, absType, 0x4); + GET_STACK(int, index, 0x8); + GET_STACK(int, count, 0xC); + + if (pHouse) + { + if (const auto pType = TechnoTypeClass::GetByTypeAndIndex(absType, index)) + { + if (const auto pTypeExt = TechnoTypeExt::TryFetch(pType)) + { + if (const auto pHouseExt = HouseExt::TryFetch(pHouse)) + { + const int refundCount = (count > 0) ? count : 1; + for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) + { + const int cost = pTypeExt->ResourceCosts[i]; + if (cost > 0) + { + pHouseExt->UpdateResourceAmount(static_cast(i), cost * refundCount); + } + } + } + } + } + } + + return 0; +} + +#pragma endregion + diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp index 78bddee175..08c0adfd4e 100644 --- a/src/Ext/HouseType/Body.cpp +++ b/src/Ext/HouseType/Body.cpp @@ -17,8 +17,6 @@ void HouseTypeExt::LoadFromINIFile(CCINIClass* pINI) INI_EX exINI(pINI); - this->BattlePoints.Read(exINI, pSection, "BattlePoints"); - this->BattlePoints_CanUseStandardPoints.Read(exINI, pSection, "BattlePoints.CanUseStandardPoints"); this->EVATag.Read(pINI, pSection, "EVA.Tag"); } @@ -26,8 +24,6 @@ template void HouseTypeExt::Serialize(T& Stm) { Stm - .Process(this->BattlePoints) - .Process(this->BattlePoints_CanUseStandardPoints) .Process(this->EVATag) ; } diff --git a/src/Ext/HouseType/Body.h b/src/Ext/HouseType/Body.h index ed16363dd1..7f935fbe19 100644 --- a/src/Ext/HouseType/Body.h +++ b/src/Ext/HouseType/Body.h @@ -25,14 +25,9 @@ class HouseTypeExt final : public AbstractTypeExt return static_cast(this->GetAttachedObject()); } - Valueable BattlePoints; - Valueable BattlePoints_CanUseStandardPoints; - EVAType EVATag; HouseTypeExt(HouseTypeClass* OwnerObject) : AbstractTypeExt(OwnerObject) - , BattlePoints { false } - , BattlePoints_CanUseStandardPoints { false } , EVATag { -2 } { } diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 738deb8853..7168485162 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -11,6 +11,7 @@ #include #include #include +#include std::unique_ptr RulesExt::Data = nullptr; @@ -31,6 +32,8 @@ void RulesExt::LoadFromINIFile(RulesClass* pThis, CCINIClass* pINI) void RulesExt::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) { + ResourceTypeClass::LoadFromINIList(pINI); + ResourceTypeClass::LoadGlobalsFromINI(pINI); DigitalDisplayTypeClass::LoadFromINIList(pINI); SelectBoxTypeClass::LoadFromINIList(pINI); RadTypeClass::LoadFromINIList(pINI); @@ -410,10 +413,6 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->InfantryAutoDeploy.Read(exINI, GameStrings::General, "InfantryAutoDeploy"); - this->BattlePoints.Read(exINI, GameStrings::General, "BattlePoints"); - this->BattlePoints_DefaultValue.Read(exINI, GameStrings::General, "BattlePoints.DefaultValue"); - this->BattlePoints_DefaultFriendlyValue.Read(exINI, GameStrings::General, "BattlePoints.DefaultFriendlyValue"); - this->AdjacentWallDamage.Read(exINI, GameStrings::CombatDamage, "AdjacentWallDamage"); this->WarheadAnimZAdjust.Read(exINI, GameStrings::AudioVisual, "WarheadAnimZAdjust"); @@ -967,9 +966,6 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->Parasite_GrappleAnim) .Process(this->Parasite_AllowWaterExit) .Process(this->InfantryAutoDeploy) - .Process(this->BattlePoints) - .Process(this->BattlePoints_DefaultValue) - .Process(this->BattlePoints_DefaultFriendlyValue) .Process(this->AdjacentWallDamage) .Process(this->WarheadAnimZAdjust) .Process(this->IvanBombAttachToCenter) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 2fae075447..54bbefd52b 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -347,10 +347,6 @@ class RulesExt int TintColorBerserk; Valueable InfantryAutoDeploy; - Nullable BattlePoints; - Nullable BattlePoints_DefaultValue; - Nullable BattlePoints_DefaultFriendlyValue; - Valueable AdjacentWallDamage; Valueable WarheadAnimZAdjust; @@ -837,9 +833,6 @@ class RulesExt , Parasite_GrappleAnim {} , Parasite_AllowWaterExit {} , InfantryAutoDeploy { false } - , BattlePoints {} - , BattlePoints_DefaultValue {} - , BattlePoints_DefaultFriendlyValue {} , AdjacentWallDamage { 200 } , WarheadAnimZAdjust { -15 } diff --git a/src/Ext/SWType/Body.cpp b/src/Ext/SWType/Body.cpp index 87d0778b04..30b07c774a 100644 --- a/src/Ext/SWType/Body.cpp +++ b/src/Ext/SWType/Body.cpp @@ -1,6 +1,7 @@ #include "Body.h" #include "NewSWType/NewSWType.h" +#include SWTypeExt::ExtContainer SWTypeExt::ExtMap; @@ -100,7 +101,7 @@ void SWTypeExt::Serialize(T& Stm) .Process(this->SW_Link_RollChances) .Process(this->Message_LinkedSWAcquired) .Process(this->EVA_LinkedSWAcquired) - .Process(this->BattlePoints_Amount) + .Process(this->SW_ResourceAmounts) .Process(this->Message_Activated_Owner) .Process(this->Message_Activated_Allies) .Process(this->Message_Activated_Enemies) @@ -308,7 +309,23 @@ void SWTypeExt::LoadFromINIFile(CCINIClass* const pINI) pNewSWType->LoadFromINI(const_cast(this), OwnerObject(), pINI); } - this->BattlePoints_Amount.Read(exINI, pSection, "BattlePoints.Amount"); + const size_t resourceCount = ResourceTypeClass::Array.size(); + this->SW_ResourceAmounts.resize(resourceCount, 0); + + for (size_t i = 0; i < resourceCount; ++i) + { + const auto pResource = ResourceTypeClass::Array[i].get(); + if (!pResource) + continue; + + char tag[64]; + sprintf_s(tag, "ResourceType.%s.Amount", (const char*)pResource->Name); + int amount = 0; + if (exINI.ReadInteger(pSection, tag, &amount)) + { + this->SW_ResourceAmounts[i] = amount; + } + } } void SWTypeExt::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/SWType/Body.h b/src/Ext/SWType/Body.h index b42250772f..2639b10ba2 100644 --- a/src/Ext/SWType/Body.h +++ b/src/Ext/SWType/Body.h @@ -110,8 +110,7 @@ class SWTypeExt final : public AbstractTypeExt Valueable EMPulse_SuspendOthers; ValueableVector EMPulse_Cannons; Valueable EMPulse_TargetSelf; - - Valueable BattlePoints_Amount; + std::vector SW_ResourceAmounts; ValueableIdxVector SW_Link; Valueable SW_Link_Grant; Valueable SW_Link_Ready; @@ -214,7 +213,7 @@ class SWTypeExt final : public AbstractTypeExt , EVA_Activated_Owner { -1 } , EVA_Activated_Allies { -1 } , EVA_Activated_Enemies { -1 } - , BattlePoints_Amount { 0 } + , SW_ResourceAmounts {} { } // Ares 0.A functions @@ -239,7 +238,8 @@ class SWTypeExt final : public AbstractTypeExt std::vector GetEMPulseCannons(HouseClass* pOwner, const CellStruct& cell) const; std::pair GetEMPulseCannonRange(BuildingClass* pBuilding) const; - void ApplyBattlePoints(SuperClass* pSW); + void ApplyResourceAmounts(SuperClass* pSW); + bool AreResourcesSufficient(HouseClass* pHouse) const; void ApplyLinkedSW(SuperClass* pSW); void ApplyActivatedMessage(SuperClass* pSW) const; diff --git a/src/Ext/SWType/FireSuperWeapon.cpp b/src/Ext/SWType/FireSuperWeapon.cpp index 37160b3b44..d8f30430e7 100644 --- a/src/Ext/SWType/FireSuperWeapon.cpp +++ b/src/Ext/SWType/FireSuperWeapon.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include @@ -37,8 +39,7 @@ void SWTypeExt::FireSuperWeaponExt(SuperClass* pSW, const CellStruct& cell) if (static_cast(pType->Type) == 28 && !pTypeExt->EMPulse_TargetSelf) // Ares' Type=EMPulse SW pTypeExt->HandleEMPulseLaunch(pSW, cell); - if (pTypeExt->BattlePoints_Amount != 0) - pTypeExt->ApplyBattlePoints(pSW); + pTypeExt->ApplyResourceAmounts(pSW); pTypeExt->ApplyActivatedMessage(pSW); @@ -491,10 +492,44 @@ void SWTypeExt::ApplyLinkedSW(SuperClass* pSW) } } -void SWTypeExt::ApplyBattlePoints(SuperClass* pSW) +void SWTypeExt::ApplyResourceAmounts(SuperClass* pSW) { - auto pOwnerExt = HouseExt::Fetch(pSW->Owner); - pOwnerExt->UpdateBattlePoints(this->BattlePoints_Amount); + if (!pSW || !pSW->Owner) + return; + + if (const auto pOwnerExt = HouseExt::TryFetch(pSW->Owner)) + { + for (size_t i = 0; i < this->SW_ResourceAmounts.size(); ++i) + { + const int amount = this->SW_ResourceAmounts[i]; + if (amount != 0) + { + pOwnerExt->UpdateResourceAmount(static_cast(i), amount); + } + } + } +} + +bool SWTypeExt::AreResourcesSufficient(HouseClass* pHouse) const +{ + if (!pHouse) + return false; + + const auto pOwnerExt = HouseExt::TryFetch(pHouse); + if (!pOwnerExt) + return false; + + for (size_t i = 0; i < this->SW_ResourceAmounts.size(); ++i) + { + const int amount = this->SW_ResourceAmounts[i]; + if (amount < 0) // Negative amount = required cost + { + if (pOwnerExt->GetResourceAmount(static_cast(i)) < std::abs(amount)) + return false; + } + } + + return true; } void SWTypeExt::ApplyActivatedMessage(SuperClass* pSW) const diff --git a/src/Ext/SWType/Hooks.cpp b/src/Ext/SWType/Hooks.cpp index 85cb93ad0b..5c620f4b00 100644 --- a/src/Ext/SWType/Hooks.cpp +++ b/src/Ext/SWType/Hooks.cpp @@ -266,7 +266,7 @@ DEFINE_JUMP(LJMP, 0x6A8D07, 0x6A8D17) // Skip tabIndex check #pragma endregion // Full rewrite -DEFINE_HOOK(0x6CC367, SuperClass_IsReady_BattlePoints, 0xD) +DEFINE_HOOK(0x6CC367, SuperClass_IsReady_Resources, 0xD) { GET(SuperClass*, pSuper, ECX); @@ -282,15 +282,9 @@ DEFINE_HOOK(0x6CC367, SuperClass_IsReady_BattlePoints, 0xD) } const auto pExt = SWTypeExt::Fetch(pSuper->Type); - if (pExt->BattlePoints_Amount != 0) + if (!pExt->AreResourcesSufficient(pSuper->Owner)) { - const auto pOwnerExt = HouseExt::Fetch(pSuper->Owner); - - if (pExt->BattlePoints_Amount < 0) - { - if (pOwnerExt->BattlePoints < std::abs(pExt->BattlePoints_Amount)) - return ReturnZero; - } + return ReturnZero; } return ReturnIsReady; diff --git a/src/Ext/Side/Body.cpp b/src/Ext/Side/Body.cpp index a26f4f390f..2ec456b45e 100644 --- a/src/Ext/Side/Body.cpp +++ b/src/Ext/Side/Body.cpp @@ -45,9 +45,11 @@ void SideExt::LoadFromINIFile(CCINIClass* pINI) this->Sidebar_PowerDelta_ColorRed.Read(exINI, pSection, "Sidebar.PowerDelta.ColorRed"); this->Sidebar_PowerDelta_ColorGrey.Read(exINI, pSection, "Sidebar.PowerDelta.ColorGrey"); this->Sidebar_PowerDelta_Align.Read(exINI, pSection, "Sidebar.PowerDelta.Align"); - this->Sidebar_BattlePoints_Offset.Read(exINI, pSection, "Sidebar.BattlePoints.Offset"); - this->Sidebar_BattlePoints_Color.Read(exINI, pSection, "Sidebar.BattlePoints.Color"); - this->Sidebar_BattlePoints_Align.Read(exINI, pSection, "Sidebar.BattlePoints.Align"); + this->Sidebar_ResourceTypes_Offset.Read(exINI, pSection, "Sidebar.ResourceTypes.Offset"); + this->Sidebar_ResourceTypes_Color.Read(exINI, pSection, "Sidebar.ResourceTypes.Color"); + this->Sidebar_ResourceTypes_Align.Read(exINI, pSection, "Sidebar.ResourceTypes.Align"); + this->Sidebar_ResourceTypes_Types.Read(exINI, pSection, "Sidebar.ResourceTypes.Types"); + this->ToolTip_Background_Color.Read(exINI, pSection, "ToolTip.Background.Color"); this->ToolTip_Background_Opacity.Read(exINI, pSection, "ToolTip.Background.Opacity"); this->ToolTip_Background_BlurSize.Read(exINI, pSection, "ToolTip.Background.BlurSize"); @@ -60,7 +62,7 @@ void SideExt::LoadFromINIFile(CCINIClass* pINI) this->SuperWeaponSidebar_BottomPCX.Read(pINI, pSection, "SuperWeaponSidebar.BottomPCX"); } -// ============================= +// ============= // load / save template @@ -84,9 +86,10 @@ void SideExt::Serialize(T& Stm) .Process(this->Sidebar_PowerDelta_ColorRed) .Process(this->Sidebar_PowerDelta_ColorGrey) .Process(this->Sidebar_PowerDelta_Align) - .Process(this->Sidebar_BattlePoints_Offset) - .Process(this->Sidebar_BattlePoints_Color) - .Process(this->Sidebar_BattlePoints_Align) + .Process(this->Sidebar_ResourceTypes_Offset) + .Process(this->Sidebar_ResourceTypes_Color) + .Process(this->Sidebar_ResourceTypes_Align) + .Process(this->Sidebar_ResourceTypes_Types) .Process(this->ToolTip_Background_Color) .Process(this->ToolTip_Background_Opacity) .Process(this->ToolTip_Background_BlurSize) diff --git a/src/Ext/Side/Body.h b/src/Ext/Side/Body.h index d104ad7930..32f65dabc5 100644 --- a/src/Ext/Side/Body.h +++ b/src/Ext/Side/Body.h @@ -4,6 +4,7 @@ #include #include #include +#include class SideExt final : public AbstractTypeExt { @@ -41,9 +42,10 @@ class SideExt final : public AbstractTypeExt Valueable Sidebar_PowerDelta_ColorRed; Valueable Sidebar_PowerDelta_ColorGrey; Valueable Sidebar_PowerDelta_Align; - Valueable Sidebar_BattlePoints_Offset; - Nullable Sidebar_BattlePoints_Color; - Valueable Sidebar_BattlePoints_Align; + Valueable Sidebar_ResourceTypes_Offset; + Nullable Sidebar_ResourceTypes_Color; + Valueable Sidebar_ResourceTypes_Align; + ValueableIdxVector Sidebar_ResourceTypes_Types; Nullable ToolTip_Background_Color; Nullable ToolTip_Background_Opacity; Nullable ToolTip_Background_BlurSize; @@ -75,9 +77,10 @@ class SideExt final : public AbstractTypeExt , Sidebar_PowerDelta_ColorRed { { 255, 0, 0 } } , Sidebar_PowerDelta_ColorGrey { { 0x80,0x80,0x80 } } , Sidebar_PowerDelta_Align { TextAlign::Left } - , Sidebar_BattlePoints_Offset { { 0, 0 } } - , Sidebar_BattlePoints_Color {} - , Sidebar_BattlePoints_Align { TextAlign::Left } + , Sidebar_ResourceTypes_Offset { { 0, 0 } } + , Sidebar_ResourceTypes_Color {} + , Sidebar_ResourceTypes_Align { TextAlign::Left } + , Sidebar_ResourceTypes_Types {} , ToolTip_Background_Color { } , ToolTip_Background_Opacity { } , ToolTip_Background_BlurSize { } diff --git a/src/Ext/TAction/Body.cpp b/src/Ext/TAction/Body.cpp index 201e8dfef7..2d01f8a4f0 100644 --- a/src/Ext/TAction/Body.cpp +++ b/src/Ext/TAction/Body.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include //Static init @@ -92,6 +93,11 @@ bool TActionExt::Execute(TActionClass* pThis, HouseClass* pHouse, ObjectClass* p case PhobosTriggerAction::DeleteBanner: return TActionExt::DeleteBanner(pThis, pHouse, pObject, pTrigger, location); + case PhobosTriggerAction::SetCustomResource: + case PhobosTriggerAction::AddCustomResource: + case PhobosTriggerAction::SubtractCustomResource: + return TActionExt::ModifyCustomResource(pThis, pHouse, pObject, pTrigger, location); + default: bHandled = false; return true; @@ -834,6 +840,51 @@ bool TActionExt::DeleteBanner(TActionClass* pThis, HouseClass* pHouse, ObjectCla return true; } +bool TActionExt::ModifyCustomResource(TActionClass* pThis, HouseClass* pHouse, ObjectClass* pObject, TriggerClass* pTrigger, CellStruct const& location) +{ + if (!pThis) + return true; + + const int resIdx = ResourceTypeClass::FindIndex(pThis->Text); + if (resIdx < 0) + return true; + + HouseClass* pTargetHouse = pHouse; + if (pThis->Value2 >= 0) + { + pTargetHouse = HouseClass::Index_IsMP(pThis->Value2) + ? HouseClass::FindByIndex(pThis->Value2) + : HouseClass::FindByCountryIndex(pThis->Value2); + } + if (!pTargetHouse) + pTargetHouse = HouseClass::CurrentPlayer; + + if (!pTargetHouse) + return true; + + const auto pHouseExt = HouseExt::TryFetch(pTargetHouse); + if (!pHouseExt) + return true; + + const auto action = static_cast(pThis->ActionKind); + const int value = pThis->Value; + + if (action == PhobosTriggerAction::SetCustomResource) + { + pHouseExt->SetResourceAmount(resIdx, value); + } + else if (action == PhobosTriggerAction::AddCustomResource) + { + pHouseExt->UpdateResourceAmount(resIdx, value); + } + else if (action == PhobosTriggerAction::SubtractCustomResource) + { + pHouseExt->UpdateResourceAmount(resIdx, -value); + } + + return true; +} + // ============================= // container diff --git a/src/Ext/TAction/Body.h b/src/Ext/TAction/Body.h index 03f956755d..c2354544a0 100644 --- a/src/Ext/TAction/Body.h +++ b/src/Ext/TAction/Body.h @@ -28,6 +28,10 @@ enum class PhobosTriggerAction : unsigned int SetTeamDelay = 610, SetNextScanario = 611, + SetCustomResource = 620, + AddCustomResource = 621, + SubtractCustomResource = 622, + CreateBannerLocal = 800, // any banner w/ local variable CreateBannerGlobal = 801, // any banner w/ global variable DeleteBanner = 802, @@ -95,6 +99,7 @@ class TActionExt final : public AbstractExt ACTION_FUNC(CreateBannerLocal); ACTION_FUNC(CreateBannerGlobal); ACTION_FUNC(DeleteBanner); + ACTION_FUNC(ModifyCustomResource); static bool RunSuperWeaponAt(TActionClass* pThis, int X, int Y); diff --git a/src/Ext/TEvent/Body.cpp b/src/Ext/TEvent/Body.cpp index c7cd3cee94..0fec1a220d 100644 --- a/src/Ext/TEvent/Body.cpp +++ b/src/Ext/TEvent/Body.cpp @@ -1,7 +1,9 @@ #include "Body.h" +#include #include #include +#include //Static init TEventExt::ExtContainer TEventExt::ExtMap; @@ -147,6 +149,10 @@ std::optional TEventExt::Execute(TEventClass* pThis, int iEvent, HouseClas return TEventExt::CellHasAnyTechnoTypeFromListTEvent(pThis, pObject, pHouse); case PhobosTriggerEvent::AttachedIsUnderAttachedEffect: return TEventExt::AttachedIsUnderAttachedEffectTEvent(pThis, pObject); + case PhobosTriggerEvent::HouseHasCustomResourceGreaterThan: + return TEventExt::HouseHasCustomResource(pThis, pHouse, true); + case PhobosTriggerEvent::HouseHasCustomResourceLessThan: + return TEventExt::HouseHasCustomResource(pThis, pHouse, false); // If it requires an additional object as like mapping events 7 or 48, please fill it in here. @@ -339,6 +345,29 @@ bool TEventExt::AttachedIsUnderAttachedEffectTEvent(TEventClass* pThis, ObjectCl return false; } +bool TEventExt::HouseHasCustomResource(TEventClass* pThis, HouseClass* pEventHouse, bool isGreaterThan) +{ + if (!pThis) + return false; + + const int resIdx = ResourceTypeClass::FindIndex(pThis->String); + if (resIdx < 0) + return false; + + HouseClass* pHouse = pEventHouse ? pEventHouse : HouseClass::CurrentPlayer; + if (!pHouse) + return false; + + const auto pHouseExt = HouseExt::TryFetch(pHouse); + if (!pHouseExt) + return false; + + const int currentAmount = pHouseExt->GetResourceAmount(resIdx); + const int targetAmount = pThis->Value; + + return isGreaterThan ? (currentAmount > targetAmount) : (currentAmount < targetAmount); +} + // ============================= // container diff --git a/src/Ext/TEvent/Body.h b/src/Ext/TEvent/Body.h index b54f1755c7..3b20043d91 100644 --- a/src/Ext/TEvent/Body.h +++ b/src/Ext/TEvent/Body.h @@ -52,6 +52,8 @@ enum PhobosTriggerEvent CellHasTechnoType = 604, CellHasAnyTechnoTypeFromList = 605, AttachedIsUnderAttachedEffect = 606, + HouseHasCustomResourceGreaterThan = 610, + HouseHasCustomResourceLessThan = 611, _DummyMaximum, }; @@ -104,6 +106,7 @@ class TEventExt final : public AbstractExt static bool CellHasTechnoTypeTEvent(TEventClass* pThis, ObjectClass* pObject, HouseClass* pHouse); static bool AttachedIsUnderAttachedEffectTEvent(TEventClass* pThis, ObjectClass* pObject); + static bool HouseHasCustomResource(TEventClass* pThis, HouseClass* pHouse, bool isGreaterThan); public: diff --git a/src/Ext/Techno/Body.Internal.cpp b/src/Ext/Techno/Body.Internal.cpp index 9f1d5d994a..5ea3ab9c2a 100644 --- a/src/Ext/Techno/Body.Internal.cpp +++ b/src/Ext/Techno/Body.Internal.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include // Unsorted methods @@ -51,10 +53,25 @@ void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller, House { if (auto pHouseExt = HouseExt::TryFetch(pHouse)) { - if (pHouseExt->AreBattlePointsEnabled()) + const size_t resourceCount = ResourceTypeClass::Array.size(); + for (size_t i = 0; i < resourceCount; ++i) { - int points = pHouseExt->CalculateBattlePoints(pVictim); - pHouseExt->UpdateBattlePoints(points); + const int bounty = pHouseExt->CalculateResourceBounty(static_cast(i), pVictim); + if (bounty != 0) + { + pHouseExt->UpdateResourceAmount(static_cast(i), bounty); + if (const auto pResource = ResourceTypeClass::Array[i].get()) + { + FlyingStrings::AddResourceString( + pResource, + bounty, + pVictim, + pHouse, + AffectedHouse::All, + pVictim->GetRenderCoords() + ); + } + } } } } diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index efce80bc8f..15d84bd349 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -14,6 +14,12 @@ // It's not recommended to do anything more here it could have a better place for performance consideration void TechnoExt::OnEarlyUpdate() { + if (!this->ResourceInitialized && this->OwnerObject() && this->OwnerObject()->Owner && !this->OwnerObject()->InLimbo) + { + this->InitializeResourceProductions(); + } + + this->UpdateResourceProductions(); this->UpdateShield(); this->UpdateAttachEffects(); this->EatPassengers(); diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 1615942f11..1a6cd2e00e 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -15,6 +17,8 @@ TechnoExt::~TechnoExt() { + this->ApplyCollectorRegistration(false); + auto const pTypeExt = this->TypeExtData; auto const pType = pTypeExt->OwnerObject(); auto const pThis = this->OwnerObject(); @@ -1218,9 +1222,224 @@ void TechnoExt::Serialize(T& Stm) .Process(this->LastTargetCrdClearTimer) .Process(this->ShouldBeDead) .Process(this->PreventCrewEscape) + .Process(this->ResourceProductionTimers) + .Process(this->ResourceStartupGranted) + .Process(this->ResourceInitialized) + .Process(this->CollectorRegistered) ; } +void TechnoExt::InitializeResourceProductions() +{ + const auto pThis = this->OwnerObject(); + const auto pTypeExt = this->TypeExtData; + if (!pThis || !pTypeExt) + return; + + this->ResourceInitialized = true; + this->ApplyCollectorRegistration(true); + + this->ResourceProductionTimers.clear(); + int startupStackOffsetY = 0; + for (const auto& prod : pTypeExt->ResourceProductions) + { + if (prod.Delay > 0 && prod.Amount != 0) + { + this->ResourceProductionTimers.push_back(ResourceProductionTimer { + prod.ResourceIndex, + prod.Delay, + prod.Amount, + prod.Delay + }); + } + + if (!this->ResourceStartupGranted && prod.Startup != 0) + { + if (const auto pHouseExt = HouseExt::TryFetch(pThis->Owner)) + { + if (pHouseExt->IsResourceEnabled(prod.ResourceIndex)) + { + pHouseExt->UpdateResourceAmount(prod.ResourceIndex, prod.Startup); + if (prod.ResourceIndex < static_cast(ResourceTypeClass::Array.size())) + { + if (const auto pResource = ResourceTypeClass::Array[prod.ResourceIndex].get()) + { + Point2D offset = prod.Display_Offset.Get(Point2D::Empty); + offset.Y += startupStackOffsetY; + const ColorStruct* pColor = prod.Display_Color.isset() ? &prod.Display_Color.Get() : nullptr; + const AffectedHouse houses = prod.Display_Houses.Get(AffectedHouse::All); + + FlyingStrings::AddResourceString( + pResource, + prod.Startup, + pThis, + pThis->Owner, + houses, + pThis->GetCoords(), + offset, + pColor + ); + startupStackOffsetY -= 14; + } + } + } + } + } + } + this->ResourceStartupGranted = true; +} + +void TechnoExt::UpdateResourceProductions() +{ + const auto pThis = this->OwnerObject(); + if (!pThis || !pThis->Owner || pThis->InLimbo || pThis->IsBeingWarpedOut() || !pThis->IsAlive) + return; + + if (pThis->TemporalTargetingMe || pThis->IsIronCurtained() || pThis->Deactivated || pThis->IsUnderEMP()) + return; + + if (pThis->WhatAmI() == AbstractType::Building) + { + auto const pBuilding = static_cast(pThis); + if (pBuilding->Type->Powered && !pBuilding->IsPowerOnline()) + return; + } + + const auto pTypeExt = this->TypeExtData; + if (!pTypeExt) + return; + + if (this->ResourceProductionTimers.empty() && !pTypeExt->ResourceProductions.empty()) + { + this->InitializeResourceProductions(); + } + + const auto pHouseExt = HouseExt::TryFetch(pThis->Owner); + if (!pHouseExt) + return; + + int periodicStackOffsetY = 0; + for (auto& entry : this->ResourceProductionTimers) + { + if (entry.Delay <= 0 || entry.Amount == 0) + continue; + + if (!pHouseExt->IsResourceEnabled(entry.ResourceIndex)) + continue; + + --entry.Timer; + if (entry.Timer <= 0) + { + entry.Timer = entry.Delay; + pHouseExt->UpdateResourceAmount(entry.ResourceIndex, entry.Amount); + if (entry.ResourceIndex < static_cast(ResourceTypeClass::Array.size())) + { + if (const auto pResource = ResourceTypeClass::Array[entry.ResourceIndex].get()) + { + Point2D offset = Point2D::Empty; + const ColorStruct* pColor = nullptr; + AffectedHouse houses = AffectedHouse::All; + + for (const auto& prod : pTypeExt->ResourceProductions) + { + if (prod.ResourceIndex == entry.ResourceIndex) + { + offset = prod.Display_Offset.Get(Point2D::Empty); + if (prod.Display_Color.isset()) + pColor = &prod.Display_Color.Get(); + houses = prod.Display_Houses.Get(AffectedHouse::All); + break; + } + } + + offset.Y += periodicStackOffsetY; + FlyingStrings::AddResourceString( + pResource, + entry.Amount, + pThis, + pThis->Owner, + houses, + pThis->GetCoords(), + offset, + pColor + ); + periodicStackOffsetY -= 14; + } + } + } + } +} + +void TechnoExt::ApplyCollectorRegistration(bool registering, HouseClass* pHouse) +{ + const auto pThis = this->OwnerObject(); + HouseClass* const pTargetHouse = pHouse ? pHouse : (pThis ? pThis->Owner : nullptr); + if (!pTargetHouse) + return; + + const auto pTypeExt = this->TypeExtData; + if (!pTypeExt || pTypeExt->ResourceCollectors.empty()) + return; + + if (!pHouse && registering == this->CollectorRegistered) + return; + + const auto pHouseExt = HouseExt::TryFetch(pTargetHouse); + if (!pHouseExt) + return; + + for (int resIdx : pTypeExt->ResourceCollectors) + { + if (resIdx >= 0) + { + if (resIdx >= static_cast(pHouseExt->ResourceCollectorCounts.size())) + pHouseExt->ResourceCollectorCounts.resize(resIdx + 1, 0); + + if (registering) + pHouseExt->ResourceCollectorCounts[resIdx]++; + else + pHouseExt->ResourceCollectorCounts[resIdx] = std::max(0, pHouseExt->ResourceCollectorCounts[resIdx] - 1); + } + } + + if (!pHouse) + this->CollectorRegistered = registering; +} + +int TechnoExt::GetResourceRefund(TechnoClass* pTechno, int resourceIdx, bool isGrinder) +{ + if (!pTechno || resourceIdx < 0 || resourceIdx >= static_cast(ResourceTypeClass::Array.size())) + return 0; + + const auto pType = pTechno->GetTechnoType(); + if (!pType) + return 0; + + const auto pTypeExt = TechnoTypeExt::Fetch(pType); + if (!pTypeExt) + return 0; + + const int resourceCost = (resourceIdx < static_cast(pTypeExt->ResourceCosts.size())) ? pTypeExt->ResourceCosts[resourceIdx] : 0; + const int specificSoylent = (isGrinder && resourceIdx < static_cast(pTypeExt->ResourceSoylents.size())) ? pTypeExt->ResourceSoylents[resourceIdx] : -1; + + double baseRefund = 0.0; + if (isGrinder && specificSoylent >= 0) + { + baseRefund = static_cast(specificSoylent); + } + else if (isGrinder && pType->Soylent >= 0 && pType->Cost > 0) + { + baseRefund = static_cast(resourceCost) * (static_cast(pType->Soylent) / static_cast(pType->Cost)); + } + else + { + baseRefund = static_cast(resourceCost) * RulesClass::Instance->RefundPercent; + } + + const double healthRatio = pTechno->GetHealthPercentage(); + return static_cast(std::round(baseRefund * healthRatio)); +} + void TechnoExt::OnDetach(AirstrikeClass* pTarget, bool removed) { if (removed) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 2f6daab567..68c031700e 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -87,6 +87,10 @@ class TechnoExt : public RadioExt, public Detach::Listener Powerup DropCrateType; bool PreventCrewEscape; + std::vector ResourceProductionTimers; + bool ResourceStartupGranted; + bool ResourceInitialized; + bool CollectorRegistered; TechnoExt(TechnoClass* OwnerObject) : RadioExt(OwnerObject) , TypeExtData { nullptr } @@ -137,6 +141,10 @@ class TechnoExt : public RadioExt, public Detach::Listener , DropCrate { -1 } , DropCrateType { Powerup::Money } , PreventCrewEscape { false } + , ResourceProductionTimers {} + , ResourceStartupGranted { false } + , ResourceInitialized { false } + , CollectorRegistered { false } { } void OnEarlyUpdate(); @@ -156,6 +164,10 @@ class TechnoExt : public RadioExt, public Detach::Listener // else. Overridden by FootExt, which owns the tunnel state. virtual bool IsInTunnelState() const { return false; } + void UpdateResourceProductions(); + void InitializeResourceProductions(); + void ApplyCollectorRegistration(bool registering, HouseClass* pHouse = nullptr); + static int GetResourceRefund(TechnoClass* pTechno, int resourceIdx, bool isGrinder = false); void ApplyInterceptor(); bool CheckDeathConditions(bool isInLimbo = false); void EatPassengers(); diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 29e94b94f2..28ee6160ad 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1137,7 +1137,156 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->AttackMove_Follow_IncludeAir.Read(exINI, pSection, "AttackMove.Follow.IncludeAir"); this->AttackMove_Follow_IfMindControlIsFull.Read(exINI, pSection, "AttackMove.Follow.IfMindControlIsFull"); - this->BattlePoints.Read(exINI, pSection, "BattlePoints"); + const size_t resourceCount = ResourceTypeClass::Array.size(); + if (this->ResourceCosts.size() < resourceCount) + this->ResourceCosts.resize(resourceCount, 0); + if (this->ResourceBounties.size() < resourceCount) + this->ResourceBounties.resize(resourceCount, 0); + if (this->ResourceFriendlyBounties.size() < resourceCount) + this->ResourceFriendlyBounties.resize(resourceCount, 0); + if (this->ResourceSoylents.size() < resourceCount) + this->ResourceSoylents.resize(resourceCount, -1); + + for (size_t i = 0; i < resourceCount; ++i) + { + const auto pResource = ResourceTypeClass::Array[i].get(); + if (!pResource) + continue; + + char costTag[64]; + sprintf_s(costTag, "Cost.%s", (const char*)pResource->Name); + int costVal = 0; + if (exINI.ReadInteger(pSection, costTag, &costVal)) + { + this->ResourceCosts[i] = std::max(0, costVal); + } + + char bountyTag[64]; + sprintf_s(bountyTag, "Bounty.%s.Value", (const char*)pResource->Name); + int bountyVal = 0; + if (exINI.ReadInteger(pSection, bountyTag, &bountyVal)) + { + this->ResourceBounties[i] = std::max(0, bountyVal); + } + + char friendlyTag[64]; + sprintf_s(friendlyTag, "Bounty.%s.FriendlyValue", (const char*)pResource->Name); + int friendlyVal = 0; + if (exINI.ReadInteger(pSection, friendlyTag, &friendlyVal)) + { + this->ResourceFriendlyBounties[i] = -std::abs(friendlyVal); + } + + char soylentTag[64]; + sprintf_s(soylentTag, "Soylent.%s", (const char*)pResource->Name); + int soylentVal = 0; + if (exINI.ReadInteger(pSection, soylentTag, &soylentVal)) + { + this->ResourceSoylents[i] = std::max(0, soylentVal); + } + + // Check ResourceCollector. + char collectorTag[64]; + sprintf_s(collectorTag, "ResourceCollector.%s", (const char*)pResource->Name); + bool isCollector = false; + if (exINI.ReadBool(pSection, collectorTag, &isCollector)) + { + const int resIdx = static_cast(i); + auto it = std::find(this->ResourceCollectors.begin(), this->ResourceCollectors.end(), resIdx); + if (isCollector && it == this->ResourceCollectors.end()) + { + this->ResourceCollectors.push_back(resIdx); + } + else if (!isCollector && it != this->ResourceCollectors.end()) + { + this->ResourceCollectors.erase(it); + } + } + + // Check Production., Production..Delay, Production..Startup + char amountTag[64]; + sprintf_s(amountTag, "Production.%s", (const char*)pResource->Name); + char delayTag[64]; + sprintf_s(delayTag, "Production.%s.Delay", (const char*)pResource->Name); + char startupTag[64]; + sprintf_s(startupTag, "Production.%s.Startup", (const char*)pResource->Name); + + int amount = 0; + int delay = 0; + int startup = 0; + + const bool hasAmount = exINI.ReadInteger(pSection, amountTag, &amount); + const bool hasDelay = exINI.ReadInteger(pSection, delayTag, &delay); + const bool hasStartup = exINI.ReadInteger(pSection, startupTag, &startup); + + char displayOffsetTag[64]; + sprintf_s(displayOffsetTag, "Production.%s.Display.Offset", (const char*)pResource->Name); + char displayColorTag[64]; + sprintf_s(displayColorTag, "Production.%s.Display.Color", (const char*)pResource->Name); + char displayHousesTag[64]; + sprintf_s(displayHousesTag, "Production.%s.Display.Houses", (const char*)pResource->Name); + + Nullable displayOffset; + displayOffset.Read(exINI, pSection, displayOffsetTag); + + Nullable displayColor; + displayColor.Read(exINI, pSection, displayColorTag); + + Nullable displayHouses; + displayHouses.Read(exINI, pSection, displayHousesTag); + + const bool hasAnyProdTag = hasAmount || hasDelay || hasStartup || displayOffset.isset() || displayColor.isset() || displayHouses.isset(); + if (hasAnyProdTag) + { + const int resIdx = static_cast(i); + auto it = std::find_if(this->ResourceProductions.begin(), this->ResourceProductions.end(), + [resIdx](const ResourceProductionData& d) { return d.ResourceIndex == resIdx; }); + + if ((hasAmount && amount != 0 && hasDelay && delay > 0) || (hasStartup && startup != 0)) + { + ResourceProductionData data { + resIdx, + amount, + delay, + startup, + displayOffset, + displayColor, + displayHouses + }; + + if (it != this->ResourceProductions.end()) + { + if (!hasAmount) data.Amount = it->Amount; + if (!hasDelay) data.Delay = it->Delay; + if (!hasStartup) data.Startup = it->Startup; + if (!displayOffset.isset()) data.Display_Offset = it->Display_Offset; + if (!displayColor.isset()) data.Display_Color = it->Display_Color; + if (!displayHouses.isset()) data.Display_Houses = it->Display_Houses; + *it = data; + } + else + { + this->ResourceProductions.push_back(data); + } + } + else if (it != this->ResourceProductions.end()) + { + if (hasAmount && amount == 0 && hasStartup && startup == 0) + { + this->ResourceProductions.erase(it); + } + else + { + if (hasAmount) it->Amount = amount; + if (hasDelay) it->Delay = delay; + if (hasStartup) it->Startup = startup; + if (displayOffset.isset()) it->Display_Offset = displayOffset; + if (displayColor.isset()) it->Display_Color = displayColor; + if (displayHouses.isset()) it->Display_Houses = displayHouses; + } + } + } + } this->Ammo_AutoConvertMinimumAmount.Read(exINI, pSection, "Ammo.AutoConvertMinimumAmount"); this->Ammo_AutoConvertMaximumAmount.Read(exINI, pSection, "Ammo.AutoConvertMaximumAmount"); @@ -1774,8 +1923,12 @@ void TechnoTypeExt::Serialize(T& Stm) .Process(this->VoiceEliteWeaponAttacks) .Process(this->TeamMember_ConsideredAs) - - .Process(this->BattlePoints) + .Process(this->ResourceCosts) + .Process(this->ResourceBounties) + .Process(this->ResourceFriendlyBounties) + .Process(this->ResourceSoylents) + .Process(this->ResourceProductions) + .Process(this->ResourceCollectors) .Process(this->AttackFriendlies) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 4b5d350797..f5fae9e1a1 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -452,7 +453,12 @@ class TechnoTypeExt : public ObjectTypeExt Nullable Unsellable; Nullable KeepAlive; - Nullable BattlePoints; + std::vector ResourceCosts; + std::vector ResourceBounties; + std::vector ResourceFriendlyBounties; + std::vector ResourceSoylents; + std::vector ResourceProductions; + std::vector ResourceCollectors; TechnoTypeExt(TechnoTypeClass* OwnerObject) : ObjectTypeExt(OwnerObject) , HealthBar_Hide { false } @@ -854,7 +860,12 @@ class TechnoTypeExt : public ObjectTypeExt // Ares 3.0 , Unsellable {} , KeepAlive {} - , BattlePoints {} + , ResourceCosts {} + , ResourceBounties {} + , ResourceFriendlyBounties {} + , ResourceSoylents {} + , ResourceProductions {} + , ResourceCollectors {} { } virtual ~TechnoTypeExt() = default; diff --git a/src/Ext/Tiberium/Body.cpp b/src/Ext/Tiberium/Body.cpp index 95d8f4629e..780f9fbb74 100644 --- a/src/Ext/Tiberium/Body.cpp +++ b/src/Ext/Tiberium/Body.cpp @@ -1,4 +1,5 @@ #include "Body.h" +#include TiberiumExt::ExtContainer TiberiumExt::ExtMap; @@ -10,6 +11,8 @@ void TiberiumExt::Serialize(T& Stm) { Stm .Process(this->MinimapColor) + .Process(this->ResourceType) + .Process(this->ResourceValue) ; } @@ -20,6 +23,8 @@ void TiberiumExt::LoadFromINIFile(CCINIClass* const pINI) INI_EX exINI(pINI); this->MinimapColor.Read(exINI, pSection, "MinimapColor"); + this->ResourceType.Read(exINI, pSection, "ResourceType"); + this->ResourceValue.Read(exINI, pSection, "ResourceValue"); } void TiberiumExt::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/Tiberium/Body.h b/src/Ext/Tiberium/Body.h index 4db057f521..bf40d15a46 100644 --- a/src/Ext/Tiberium/Body.h +++ b/src/Ext/Tiberium/Body.h @@ -4,6 +4,7 @@ #include #include #include +#include class TiberiumExt final : public AbstractTypeExt { @@ -23,9 +24,13 @@ class TiberiumExt final : public AbstractTypeExt } Nullable MinimapColor; + NullableIdx ResourceType; + Nullable ResourceValue; TiberiumExt(TiberiumClass* OwnerObject) : AbstractTypeExt(OwnerObject) , MinimapColor {} + , ResourceType { -1 } + , ResourceValue {} { } virtual ~TiberiumExt() = default; diff --git a/src/Ext/Unit/Hooks.Harvester.cpp b/src/Ext/Unit/Hooks.Harvester.cpp index bf81210684..f8a4efcc7c 100644 --- a/src/Ext/Unit/Hooks.Harvester.cpp +++ b/src/Ext/Unit/Hooks.Harvester.cpp @@ -118,6 +118,7 @@ DEFINE_HOOK(0x73E411, UnitClass_Mission_Unload_DumpAmount, 0x7) GET(UnitClass*, pThis, ESI); GET(const int, tiberiumIdx, EBP); + REF_STACK(float, stackAmount, 0x18); const auto pTypeExt = UnitTypeExt::Fetch(pThis->Type); const float totalAmount = pThis->Tiberium.GetAmount(tiberiumIdx); float dumpAmount = pTypeExt->HarvesterDumpAmount.Get(RulesExt::Global()->HarvesterDumpAmount); @@ -125,6 +126,7 @@ DEFINE_HOOK(0x73E411, UnitClass_Mission_Unload_DumpAmount, 0x7) if (dumpAmount <= 0.0f || totalAmount < dumpAmount) dumpAmount = totalAmount; + stackAmount = dumpAmount; __asm fld dumpAmount; return SkipGameCode; diff --git a/src/Misc/FlyingStrings.cpp b/src/Misc/FlyingStrings.cpp index 59fcf6e36b..640eb11c95 100644 --- a/src/Misc/FlyingStrings.cpp +++ b/src/Misc/FlyingStrings.cpp @@ -3,6 +3,7 @@ #include #include #include +#include std::vector FlyingStrings::Data; @@ -21,7 +22,7 @@ void FlyingStrings::Add(const wchar_t* text, const CoordStruct& coords, ColorStr item.PixelOffset = pixelOffset; item.CreationFrame = Unsorted::CurrentFrame; item.Color = Drawing::RGB_To_Int(color); - PhobosCRT::wstrCopy(item.Text, text, 0x20); + PhobosCRT::wstrCopy(item.Text, text, 0x40); Data.emplace_back(item); } @@ -43,18 +44,72 @@ void FlyingStrings::AddMoneyString(int amount, ObjectClass* pSource, HouseClass* swprintf_s(moneyStr, L"%ls%ls%d", isPositive ? L"+" : L"-", Phobos::UI::CostLabel, std::abs(amount)); int width = 0, height = 0; - BitFont::Instance->GetTextDimension(moneyStr, &width, &height, 120); - pixelOffset.X -= (width / 2); + if (BitFont::Instance) + { + BitFont::Instance->GetTextDimension(moneyStr, &width, &height, 120); + pixelOffset.X -= (width / 2); + } FlyingStrings::Add(moneyStr, coords, color, pixelOffset); } +void FlyingStrings::AddResourceString(const ResourceTypeClass* pResource, int amount, ObjectClass* pSource, HouseClass* pOwner, + AffectedHouse displayToHouses, const CoordStruct& coords, Point2D pixelOffset, const ColorStruct* pColorOverride) +{ + if (!pResource || amount == 0 || MapClass::Instance.IsLocationShrouded(coords)) + return; + + if (displayToHouses != AffectedHouse::All && !EnumFunctions::CanTargetHouse(displayToHouses, pOwner, HouseClass::CurrentPlayer)) + return; + + if (pSource && pSource->VisualCharacter(false, nullptr) == VisualType::Hidden) + return; + + const bool isPositive = amount > 0; + ColorStruct color; + if (pColorOverride && *pColorOverride != ColorStruct { 0, 0, 0 }) + color = *pColorOverride; + else + { + const ColorStruct resColor = pResource->Display_Color.Get(); + color = (resColor != ColorStruct { 0, 0, 0 }) ? resColor : (isPositive ? ColorStruct { 0, 255, 0 } : ColorStruct { 255, 0, 0 }); + } + + wchar_t resStr[0x20]; + const wchar_t* label = pResource->Display_Label.Get(); + const bool useSpace = pResource->Display_Label_UseSpace.Get(); + if (label && *label) + { + if (pResource->Display_Label_InvertPosition.Get()) + { + swprintf_s(resStr, useSpace ? L"%ls%d %ls" : L"%ls%d%ls", isPositive ? L"+" : L"-", std::abs(amount), label); + } + else + { + swprintf_s(resStr, useSpace ? L"%ls%ls %d" : L"%ls%ls%d", isPositive ? L"+" : L"-", label, std::abs(amount)); + } + } + else + { + swprintf_s(resStr, L"%ls%d", isPositive ? L"+" : L"-", std::abs(amount)); + } + + int width = 0, height = 0; + if (BitFont::Instance) + { + BitFont::Instance->GetTextDimension(resStr, &width, &height, 120); + pixelOffset.X -= (width / 2); + } + + FlyingStrings::Add(resStr, coords, color, pixelOffset); +} + void FlyingStrings::UpdateAll() { - if (Data.empty()) + if (Data.empty() || !TacticalClass::Instance || !DSurface::Temp) return; - for (int i = Data.size() - 1; i >= 0; --i) + for (int i = static_cast(Data.size()) - 1; i >= 0; --i) { auto& dataItem = Data[i]; diff --git a/src/Misc/FlyingStrings.h b/src/Misc/FlyingStrings.h index 88956f9a24..c1300c62c7 100644 --- a/src/Misc/FlyingStrings.h +++ b/src/Misc/FlyingStrings.h @@ -9,6 +9,8 @@ By AlexB and Joshy #include #include +class ResourceTypeClass; + class FlyingStrings { private: @@ -18,7 +20,7 @@ class FlyingStrings CoordStruct Location; Point2D PixelOffset; int CreationFrame; - wchar_t Text[0x20]; + wchar_t Text[0x40]; COLORREF Color; }; @@ -31,5 +33,6 @@ class FlyingStrings public: static void Add(const wchar_t* text, const CoordStruct& coords, ColorStruct color, Point2D pixelOffset = Point2D::Empty); static void AddMoneyString(int amount, ObjectClass* pSource, HouseClass* pOwner, AffectedHouse displayToHouses, const CoordStruct& coords, Point2D pixelOffset = Point2D::Empty); + static void AddResourceString(const ResourceTypeClass* pResource, int amount, ObjectClass* pSource, HouseClass* pOwner, AffectedHouse displayToHouses, const CoordStruct& coords, Point2D pixelOffset = Point2D::Empty, const ColorStruct* pColorOverride = nullptr); static void UpdateAll(); }; diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index 77946f12a5..763ec296eb 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -3541,6 +3541,14 @@ DEFINE_JUMP(LJMP, 0x502630, 0x50263B); DEFINE_HOOK(0x7015A2, TechnoClass_SetOwningHouse_RefreshSidebar, 0x6) { GET(TechnoClass*, pThis, ESI); + GET(HouseClass*, pNewOwner, EBX); + + if (const auto pExt = TechnoExt::Fetch(pThis)) + { + pExt->ApplyCollectorRegistration(false, pThis->Owner); + pExt->ApplyCollectorRegistration(true, pNewOwner); + } + SidebarClass::Instance.OnTechnoDestroyed(pThis); return 0; } diff --git a/src/Misc/Hooks.UI.cpp b/src/Misc/Hooks.UI.cpp index 9d4d50866c..f1161358ec 100644 --- a/src/Misc/Hooks.UI.cpp +++ b/src/Misc/Hooks.UI.cpp @@ -10,6 +10,7 @@ #include #include +#include #include @@ -106,35 +107,118 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) RectangleStruct vRect = DSurface::Sidebar->GetRect(); auto pHouseExt = HouseExt::Fetch(pPlayer); + auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); - if (pHouseExt->AreBattlePointsEnabled()) + // Calculate base HUD coordinate based on Anchor + int baseX = 0; + int baseY = 0; + const Point2D baseOffset = ResourceTypeClass::Global_Display_BaseOffset.Get(); + + switch (ResourceTypeClass::Global_Display_Anchor.Get()) { - auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); - wchar_t counter[0x20]; + case ResourceDisplayAnchor::TopLeft: + baseX = 10 + baseOffset.X; + baseY = 2 + baseOffset.Y; + break; + case ResourceDisplayAnchor::TopRight: + baseX = DSurface::Sidebar->GetWidth() - 100 + baseOffset.X; + baseY = 2 + baseOffset.Y; + break; + case ResourceDisplayAnchor::BottomLeft: + baseX = 10 + baseOffset.X; + baseY = DSurface::Sidebar->GetHeight() - 30 + baseOffset.Y; + break; + case ResourceDisplayAnchor::BottomRight: + baseX = DSurface::Sidebar->GetWidth() - 100 + baseOffset.X; + baseY = DSurface::Sidebar->GetHeight() - 30 + baseOffset.Y; + break; + case ResourceDisplayAnchor::Sidebar: + default: + baseX = DSurface::Sidebar->GetWidth() / 2 - 70 + pSideExt->Sidebar_ResourceTypes_Offset.Get().X + baseOffset.X; + baseY = 2 + pSideExt->Sidebar_ResourceTypes_Offset.Get().Y + baseOffset.Y; + break; + } + + int const spacing = ResourceTypeClass::Global_Display_Spacing.Get(); + int stackIndex = 0; + + const bool hasExplicitTypes = !pSideExt->Sidebar_ResourceTypes_Types.empty(); + const size_t count = hasExplicitTypes ? pSideExt->Sidebar_ResourceTypes_Types.size() : ResourceTypeClass::Array.size(); + + for (size_t iter = 0; iter < count; ++iter) + { + const int i = hasExplicitTypes ? pSideExt->Sidebar_ResourceTypes_Types[iter] : static_cast(iter); + if (i < 0 || i >= static_cast(ResourceTypeClass::Array.size())) + continue; - ColorStruct clrToolTip = pSideExt->Sidebar_BattlePoints_Color.Get(Drawing::TooltipColor); + const auto pResource = ResourceTypeClass::Array[i].get(); + if (!pResource) + continue; + + if (!pHouseExt->IsResourceEnabled(i)) + continue; + + // Check display condition + if (pResource->Display_Condition.Get() == ResourceDisplayCondition::Never) + continue; + + const int amount = pHouseExt->GetResourceAmount(i); + const bool hasCollector = (i < static_cast(pHouseExt->ResourceCollectorCounts.size())) && (pHouseExt->ResourceCollectorCounts[i] > 0); + + if (!hasCollector) + { + if (pResource->Display_Condition.Get() == ResourceDisplayCondition::GreaterThanZero && amount <= 0) + continue; - int points = pHouseExt->BattlePoints; + if (pResource->Display_Condition.Get() == ResourceDisplayCondition::HasCollector) + continue; + } - if (Phobos::UI::BattlePointsSidebar_Label_InvertPosition) - swprintf_s(counter, L"%d %ls", points, Phobos::UI::BattlePointsSidebar_Label); + wchar_t counter[0x40]; + const wchar_t* label = pResource->Display_Label.Get(); + if (label && *label) + { + const bool useSpace = pResource->Display_Label_UseSpace.Get(); + if (pResource->Display_Label_InvertPosition.Get()) + swprintf_s(counter, useSpace ? L"%d %ls" : L"%d%ls", amount, label); + else + swprintf_s(counter, useSpace ? L"%ls %d" : L"%ls%d", label, amount); + } else - swprintf_s(counter, L"%ls %d", Phobos::UI::BattlePointsSidebar_Label, points); + { + swprintf_s(counter, L"%d", amount); + } - Point2D vPos = { - DSurface::Sidebar->GetWidth() / 2 - 70 + pSideExt->Sidebar_BattlePoints_Offset.Get().X, - 2 + pSideExt->Sidebar_BattlePoints_Offset.Get().Y - }; + Point2D vPos; + if (pResource->Display_Offset.isset()) + { + vPos = pResource->Display_Offset.Get(); + } + else + { + if (ResourceTypeClass::Global_Display_Orientation.Get() == ResourceDisplayOrientation::Horizontal) + { + vPos.X = baseX + stackIndex * spacing; + vPos.Y = baseY; + } + else + { + vPos.X = baseX; + vPos.Y = baseY + stackIndex * spacing; + } + ++stackIndex; + } + const ColorStruct resColor = pResource->Display_Color.Get(); + const ColorStruct clrToolTip = (resColor != ColorStruct { 0, 0, 0 }) ? resColor : pSideExt->Sidebar_ResourceTypes_Color.Get(Drawing::TooltipColor); auto const TextFlags = static_cast(static_cast(TextPrintType::UseGradPal | TextPrintType::Metal12) - | static_cast(pSideExt->Sidebar_BattlePoints_Align.Get())); + | static_cast(pSideExt->Sidebar_ResourceTypes_Align.Get())); DSurface::Sidebar->DrawText(counter, &vRect, &vPos, Drawing::RGB_To_Int(clrToolTip), 0, TextFlags); } if (Phobos::UI::HarvesterCounter_Show && Phobos::Config::ShowHarvesterCounter) { - const auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); wchar_t counter[0x20]; const int nActive = HouseExt::ActiveHarvesterCount(pPlayer); const int nTotal = HouseExt::TotalHarvesterCount(pPlayer); @@ -162,7 +246,6 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) if (Phobos::UI::PowerDelta_Show && Phobos::Config::ShowPowerDelta && pPlayer->Buildings.Count) { - const auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); wchar_t counter[0x20]; ColorStruct clrToolTip; @@ -200,7 +283,6 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) if (Phobos::UI::WeedsCounter_Show && Phobos::Config::ShowWeedsCounter) { - const auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); wchar_t counter[0x20]; const ColorStruct clrToolTip = pSideExt->Sidebar_WeedsCounter_Color.Get(Drawing::TooltipColor); diff --git a/src/Misc/PhobosToolTip.cpp b/src/Misc/PhobosToolTip.cpp index 5b70d2f209..ef2d29c333 100644 --- a/src/Misc/PhobosToolTip.cpp +++ b/src/Misc/PhobosToolTip.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -147,6 +148,31 @@ void PhobosToolTip::HelpText_Techno(TechnoTypeClass* pType) oss << std::setw(1) << nPower; } + for (size_t i = 0; i < pData->ResourceCosts.size(); ++i) + { + const int resCost = pData->ResourceCosts[i]; + if (resCost > 0 && i < ResourceTypeClass::Array.size()) + { + const auto pResource = ResourceTypeClass::Array[i].get(); + if (pResource) + { + const wchar_t* label = pResource->Display_Label.Get(); + const bool useSpace = pResource->Display_Label_UseSpace.Get(); + if (label && *label) + { + if (pResource->Display_Label_InvertPosition.Get()) + oss << L" " << resCost << (useSpace ? L" " : L"") << label; + else + oss << L" " << label << (useSpace ? L" " : L"") << resCost; + } + else + { + oss << L" " << resCost; + } + } + } + } + if (auto const pDesc = this->GetUIDescription(pData)) oss << L"\n" << pDesc; @@ -174,16 +200,33 @@ void PhobosToolTip::HelpText_Super(int swidx) showSth = true; } - if (int nPoints = std::abs(pData->BattlePoints_Amount)) + for (size_t i = 0; i < pData->SW_ResourceAmounts.size(); ++i) { - oss << L"\n"; + const int nAmount = pData->SW_ResourceAmounts[i]; + if (nAmount != 0) + { + oss << L"\n"; + const auto pResource = (i < ResourceTypeClass::Array.size()) ? ResourceTypeClass::Array[i].get() : nullptr; + const wchar_t* label = pResource ? pResource->Display_Label.Get() : L""; + const bool useSpace = pResource ? pResource->Display_Label_UseSpace.Get() : true; - if (pData->BattlePoints_Amount > 0) - oss << Phobos::UI::BattlePoints_Label << L"+" << nPoints; - else if (pData->BattlePoints_Amount < 0) - oss << Phobos::UI::BattlePoints_Label << L"-" << nPoints; + if (nAmount > 0) + { + if (pResource && pResource->Display_Label_InvertPosition.Get()) + oss << L"+" << std::abs(nAmount) << (useSpace ? L" " : L"") << label; + else + oss << label << (useSpace ? L" " : L"") << L"+" << std::abs(nAmount); + } + else + { + if (pResource && pResource->Display_Label_InvertPosition.Get()) + oss << L"-" << std::abs(nAmount) << (useSpace ? L" " : L"") << label; + else + oss << label << (useSpace ? L" " : L"") << L"-" << std::abs(nAmount); + } - showSth = true; + showSth = true; + } } const int rechargeTime = TickTimeToSeconds(pSuper->GetRechargeTime()); diff --git a/src/New/Type/ResourceTypeClass.cpp b/src/New/Type/ResourceTypeClass.cpp new file mode 100644 index 0000000000..1e43a203e0 --- /dev/null +++ b/src/New/Type/ResourceTypeClass.cpp @@ -0,0 +1,93 @@ +#include "ResourceTypeClass.h" + +template<> +const char* Enumerable::GetMainSection() +{ + return "ResourceTypes"; +} + +Valueable ResourceTypeClass::Global_Display_Orientation { ResourceDisplayOrientation::Vertical }; +Valueable ResourceTypeClass::Global_Display_Anchor { ResourceDisplayAnchor::TopRight }; +Valueable ResourceTypeClass::Global_Display_BaseOffset { Point2D::Empty }; +Valueable ResourceTypeClass::Global_Display_Spacing { 14 }; + +ResourceTypeClass::ResourceTypeClass(const char* pTitle) : Enumerable(pTitle) + , Display_Label {} + , Display_Label_InvertPosition { false } + , Display_Label_UseSpace { true } + , Display_Color { { 255, 255, 255 } } + , Display_Condition { ResourceDisplayCondition::GreaterThanZero } + , Display_Offset {} + , InitialValue { 0 } + , RequiresCollector { false } + , Bounty_Enabled { false } + , Bounty_DefaultValue { 0 } + , Bounty_DefaultFriendlyValue { 0 } + , Bounty_CanUseStandardPoints { false } + , Bounty_MoneyConversion { 0 } +{ +} + +void ResourceTypeClass::LoadGlobalsFromINI(CCINIClass* pINI) +{ + INI_EX exINI(pINI); + + Global_Display_Orientation.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Orientation"); + Global_Display_Anchor.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Anchor"); + Global_Display_BaseOffset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BaseOffset"); + Global_Display_Spacing.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Spacing"); +} + +void ResourceTypeClass::LoadFromINI(CCINIClass* pINI) +{ + const char* section = this->Name; + if (!pINI->GetSection(section)) + return; + + INI_EX exINI(pINI); + + this->Display_Label.Read(exINI, section, "Display.Label"); + this->Display_Label_InvertPosition.Read(exINI, section, "Display.Label.InvertPosition"); + this->Display_Label_UseSpace.Read(exINI, section, "Display.Label.UseSpace"); + this->Display_Color.Read(exINI, section, "Display.Color"); + this->Display_Condition.Read(exINI, section, "Display.Condition"); + this->Display_Offset.Read(exINI, section, "Display.Offset"); + + this->InitialValue.Read(exINI, section, "InitialValue"); + this->RequiresCollector.Read(exINI, section, "RequiresCollector"); + + this->Bounty_Enabled.Read(exINI, section, "Bounty.Enabled"); + this->Bounty_DefaultValue.Read(exINI, section, "Bounty.DefaultValue"); + this->Bounty_DefaultFriendlyValue.Read(exINI, section, "Bounty.DefaultFriendlyValue"); + this->Bounty_CanUseStandardPoints.Read(exINI, section, "Bounty.CanUseStandardPoints"); + this->Bounty_MoneyConversion.Read(exINI, section, "Bounty.MoneyConversion"); +} + +template +void ResourceTypeClass::Serialize(T& Stm) +{ + Stm + .Process(this->Display_Label) + .Process(this->Display_Label_InvertPosition) + .Process(this->Display_Label_UseSpace) + .Process(this->Display_Color) + .Process(this->Display_Condition) + .Process(this->Display_Offset) + .Process(this->InitialValue) + .Process(this->RequiresCollector) + .Process(this->Bounty_Enabled) + .Process(this->Bounty_DefaultValue) + .Process(this->Bounty_DefaultFriendlyValue) + .Process(this->Bounty_CanUseStandardPoints) + .Process(this->Bounty_MoneyConversion); +} + +void ResourceTypeClass::LoadFromStream(PhobosStreamReader& Stm) +{ + this->Serialize(Stm); +} + +void ResourceTypeClass::SaveToStream(PhobosStreamWriter& Stm) +{ + this->Serialize(Stm); +} diff --git a/src/New/Type/ResourceTypeClass.h b/src/New/Type/ResourceTypeClass.h new file mode 100644 index 0000000000..449baff0c5 --- /dev/null +++ b/src/New/Type/ResourceTypeClass.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include + +class ResourceTypeClass final : public Enumerable +{ +public: + // Global HUD Configuration + static Valueable Global_Display_Orientation; + static Valueable Global_Display_Anchor; + static Valueable Global_Display_BaseOffset; + static Valueable Global_Display_Spacing; + + // Per-Resource Display Configuration + Valueable Display_Label; + Valueable Display_Label_InvertPosition; + Valueable Display_Label_UseSpace; + Valueable Display_Color; + Valueable Display_Condition; + Nullable Display_Offset; + + // Economy and Activation + Valueable InitialValue; + Valueable RequiresCollector; + + // Bounty (Kills) + Valueable Bounty_Enabled; + Valueable Bounty_DefaultValue; + Valueable Bounty_DefaultFriendlyValue; + Valueable Bounty_CanUseStandardPoints; + Valueable Bounty_MoneyConversion; + + ResourceTypeClass(const char* pTitle = NONE_STR); + virtual ~ResourceTypeClass() = default; + + void LoadFromINI(CCINIClass* pINI); + static void LoadGlobalsFromINI(CCINIClass* pINI); + + void LoadFromStream(PhobosStreamReader& Stm); + void SaveToStream(PhobosStreamWriter& Stm); + +private: + template + void Serialize(T& Stm); +}; + +struct ResourceProductionData +{ + int ResourceIndex; + int Amount; + int Delay; + int Startup; + Nullable Display_Offset; + Nullable Display_Color; + Nullable Display_Houses; + + template + void Serialize(T& Stm) + { + Stm + .Process(this->ResourceIndex) + .Process(this->Amount) + .Process(this->Delay) + .Process(this->Startup) + .Process(this->Display_Offset) + .Process(this->Display_Color) + .Process(this->Display_Houses); + } +}; + +struct ResourceProductionTimer +{ + int ResourceIndex; + int Delay; + int Amount; + int Timer; + + template + void Serialize(T& Stm) + { + Stm + .Process(this->ResourceIndex) + .Process(this->Delay) + .Process(this->Amount) + .Process(this->Timer); + } +}; diff --git a/src/Phobos.Ext.cpp b/src/Phobos.Ext.cpp index bfd410084f..1e21149b53 100644 --- a/src/Phobos.Ext.cpp +++ b/src/Phobos.Ext.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -327,7 +328,8 @@ using PhobosTypeRegistry = TypeRegistry < AttachEffectTypeClass, AttachEffectClass, NewSWType, - SelectBoxTypeClass + SelectBoxTypeClass, + ResourceTypeClass // other classes > ; diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index 3e7bf13a85..e980a9e09e 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -23,9 +23,6 @@ const wchar_t* Phobos::UI::TimeLabel = L""; const wchar_t* Phobos::UI::HarvesterLabel = L""; const wchar_t* Phobos::UI::ShowBriefingResumeButtonLabel = L""; const wchar_t* Phobos::UI::SWShotsFormat = L""; -const wchar_t* Phobos::UI::BattlePoints_Label = L""; -const wchar_t* Phobos::UI::BattlePointsSidebar_Label = L""; -bool Phobos::UI::BattlePointsSidebar_Label_InvertPosition = false; char Phobos::UI::ShowBriefingResumeButtonStatusLabel[32]; bool Phobos::UI::PowerDelta_Show = false; double Phobos::UI::PowerDelta_ConditionYellow = 0.75; @@ -170,9 +167,6 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) ini_uimd.ReadString(GameStrings::ToolTips, "SWShotsFormat", NONE_STR, Phobos::readBuffer); Phobos::UI::SWShotsFormat = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"Shots: %d"); // ⌚ - - ini_uimd.ReadString(GameStrings::ToolTips, "BattlePoints.Label", NONE_STR, Phobos::readBuffer); - Phobos::UI::BattlePoints_Label = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"\u2605: "); // ★: } // Sidebar @@ -240,11 +234,6 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::UI::SuperWeaponSidebar_MaxColumns = ini_uimd.ReadInteger(SIDEBAR_SECTION, "SuperWeaponSidebar.MaxColumns", Phobos::UI::SuperWeaponSidebar_MaxColumns); - Phobos::UI::BattlePointsSidebar_Label_InvertPosition = ini_uimd.ReadBool(SIDEBAR_SECTION, "BattlePointsSidebar.Label.InvertPosition", false); - - ini_uimd.ReadString(SIDEBAR_SECTION, "BattlePointsSidebar.Label", NONE_STR, Phobos::readBuffer); - Phobos::UI::BattlePointsSidebar_Label = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L"\u2605"); // %d ★ - Phobos::UI::CreditsIndicator_MaxStep = ini_uimd.ReadInteger(SIDEBAR_SECTION, "CreditsIndicator.MaxStep", Phobos::UI::CreditsIndicator_MaxStep); diff --git a/src/Phobos.h b/src/Phobos.h index db960a2bef..e6bef45c31 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -75,9 +75,6 @@ class Phobos static const wchar_t* ShowBriefingResumeButtonLabel; static const wchar_t* SWShotsFormat; static char ShowBriefingResumeButtonStatusLabel[0x20]; - static const wchar_t* BattlePoints_Label; - static const wchar_t* BattlePointsSidebar_Label; - static bool BattlePointsSidebar_Label_InvertPosition; }; class Config diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h index 98430c02e5..948e7124c9 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -453,3 +453,27 @@ enum class AdditionalAbility : unsigned char }; constexpr size_t AdditionalAbilityCount = static_cast(AdditionalAbility::Count); + +enum class ResourceDisplayCondition : unsigned char +{ + Always = 0, + GreaterThanZero = 1, + HasCollector = 2, + Never = 3 +}; + +enum class ResourceDisplayOrientation : unsigned char +{ + Vertical = 0, + Horizontal = 1 +}; + +enum class ResourceDisplayAnchor : unsigned char +{ + TopRight = 0, + TopLeft = 1, + BottomRight = 2, + BottomLeft = 3, + Sidebar = 4 +}; + diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index c9592a410f..a3ca2875cb 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -1118,6 +1118,94 @@ namespace detail return value.Read(parser, pSection, pKey); } + template <> + inline bool read(ResourceDisplayCondition& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + if (_strcmpi(str, "always") == 0) + { + value = ResourceDisplayCondition::Always; + return true; + } + else if (_strcmpi(str, "greaterthanzero") == 0 || _strcmpi(str, "positive") == 0 || _strcmpi(str, ">0") == 0) + { + value = ResourceDisplayCondition::GreaterThanZero; + return true; + } + else if (_strcmpi(str, "hascollector") == 0 || _strcmpi(str, "collector") == 0) + { + value = ResourceDisplayCondition::HasCollector; + return true; + } + else if (_strcmpi(str, "never") == 0 || _strcmpi(str, "none") == 0 || _strcmpi(str, "hidden") == 0 || _strcmpi(str, "no") == 0 || _strcmpi(str, "false") == 0) + { + value = ResourceDisplayCondition::Never; + return true; + } + Debug::INIParseFailed(pSection, pKey, parser.value(), "Resource display condition can be Always, GreaterThanZero, HasCollector or Never/None/Hidden"); + } + return false; + } + + template <> + inline bool read(ResourceDisplayOrientation& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + if (_strcmpi(str, "vertical") == 0) + { + value = ResourceDisplayOrientation::Vertical; + return true; + } + else if (_strcmpi(str, "horizontal") == 0) + { + value = ResourceDisplayOrientation::Horizontal; + return true; + } + Debug::INIParseFailed(pSection, pKey, parser.value(), "Resource display orientation can be Vertical or Horizontal"); + } + return false; + } + + template <> + inline bool read(ResourceDisplayAnchor& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + if (_strcmpi(str, "topright") == 0) + { + value = ResourceDisplayAnchor::TopRight; + return true; + } + else if (_strcmpi(str, "topleft") == 0) + { + value = ResourceDisplayAnchor::TopLeft; + return true; + } + else if (_strcmpi(str, "bottomright") == 0) + { + value = ResourceDisplayAnchor::BottomRight; + return true; + } + else if (_strcmpi(str, "bottomleft") == 0) + { + value = ResourceDisplayAnchor::BottomLeft; + return true; + } + else if (_strcmpi(str, "sidebar") == 0) + { + value = ResourceDisplayAnchor::Sidebar; + return true; + } + Debug::INIParseFailed(pSection, pKey, parser.value(), "Resource display anchor can be TopRight, TopLeft, BottomRight, BottomLeft or Sidebar"); + } + return false; + } + template <> inline bool read(IronCurtainEffect& value, INI_EX& parser, const char* pSection, const char* pKey) { From 0a0560c4e4c9e96051079211ac92673d636c3471 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 26 Aug 2026 19:24:21 +0200 Subject: [PATCH 15/19] Repair stuff & production --- src/Ext/Building/Body.h | 1 + src/Ext/Building/Hooks.Production.cpp | 118 ++++++++++++++++++ src/Ext/Building/Hooks.cpp | 168 +++++++++++++++++++++++++- src/Ext/House/Body.cpp | 44 +++++++ src/Ext/House/Body.h | 27 +++++ src/Ext/House/Hooks.cpp | 104 +--------------- src/Ext/Techno/Body.Update.cpp | 1 + src/Misc/Hooks.AlphaImage.cpp | 24 +++- 8 files changed, 377 insertions(+), 110 deletions(-) diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index 2dfd71c9d7..b650ebb423 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -48,6 +48,7 @@ class BuildingExt final : public TechnoExt, public Detach::Listener +#include +#include + +DEFINE_HOOK(0x4C9BD5, FactoryClass_AI_ProcessProductionStep, 0x31) +{ + GET(FactoryClass*, pFactory, ESI); + GET(int, moneyCost, EDI); + + const auto pOwner = pFactory->Owner; + if (pOwner) + { + const int availableMoney = pOwner->Available_Money(); + bool canAfford = (moneyCost <= availableMoney); + + TechnoTypeClass* pType = pFactory->Object ? pFactory->Object->GetTechnoType() : (pFactory->QueuedObjects.Count > 0 ? pFactory->QueuedObjects[0] : nullptr); + const auto pTypeExt = TechnoTypeExt::TryFetch(pType); + const auto pHouseExt = HouseExt::TryFetch(pOwner); + + std::vector resDues; + if (pTypeExt && pHouseExt && !pTypeExt->ResourceCosts.empty()) + { + const int stage = pFactory->Production.Value; + const int stepsRemaining = 54 - stage; + resDues.resize(pTypeExt->ResourceCosts.size(), 0); + + for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) + { + const int totalCost = pTypeExt->ResourceCosts[i]; + if (totalCost > 0) + { + const int spent = pHouseExt->GetFactoryResourceSpent(pFactory, i); + const int remaining = totalCost - spent; + int resCost = (stage >= 54) ? remaining : (remaining / (stepsRemaining > 0 ? stepsRemaining : 1)); + resCost = std::clamp(resCost, 0, remaining); + + if (resCost > 0) + { + resDues[i] = resCost; + if (!pHouseExt->CanAffordResource(static_cast(i), resCost)) + { + canAfford = false; + } + } + } + } + } + + if (canAfford) + { + // Deduct money + if (moneyCost > 0) + { + pOwner->TakeMoney(moneyCost); + } + pFactory->Balance -= moneyCost; + pFactory->OnHold = false; + + // Deduct custom resources + if (pHouseExt && !resDues.empty()) + { + for (size_t i = 0; i < resDues.size(); ++i) + { + if (resDues[i] > 0) + { + pHouseExt->UpdateResourceAmount(static_cast(i), -resDues[i]); + pHouseExt->AddFactoryResourceSpent(pFactory, i, resDues[i]); + } + } + } + } + else + { + // Insufficient money or resources: put on hold and revert step + pFactory->OnHold = true; + pFactory->Production.Value = std::max(0, pFactory->Production.Value - 1); + } + } + + return 0x4C9C06; +} + +DEFINE_HOOK(0x4C9C28, FactoryClass_AI_FinishProduction, 0x13) +{ + GET(FactoryClass*, pFactory, ESI); + + if (pFactory->Owner) + { + const int remainingMoney = pFactory->Balance; + if (remainingMoney > 0) + { + pFactory->Owner->TakeMoney(remainingMoney); + } + pFactory->Balance = 0; + + if (const auto pHouseExt = HouseExt::TryFetch(pFactory->Owner)) + { + pHouseExt->ClearFactoryResourceState(pFactory); + } + } + + return 0x4C9C3B; +} DEFINE_HOOK(0x4401BB, BuildingClass_AI_PickWithFreeDocks, 0x6) { @@ -129,6 +231,22 @@ DEFINE_HOOK(0x4CA07A, FactoryClass_AbandonProduction_Phobos, 0x8) GET(FactoryClass*, pFactory, ESI); GET_STACK(DWORD const, calledby, 0x18); + if (pFactory && pFactory->Owner) + { + if (const auto pHouseExt = HouseExt::TryFetch(pFactory->Owner)) + { + for (size_t i = 0; i < ResourceTypeClass::Array.size(); ++i) + { + const int spent = pHouseExt->GetFactoryResourceSpent(pFactory, i); + if (spent > 0) + { + pHouseExt->UpdateResourceAmount(static_cast(i), spent); + } + } + pHouseExt->ClearFactoryResourceState(pFactory); + } + } + auto const pTechno = pFactory->Object; if (calledby < 0x7F0000) // Replace the old log with this to figure out where keeps flushing the stream diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 5deaed38ce..0ef935cdb9 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1,5 +1,6 @@ #include "Body.h" +#include #include #include #include @@ -391,13 +392,17 @@ DEFINE_HOOK(0x4511D6, BuildingClass_AnimationAI_SellBuildup, 0x7) DEFINE_HOOK(0x441553, BuildingClass_Unlimbo_AddOwned, 0x6) { GET(BuildingClass*, pThis, ESI); - const auto pTypeExt = BuildingTypeExt::Fetch(pThis->Type); - const auto pOwnerExt = HouseExt::Fetch(pThis->Owner); - const auto pBuildingExt = BuildingExt::Fetch(pThis); + if (!pThis) + return 0; - pBuildingExt->InitializeResourceProductions(); + const auto pTypeExt = BuildingTypeExt::TryFetch(pThis->Type); + const auto pOwnerExt = HouseExt::TryFetch(pThis->Owner); + const auto pBuildingExt = BuildingExt::TryFetch(pThis); - if (!pTypeExt->PowerPlantEnhancer_Buildings.empty() && (pTypeExt->PowerPlantEnhancer_Amount != 0 || pTypeExt->PowerPlantEnhancer_Factor != 1.0f)) + if (pBuildingExt) + pBuildingExt->InitializeResourceProductions(); + + if (pTypeExt && pOwnerExt && !pTypeExt->PowerPlantEnhancer_Buildings.empty() && (pTypeExt->PowerPlantEnhancer_Amount != 0 || pTypeExt->PowerPlantEnhancer_Factor != 1.0f)) pOwnerExt->PowerPlantEnhancers.push_back(pThis); return 0; @@ -1405,3 +1410,156 @@ DEFINE_HOOK(0x6F6D9E, TechnoClass_Unlimbo_BuildingStartFacing, 0x7) } #pragma endregion + +#pragma region Repair + +DEFINE_HOOK(0x450885, BuildingClass_Repair_CheckAndSpend, 0x23) +{ + enum { CanRepair = 0x4508A8, StopRepair = 0x4509B4 }; + + GET(BuildingClass*, pThis, ESI); + GET(int, moneyStepCost, EDI); + GET(int, repairHP, EBP); + + const auto pOwner = pThis->Owner; + if (!pOwner) + return StopRepair; + + const int availableMoney = pOwner->Available_Money(); + if (moneyStepCost > availableMoney) + return StopRepair; + + const auto pType = pThis->Type; + const auto pTypeExt = TechnoTypeExt::TryFetch(pType); + const auto pHouseExt = HouseExt::TryFetch(pOwner); + + std::vector> resDues; + if (pTypeExt && pHouseExt && !pTypeExt->ResourceCosts.empty() && pType && pType->Strength > 0 && repairHP > 0) + { + const double repairFactor = (RulesClass::Instance && RulesClass::Instance->RepairPercent > 0.0) + ? RulesClass::Instance->RepairPercent + : 0.5; + + for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) + { + const int totalResCost = pTypeExt->ResourceCosts[i]; + if (totalResCost > 0) + { + const double stepCostExact = (static_cast(totalResCost) * repairFactor * repairHP) / static_cast(pType->Strength); + const int resCost = std::max(1, static_cast(std::round(stepCostExact))); + + if (!pHouseExt->CanAffordResource(static_cast(i), resCost)) + { + return StopRepair; + } + + resDues.push_back(std::make_pair(i, resCost)); + } + } + } + + // Afforded - Deduct money + if (moneyStepCost > 0) + { + pOwner->TakeMoney(moneyStepCost); + } + + // Deduct custom resources + if (pHouseExt && !resDues.empty()) + { + for (const auto& due : resDues) + { + pHouseExt->UpdateResourceAmount(static_cast(due.first), -due.second); + } + } + + return CanRepair; +} + +static void DeductUnitRepairCustomResources(BuildingClass* pDepot) +{ + if (!pDepot || !pDepot->Owner) + return; + + const auto pOwner = pDepot->Owner; + const auto pHouseExt = HouseExt::TryFetch(pOwner); + if (!pHouseExt) + return; + + const signed char unitIdx = *reinterpret_cast(reinterpret_cast(pDepot) + 0x702); + if (unitIdx <= 0) + return; + + const auto pUnit = *reinterpret_cast(reinterpret_cast(pDepot) + 0x5E8 + unitIdx * 4); + if (!pUnit) + return; + + const auto pType = pUnit->GetTechnoType(); + if (!pType || pType->Strength <= 0) + return; + + const auto pTypeExt = TechnoTypeExt::TryFetch(pType); + if (!pTypeExt || pTypeExt->ResourceCosts.empty()) + return; + + const int repairHP = pType->GetRepairStep(); + if (repairHP <= 0) + return; + + const double repairFactor = (RulesClass::Instance && RulesClass::Instance->RepairPercent > 0.0) + ? RulesClass::Instance->RepairPercent + : 0.5; + + for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) + { + const int totalResCost = pTypeExt->ResourceCosts[i]; + if (totalResCost > 0) + { + const double stepCostExact = (static_cast(totalResCost) * repairFactor * repairHP) / static_cast(pType->Strength); + const int resCost = std::max(1, static_cast(std::round(stepCostExact))); + + if (pHouseExt->CanAffordResource(static_cast(i), resCost)) + { + pHouseExt->UpdateResourceAmount(static_cast(i), -resCost); + } + } + } +} + +DEFINE_HOOK(0x4575E4, BuildingClass_ServiceDepot_TakeMoneyAndResources, 0x5) +{ + GET(BuildingClass*, pDepot, ESI); + GET(int, moneyCost, EAX); + + if (pDepot && pDepot->Owner) + { + if (moneyCost > 0) + { + pDepot->Owner->TakeMoney(moneyCost); + } + + DeductUnitRepairCustomResources(pDepot); + } + + return 0x4575E9; +} + +DEFINE_HOOK(0x44AAEF, BuildingClass_MissionRepair_TakeMoneyAndResources, 0x5) +{ + GET(BuildingClass*, pDepot, EBP); + GET(int, moneyCost, EAX); + + if (pDepot && pDepot->Owner) + { + if (moneyCost > 0) + { + pDepot->Owner->TakeMoney(moneyCost); + } + + DeductUnitRepairCustomResources(pDepot); + } + + return 0x44AAF4; +} + +#pragma endregion diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 686cffa32e..a144e98ffe 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include //Static init @@ -717,6 +718,7 @@ void HouseExt::Serialize(T& Stm) .Process(this->FreeRadar) .Process(this->ForceRadar) .Process(this->PlayerAutoRepair) + .Process(this->FactoryResourceStates) //.Process(this->BeaconsPlacedOrder) beacon is not saved, so this follows it. ; } @@ -1321,6 +1323,48 @@ void HouseExt::InitializeCustomResources() } } +int HouseExt::GetFactoryResourceSpent(FactoryClass* pFactory, size_t resIdx) const +{ + for (const auto& s : this->FactoryResourceStates) + { + if (s.pFactory == pFactory && resIdx < s.Spent.size()) + return s.Spent[resIdx]; + } + return 0; +} + +void HouseExt::AddFactoryResourceSpent(FactoryClass* pFactory, size_t resIdx, int amount) +{ + for (auto& s : this->FactoryResourceStates) + { + if (s.pFactory == pFactory) + { + if (s.Spent.size() <= resIdx) + s.Spent.resize(resIdx + 1, 0); + s.Spent[resIdx] += amount; + return; + } + } + + FactoryResourceState newState; + newState.pFactory = pFactory; + newState.Spent.resize(resIdx + 1, 0); + newState.Spent[resIdx] = amount; + const_cast(this)->FactoryResourceStates.push_back(newState); +} + +void HouseExt::ClearFactoryResourceState(FactoryClass* pFactory) +{ + for (auto it = this->FactoryResourceStates.begin(); it != this->FactoryResourceStates.end(); ++it) + { + if (it->pFactory == pFactory) + { + this->FactoryResourceStates.erase(it); + return; + } + } +} + // ============================= // container diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index befc2af7cd..a9375bef40 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -9,6 +9,28 @@ #include +class FactoryClass; +class TechnoTypeClass; + +struct FactoryResourceState +{ + FactoryClass* pFactory { nullptr }; + TechnoTypeClass* pType { nullptr }; + int LastStep { 0 }; + std::vector Spent {}; + + template + void Serialize(T& Stm) + { + Stm + .Process(this->pFactory) + .Process(this->pType) + .Process(this->LastStep) + .Process(this->Spent) + ; + } +}; + class HouseExt final : public AbstractExt, public Detach::Listener { public: @@ -83,6 +105,7 @@ class HouseExt final : public AbstractExt, public Detach::Listener BeaconsPlacedOrder; + std::vector FactoryResourceStates; HouseExt(HouseClass* OwnerObject) : AbstractExt(OwnerObject) , PowerPlantEnhancers {} @@ -121,6 +144,7 @@ class HouseExt final : public AbstractExt, public Detach::ListenerResourceCosts.size(); ++i) - { - const int cost = pTypeExt->ResourceCosts[i]; - if (cost > 0 && !pHouseExt->CanAffordResource(static_cast(i), cost)) - { - R->AL(0); - return 0x4FD605; - } - } - } - } - } - - return 0; -} - -DEFINE_HOOK(0x4FA4B0, HouseClass_BeginProduction_CheckAndDeductResources, 0x6) -{ - GET(HouseClass*, pHouse, ECX); - GET_STACK(AbstractType, absType, 0x4); - GET_STACK(int, index, 0x8); - GET_STACK(int, count, 0xC); - - if (pHouse && count > 0) - { - if (const auto pType = TechnoTypeClass::GetByTypeAndIndex(absType, index)) - { - if (const auto pTypeExt = TechnoTypeExt::TryFetch(pType)) - { - if (const auto pHouseExt = HouseExt::TryFetch(pHouse)) - { - for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) - { - const int cost = pTypeExt->ResourceCosts[i]; - if (cost > 0 && !pHouseExt->CanAffordResource(static_cast(i), cost * count)) - { - R->AL(0); - return 0x4FA66C; - } - } - - for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) - { - const int cost = pTypeExt->ResourceCosts[i]; - if (cost > 0) - { - pHouseExt->UpdateResourceAmount(static_cast(i), -(cost * count)); - } - } - } - } - } - } - - return 0; -} - -DEFINE_HOOK(0x4FAA20, HouseClass_AbandonProduction_RefundResources, 0x6) -{ - GET(HouseClass*, pHouse, ECX); - GET_STACK(AbstractType, absType, 0x4); - GET_STACK(int, index, 0x8); - GET_STACK(int, count, 0xC); - - if (pHouse) - { - if (const auto pType = TechnoTypeClass::GetByTypeAndIndex(absType, index)) - { - if (const auto pTypeExt = TechnoTypeExt::TryFetch(pType)) - { - if (const auto pHouseExt = HouseExt::TryFetch(pHouse)) - { - const int refundCount = (count > 0) ? count : 1; - for (size_t i = 0; i < pTypeExt->ResourceCosts.size(); ++i) - { - const int cost = pTypeExt->ResourceCosts[i]; - if (cost > 0) - { - pHouseExt->UpdateResourceAmount(static_cast(i), cost * refundCount); - } - } - } - } - } - } - - return 0; -} - #pragma endregion + diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 15d84bd349..9042a21e17 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include diff --git a/src/Misc/Hooks.AlphaImage.cpp b/src/Misc/Hooks.AlphaImage.cpp index ff8b6795e2..a9136b3d06 100644 --- a/src/Misc/Hooks.AlphaImage.cpp +++ b/src/Misc/Hooks.AlphaImage.cpp @@ -5,10 +5,30 @@ DEFINE_PATCH(0x5F3E70, 0x83, 0xEC, 0x10, 0x55, 0x56) // Disbale Ares::ObjectClas static void __fastcall UpdateAlphaShape(ObjectClass* pSource) { - if (!pSource || !pSource->IsAlive) + if (!pSource || !*(void**)pSource || pSource->UniqueID <= 0 || !pSource->IsAlive) return; - const auto pSourceType = pSource->GetType(); + ObjectTypeClass* pSourceType = nullptr; + switch (pSource->WhatAmI()) + { + case AbstractType::Building: + pSourceType = static_cast(pSource)->Type; + break; + case AbstractType::Unit: + pSourceType = static_cast(pSource)->Type; + break; + case AbstractType::Infantry: + pSourceType = static_cast(pSource)->Type; + break; + case AbstractType::Aircraft: + pSourceType = static_cast(pSource)->Type; + break; + case AbstractType::Anim: + pSourceType = static_cast(pSource)->Type; + break; + default: + return; + } if (!pSourceType) return; From 6a356fb4017db2d4539e1407154bdcec7fde76eb Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 27 Aug 2026 09:59:12 +0200 Subject: [PATCH 16/19] more tags, money & power supported --- docs/New-or-Enhanced-Logics.md | 194 +++++----- scratch/disasm_credits.py | 35 ++ src/Ext/Building/Hooks.Production.cpp | 15 +- src/Ext/Building/Hooks.cpp | 13 + src/Ext/House/Body.cpp | 41 +++ src/Ext/TAction/Body.cpp | 4 + src/Ext/TechnoType/Body.cpp | 57 +-- src/Misc/Hooks.Crates.cpp | 78 ++++ src/Misc/Hooks.UI.cpp | 130 ++----- src/Misc/PhobosToolTip.cpp | 111 ++++-- src/New/Type/ResourceTypeClass.cpp | 489 +++++++++++++++++++++++++- src/New/Type/ResourceTypeClass.h | 22 +- src/Phobos.cpp | 3 + src/Utilities/Enum.h | 14 +- src/Utilities/TemplateDef.h | 48 ++- 15 files changed, 990 insertions(+), 264 deletions(-) create mode 100644 scratch/disasm_credits.py diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index b0b39d1353..cb48380b20 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -282,95 +282,123 @@ Similarly, unless you really have a need, `UseGlobalRadApplicationDelay` should ### Custom Resource Types -- Now you can define and customize arbitrary secondary/tertiary resources with their own HUD counters, production cycles, unit costs, bounties, and superweapon requirements. - - `[ResourceTypes]` defines the list of custom resource types available in the game. - - `[AudioVisual] -> Display.ResourceTypes.Orientation` controls whether multiple resource counters stack vertically or horizontally on the HUD (`Vertical` / `Horizontal`). - - `[AudioVisual] -> Display.ResourceTypes.Anchor` sets the reference HUD anchor corner (`TopRight`, `TopLeft`, `BottomRight`, `BottomLeft`, `Sidebar`). - - `[AudioVisual] -> Display.ResourceTypes.BaseOffset` provides an initial X,Y pixel offset for the resource display group. - - `[AudioVisual] -> Display.ResourceTypes.Spacing` specifies the spacing in pixels between consecutive resource counters. - - `[] -> Display.Label` specifies the CSF label or text symbol displayed alongside the counter. - - `[] -> Display.Label.InvertPosition` when set to true, places the label after the value (`100 ★` vs `★ 100`). - - `[] -> Display.Label.UseSpace` when set to false, omits the separating space between label and amount (`100★` or `★100` vs `100 ★` / `★ 100`). Defaults to `true`. - - `[] -> Display.Color` specifies the text color (R,G,B) for the resource on the HUD and floating strings. - - `[] -> Display.Condition` specifies when the counter is visible (`Always`, `GreaterThanZero`, `HasCollector`, `Never` / `Hidden` / `None`). - - `[] -> Display.Offset` optional explicit X,Y screen coordinates overriding auto-stacking. - - `[] -> InitialValue` starting resource amount granted to houses at match start. - - `[] -> RequiresCollector` when true, the house resource only activates once a structure with `ResourceCollector.=true` is built. - - `[] -> Bounty.Enabled` enables kill bounty rewards for this resource. - - `[] -> Bounty.DefaultValue` default bounty granted when destroying an enemy unit that lacks a specific bounty tag. - - `[] -> Bounty.DefaultFriendlyValue` bounty when destroying an allied unit. - - `[] -> Bounty.CanUseStandardPoints` fall back to the techno's `Points=` tag for bounty calculation. - - `[] -> Bounty.MoneyConversion` divides the destroyed unit's `Cost=` by this value to determine the bounty reward when not explicitly defined on the techno (e.g. 100 calculates 700 / 100 = 7). Falls back to `Bounty.DefaultValue` if result is 0. - - `[] -> Cost.` additional resource cost required to build the techno. - - `[] -> Bounty..Value` custom bounty awarded to the killer when this enemy unit is destroyed. - - `[] -> Bounty..FriendlyValue` custom friendly fire penalty applied if destroyed by an ally (e.g. -50; always evaluated as negative to prevent accidental rewards). - - `[] -> Production.`, `Production..Delay`, and `Production..Startup` configure periodic and initial resource generation. Works on all units, vehicles, infantry, aircraft, and buildings. `Startup` is optional (defaults to 0). Periodic production occurs every `Delay` frames. - - `[] -> Production..Display.Offset` optional X,Y pixel offset for the floating string text relative to the object render center. - - `[] -> Production..Display.Color` optional R,G,B text color override for the floating string (falls back to `Display.Color` from `[]`). - - `[] -> Production..Display.Houses` controls which players can see the floating string (`All`, `Owner`, `Allies`, `Enemies`, `None`, defaults to `All`). - - `[] -> ResourceCollector.` designates this unit or building as an active collector for this resource. - - `[] -> ResourceType..Amount` specifies resource cost (positive) or grant (negative) upon firing. - - `[] -> ResourceType` and `ResourceValue` configure Tiberium/Ore harvesting to yield custom resources. When `ResourceValue=` is omitted, `Value=` is used for the custom resource and no money is granted (replaces money). When `ResourceValue=` is defined, `ResourceValue` is used for the custom resource AND `Value=` is used to grant standard money (dual income). - - `[] -> Sidebar.ResourceTypes.Types` list of resources to show on this side's sidebar (if omitted, all non-hidden resources are shown). - - `[] -> Sidebar.ResourceTypes.Offset`, `Sidebar.ResourceTypes.Color`, and `Sidebar.ResourceTypes.Align` provide per-side HUD positioning and text styling fallbacks. +- Now you can define and customize arbitrary secondary/tertiary resources with their own HUD counters, production cycles, unit costs, bounties, crates, and superweapon requirements. + - `Display.ResourceTypes.Orientation` controls whether multiple resource counters stack vertically or horizontally on the HUD (`Vertical` / `Horizontal`). + - `Display.ResourceTypes.Anchor` sets the reference HUD anchor corner (`TopRight`, `TopLeft`, `TopCenter`, `BottomRight`, `BottomLeft`, `BottomCenter`, `Sidebar`). + - `Display.ResourceTypes.BaseOffset` provides an initial X,Y pixel offset for the resource display group. + - `Display.ResourceTypes.Spacing` specifies the spacing in pixels between consecutive resource counters. + - `Display.ResourceTypes.Align` text alignment for HUD counters (`Left`, `Center`, `Right`; defaults to `Right` for right anchors, `Center` for center anchors, `Left` for left anchors). + - `Display.ResourceTypes.BackgroundPCX` optional PCX background image rendered behind the tactical HUD counters (takes precedence over SHP if both are defined). + - `Display.ResourceTypes.Background` optional SHP background image rendered behind the tactical HUD counters (without `.shp` extension; multi-frame SHPs animate continuously). + - `Display.ResourceTypes.Background.Offset` optional X,Y pixel offset for the background image (applies to both PCX and SHP). + - `Display.ResourceTypes.Background.Palette` optional palette for SHP background. + - `Display.Label` specifies the CSF label key displayed alongside the counter. + - `Display.Label.InvertPosition` when set to true, places the label after the value (`100 ★` vs `★ 100`). + - `Display.Label.UseSpace` when set to false, omits the separating space between label and amount (`100★` or `★100` vs `100 ★` / `★ 100`). Defaults to `true`. + - `Display.Color` specifies the text color (R,G,B) for the resource on the HUD and floating strings. + - `Display.Condition` specifies when the counter is visible (`Always`, `GreaterThanZero`, `HasCollector`, `Never` / `Hidden` / `None`). + - `Display.Offset` optional explicit X,Y screen coordinates overriding auto-stacking. + - `InitialValue` starting resource amount granted to houses at match start. + - `RequiresCollector` when true, the house resource only activates once a structure with `ResourceCollector.=true` is built. + - `Bounty.Enabled` enables kill bounty rewards for this resource. + - `Bounty.DefaultValue` default bounty granted when destroying an enemy unit that lacks a specific bounty tag. + - `Bounty.DefaultFriendlyValue` bounty when destroying an allied unit. + - `Bounty.CanUseStandardPoints` fall back to the techno's `Points=` tag for bounty calculation. + - `Bounty.MoneyConversion` divides the destroyed unit's `Cost=` by this value to determine the bounty reward when not explicitly defined on the techno (e.g. 100 calculates 700 / 100 = 7). Falls back to `Bounty.DefaultValue` if result is 0. + - `Crate.Amount` amount granted when rolled from a money crate (if > 0, the resource enters the money crate raffle with equal probability alongside standard credits). + - `Crate.Anim` optional animation played at the crate location when this resource is awarded. + - `Crate.Sound` optional sound effect played when this resource is awarded. + - `Cost.` additional resource cost required to build the techno. Consumed progressively during production (pausing if funds are insufficient and fully refunded on cancellation), and consumed proportionally during structure repair and Service Depot unit repair. + - `Bounty..Value` custom bounty awarded to the killer when this enemy unit is destroyed. + - `Bounty..FriendlyValue` custom friendly fire penalty applied if destroyed by an ally (e.g. -50; always evaluated as negative to prevent accidental rewards). + - `Production.`, `Production..Delay`, and `Production..Startup` configure periodic and initial resource generation. Works on all units, vehicles, infantry, aircraft, and buildings. `Startup` is optional (defaults to 0). Periodic production occurs every `Delay` frames. + - `Production..Display.Offset` optional X,Y pixel offset for the floating string text relative to the object render center. + - `Production..Display.Color` optional R,G,B text color override for the floating string (falls back to `Display.Color` from `[]`). + - `Production..Display.Houses` controls which players can see the floating string (`All`, `Owner`, `Allies`, `Enemies`, `None`, defaults to `All`). + - `ResourceCollector.` designates this unit or building as an active collector for this resource. + - `ResourceType..Amount` specifies resource cost (positive) or grant (negative) upon firing. + - `ResourceType` and `ResourceValue` configure Tiberium/Ore harvesting to yield custom resources. When `ResourceValue=` is omitted, `Value=` is used for the custom resource and no money is granted (replaces money). When `ResourceValue=` is defined, `ResourceValue` is used for the custom resource AND `Value=` is used to grant standard money (dual income). + - `Sidebar.ResourceTypes.Types` list of resources to show on this side's sidebar (if omitted, all non-hidden resources are shown). +- `[ResourceTypes]` also supports **`Money`** and **`Power`** pseudo-resources. Simply declare them in the list (e.g. `0=Money`, `1=Power`) to customize their HUD display (labels, positioning, formatting, or moving them out of the sidebar to the tactical screen). + - When `Money` is declared, it automatically tracks player credits and replaces the default sidebar counter with the Resource HUD. It also supports `Production.Money` and `ResourceCollector.Money`. + - `Display.Power.Format` controls the format of the displayed power value (`NetAndTotal`, `Net`, `DrainAndTotal`, `Drain`, `Total`). In `rulesmd.ini`: ```ini [ResourceTypes] -0=BattlePoints -1=OreAlloy +0=SOMERESOURCETYPE [AudioVisual] -; Global HUD alignment and stacking for custom resources -Display.ResourceTypes.Orientation=Vertical ; Vertical, Horizontal -Display.ResourceTypes.Anchor=TopRight ; TopRight, TopLeft, BottomRight, BottomLeft, Sidebar -Display.ResourceTypes.BaseOffset=0,0 ; X,Y pixel offset -Display.ResourceTypes.Spacing=14 ; integer, pixels between stacked counters - -[BattlePoints] ; ResourceTypeClass -Display.Label=TXT_BP_SYMBOL ; CSF entry key (e.g. "★") -Display.Label.InvertPosition=false ; boolean -Display.Label.UseSpace=true ; boolean (false = "★100", true = "★ 100") -Display.Color=255,215,0 ; R,G,B ColorStruct -Display.Condition=GreaterThanZero ; Always, GreaterThanZero, HasCollector, Never -Display.Offset= ; X,Y pixels (optional, overrides auto-stacking) -InitialValue=0 ; integer -RequiresCollector=no ; boolean -Bounty.Enabled=yes ; boolean -Bounty.DefaultValue=1 ; integer -Bounty.DefaultFriendlyValue=-10 ; integer -Bounty.CanUseStandardPoints=no ; boolean -Bounty.MoneyConversion=100 ; integer (calculates Cost / 100) - -[SOMETECHNO] ; Works on InfantryType, UnitType, AircraftType, BuildingType -Cost.BattlePoints=50 ; integer -Bounty.BattlePoints.Value=15 ; integer -Bounty.BattlePoints.FriendlyValue=-50; integer (optional special penalty for friendly fire) -ResourceCollector.BattlePoints=yes ; boolean (enables resource for house if RequiresCollector=yes) -Production.BattlePoints=10 ; integer (periodic amount generated) -Production.BattlePoints.Delay=450 ; integer (frames between generation) -Production.BattlePoints.Startup=25 ; integer (optional one-time grant upon creation) -Production.BattlePoints.Display.Offset=0,-20 ; X,Y pixels offset for floating text (optional) -Production.BattlePoints.Display.Color=255,215,0 ; R,G,B color override for floating text (optional) -Production.BattlePoints.Display.Houses=All ; All, Owner, Allies, Enemies, None (optional) - -[Cruentus] ; TiberiumType -; Example 1: Replace money entirely with BattlePoints (uses Value= for points) -ResourceType=BattlePoints - -; Example 2: Dual income (gives $50 money via Value= AND 15 points via ResourceValue=) -; Value=50 -; ResourceType=BattlePoints -; ResourceValue=15 - -[SOMESW] ; SuperWeaponType -ResourceType.BattlePoints.Amount=100 ; integer (>0 required to launch, <0 grants on fire) - -[SOMESIDE] ; SideClass -Sidebar.ResourceTypes.Types=BattlePoints ; List of ResourceTypes to show on this side's sidebar -Sidebar.ResourceTypes.Offset=0,0 ; X,Y -Sidebar.ResourceTypes.Color= ; R,G,B -Sidebar.ResourceTypes.Align=Left ; Left, Right, Center/Centre +Display.ResourceTypes.Orientation=Vertical ; ResourceDisplayOrientation (Vertical | Horizontal) +Display.ResourceTypes.Anchor=TopRight ; ResourceDisplayAnchor (TopRight | TopLeft | TopCenter | BottomRight | BottomLeft | BottomCenter | Sidebar) +Display.ResourceTypes.BaseOffset=0,0 ; Point2D - X,Y +Display.ResourceTypes.Spacing=14 ; integer - pixels +Display.ResourceTypes.Align= ; TextAlign (Left | Center | Right, default depends on Anchor) +Display.ResourceTypes.BackgroundPCX= ; filename - .pcx +Display.ResourceTypes.Background= ; filename - without .shp extension +Display.ResourceTypes.Background.Offset=0,0 ; Point2D - X,Y (applies to both PCX and SHP) +Display.ResourceTypes.Background.Palette= ; CustomPalette (palette for SHP background) + +[SOMERESOURCETYPE] ; ResourceType +Display.Label= ; CSF entry key +Display.Label.InvertPosition=false ; boolean +Display.Label.UseSpace=true ; boolean +Display.Color=255,255,255 ; integer - Red,Green,Blue +Display.Condition=Always ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) +Display.Offset= ; Point2D - X,Y +InitialValue=0 ; integer +RequiresCollector=false ; boolean +Bounty.Enabled=false ; boolean +Bounty.DefaultValue=0 ; integer +Bounty.DefaultFriendlyValue=0 ; integer +Bounty.CanUseStandardPoints=false ; boolean +Bounty.MoneyConversion=0 ; integer +Crate.Amount=0 ; integer +Crate.Anim= ; AnimationType +Crate.Sound= ; Sound + +[Money] ; Money Pseudo-Resource (tracks player credits) +Display.Label= ; CSF entry key, default to [ToolTips] -> CostLabel from uimd.ini ($) +Display.Label.InvertPosition=false ; boolean +Display.Label.UseSpace=false ; boolean +Display.Color=255,255,0 ; integer - Red,Green,Blue +Display.Condition=Always ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) +Display.Offset= ; Point2D - X,Y +RequiresCollector=false ; boolean + +[Power] ; Power Pseudo-Resource (tracks power status) +Display.Label= ; CSF entry key, default to [ToolTips] -> PowerLabel from uimd.ini (⚡) +Display.Label.InvertPosition=false ; boolean +Display.Label.UseSpace=false ; boolean +Display.Color= ; integer - Red,Green,Blue (uses dynamic grid colors if omitted) +Display.Condition=Never ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) +Display.Offset= ; Point2D - X,Y +Display.Power.Format=NetAndTotal ; ResourcePowerDisplayMode (NetAndTotal | Net | DrainAndTotal | Drain | Total) + +[SOMETECHNO] ; TechnoType +Cost.=0 ; integer +Bounty..Value= ; integer, default to [SOMERESOURCETYPE] -> Bounty.DefaultValue +Bounty..FriendlyValue= ; integer, default to [SOMERESOURCETYPE] -> Bounty.DefaultFriendlyValue +ResourceCollector.=false ; boolean +Production.=0 ; integer +Production..Delay=0 ; integer - game frames +Production..Startup=0 ; integer +Production..Display.Offset= ; Point2D - X,Y +Production..Display.Color= ; integer - Red,Green,Blue, default to [SOMERESOURCETYPE] -> Display.Color +Production..Display.Houses=all ; AffectedHouse (all | owner | allies | enemies | none) + +[SOMETIBERIUM] ; TiberiumType +ResourceType= ; ResourceType +ResourceValue= ; integer, default to Value + +[SOMESUPERWEAPON] ; SuperWeaponType +ResourceType..Amount=0 ; integer + +[SOMESIDE] ; SideClass +Sidebar.ResourceTypes.Types= ; List of ResourceTypes +Sidebar.ResourceTypes.Offset=0,0 ; Point2D - X,Y +Sidebar.ResourceTypes.Color= ; integer - Red,Green,Blue +Sidebar.ResourceTypes.Align=Left ; TextAlign (Left | Right | Center) ``` ### Laser Trails diff --git a/scratch/disasm_credits.py b/scratch/disasm_credits.py new file mode 100644 index 0000000000..e9e3f511b8 --- /dev/null +++ b/scratch/disasm_credits.py @@ -0,0 +1,35 @@ +import capstone + +with open(r'E:\Juegos\CnCReloaded - 2.7 - development\gamemd.exe', 'rb') as f: + exe_data = f.read() + +# PE header parsing to find RVA mapping +pe_offset = int.from_bytes(exe_data[0x3C:0x40], 'little') +num_sections = int.from_bytes(exe_data[pe_offset+6:pe_offset+8], 'little') +opt_header_size = int.from_bytes(exe_data[pe_offset+20:pe_offset+22], 'little') +sec_table_offset = pe_offset + 24 + opt_header_size + +sections = [] +for i in range(num_sections): + sec = exe_data[sec_table_offset + i*40 : sec_table_offset + (i+1)*40] + name = sec[:8].strip(b'\x00').decode('latin1') + vsize = int.from_bytes(sec[8:12], 'little') + vaddr = int.from_bytes(sec[12:16], 'little') + rsize = int.from_bytes(sec[16:20], 'little') + rptr = int.from_bytes(sec[20:24], 'little') + sections.append((vaddr, vsize, rptr, rsize)) + +def va_to_offset(va): + rva = va - 0x400000 + for vaddr, vsize, rptr, rsize in sections: + if vaddr <= rva < vaddr + vsize: + return rptr + (rva - vaddr) + return None + +md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32) +for va in [0x4A24D0, 0x4A25D0]: + start_off = va_to_offset(va) + code = exe_data[start_off:start_off+25] + for insn in md.disasm(code, va): + print(f"0x{insn.address:X}: {insn.mnemonic} {insn.op_str}") + print() diff --git a/src/Ext/Building/Hooks.Production.cpp b/src/Ext/Building/Hooks.Production.cpp index 4cf96ad5eb..a7159104fd 100644 --- a/src/Ext/Building/Hooks.Production.cpp +++ b/src/Ext/Building/Hooks.Production.cpp @@ -12,12 +12,25 @@ DEFINE_HOOK(0x4C9BD5, FactoryClass_AI_ProcessProductionStep, 0x31) const auto pOwner = pFactory->Owner; if (pOwner) { + const auto pHouseExt = HouseExt::TryFetch(pOwner); const int availableMoney = pOwner->Available_Money(); bool canAfford = (moneyCost <= availableMoney); + for (size_t resIdx = 0; resIdx < ResourceTypeClass::Array.size(); ++resIdx) + { + const auto pResource = ResourceTypeClass::Array[resIdx].get(); + if (pResource && pResource->IsMoneyResource()) + { + if (pHouseExt && !pHouseExt->IsResourceEnabled(static_cast(resIdx))) + { + canAfford = false; + } + break; + } + } + TechnoTypeClass* pType = pFactory->Object ? pFactory->Object->GetTechnoType() : (pFactory->QueuedObjects.Count > 0 ? pFactory->QueuedObjects[0] : nullptr); const auto pTypeExt = TechnoTypeExt::TryFetch(pType); - const auto pHouseExt = HouseExt::TryFetch(pOwner); std::vector resDues; if (pTypeExt && pHouseExt && !pTypeExt->ResourceCosts.empty()) diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 0ef935cdb9..7bdc1c89ff 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1433,6 +1433,19 @@ DEFINE_HOOK(0x450885, BuildingClass_Repair_CheckAndSpend, 0x23) const auto pTypeExt = TechnoTypeExt::TryFetch(pType); const auto pHouseExt = HouseExt::TryFetch(pOwner); + for (size_t resIdx = 0; resIdx < ResourceTypeClass::Array.size(); ++resIdx) + { + const auto pResource = ResourceTypeClass::Array[resIdx].get(); + if (pResource && pResource->IsMoneyResource()) + { + if (pHouseExt && !pHouseExt->IsResourceEnabled(static_cast(resIdx))) + { + return StopRepair; + } + break; + } + } + std::vector> resDues; if (pTypeExt && pHouseExt && !pTypeExt->ResourceCosts.empty() && pType && pType->Strength > 0 && repairHP > 0) { diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index a144e98ffe..2282e49de7 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -1197,6 +1197,21 @@ bool HouseExt::ReachedBuildLimit(const HouseClass* pHouse, const TechnoTypeClass int HouseExt::GetResourceAmount(int resourceIdx) const { + if (resourceIdx >= 0 && resourceIdx < static_cast(ResourceTypeClass::Array.size())) + { + const auto pResource = ResourceTypeClass::Array[resourceIdx].get(); + if (pResource && pResource->IsMoneyResource()) + { + auto pHouse = this->OwnerObject(); + return pHouse ? pHouse->Available_Money() : 0; + } + if (pResource && pResource->IsPowerResource()) + { + auto pHouse = this->OwnerObject(); + return pHouse ? (pHouse->PowerOutput - pHouse->PowerDrain) : 0; + } + } + if (resourceIdx >= 0 && resourceIdx < static_cast(this->CustomResources.size())) return this->CustomResources[resourceIdx]; return 0; @@ -1207,6 +1222,18 @@ void HouseExt::SetResourceAmount(int resourceIdx, int amount) if (resourceIdx < 0) return; + if (resourceIdx < static_cast(ResourceTypeClass::Array.size())) + { + const auto pResource = ResourceTypeClass::Array[resourceIdx].get(); + if (pResource && pResource->IsMoneyResource()) + { + auto pHouse = this->OwnerObject(); + if (pHouse) + pHouse->TransactMoney(amount - pHouse->Available_Money()); + return; + } + } + if (resourceIdx >= static_cast(this->CustomResources.size())) const_cast(this)->CustomResources.resize(resourceIdx + 1, 0); @@ -1218,6 +1245,18 @@ void HouseExt::UpdateResourceAmount(int resourceIdx, int delta) if (resourceIdx < 0) return; + if (resourceIdx < static_cast(ResourceTypeClass::Array.size())) + { + const auto pResource = ResourceTypeClass::Array[resourceIdx].get(); + if (pResource && pResource->IsMoneyResource()) + { + auto pHouse = this->OwnerObject(); + if (pHouse) + pHouse->TransactMoney(delta); + return; + } + } + if (resourceIdx >= static_cast(this->CustomResources.size())) const_cast(this)->CustomResources.resize(resourceIdx + 1, 0); @@ -1226,6 +1265,8 @@ void HouseExt::UpdateResourceAmount(int resourceIdx, int delta) bool HouseExt::CanAffordResource(int resourceIdx, int amount) const { + if (!this->IsResourceEnabled(resourceIdx)) + return false; return this->GetResourceAmount(resourceIdx) >= amount; } diff --git a/src/Ext/TAction/Body.cpp b/src/Ext/TAction/Body.cpp index 2d01f8a4f0..5055ff87cb 100644 --- a/src/Ext/TAction/Body.cpp +++ b/src/Ext/TAction/Body.cpp @@ -849,6 +849,10 @@ bool TActionExt::ModifyCustomResource(TActionClass* pThis, HouseClass* pHouse, O if (resIdx < 0) return true; + const auto pResource = ResourceTypeClass::Array[resIdx].get(); + if (pResource && pResource->IsPowerResource()) + return true; + HouseClass* pTargetHouse = pHouse; if (pThis->Value2 >= 0) { diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 28ee6160ad..cbf9d1b292 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1150,39 +1150,42 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) for (size_t i = 0; i < resourceCount; ++i) { const auto pResource = ResourceTypeClass::Array[i].get(); - if (!pResource) + if (!pResource || pResource->IsPowerResource()) continue; - char costTag[64]; - sprintf_s(costTag, "Cost.%s", (const char*)pResource->Name); - int costVal = 0; - if (exINI.ReadInteger(pSection, costTag, &costVal)) + if (!pResource->IsMoneyResource()) { - this->ResourceCosts[i] = std::max(0, costVal); - } + char costTag[64]; + sprintf_s(costTag, "Cost.%s", (const char*)pResource->Name); + int costVal = 0; + if (exINI.ReadInteger(pSection, costTag, &costVal)) + { + this->ResourceCosts[i] = std::max(0, costVal); + } - char bountyTag[64]; - sprintf_s(bountyTag, "Bounty.%s.Value", (const char*)pResource->Name); - int bountyVal = 0; - if (exINI.ReadInteger(pSection, bountyTag, &bountyVal)) - { - this->ResourceBounties[i] = std::max(0, bountyVal); - } + char bountyTag[64]; + sprintf_s(bountyTag, "Bounty.%s.Value", (const char*)pResource->Name); + int bountyVal = 0; + if (exINI.ReadInteger(pSection, bountyTag, &bountyVal)) + { + this->ResourceBounties[i] = std::max(0, bountyVal); + } - char friendlyTag[64]; - sprintf_s(friendlyTag, "Bounty.%s.FriendlyValue", (const char*)pResource->Name); - int friendlyVal = 0; - if (exINI.ReadInteger(pSection, friendlyTag, &friendlyVal)) - { - this->ResourceFriendlyBounties[i] = -std::abs(friendlyVal); - } + char friendlyTag[64]; + sprintf_s(friendlyTag, "Bounty.%s.FriendlyValue", (const char*)pResource->Name); + int friendlyVal = 0; + if (exINI.ReadInteger(pSection, friendlyTag, &friendlyVal)) + { + this->ResourceFriendlyBounties[i] = -std::abs(friendlyVal); + } - char soylentTag[64]; - sprintf_s(soylentTag, "Soylent.%s", (const char*)pResource->Name); - int soylentVal = 0; - if (exINI.ReadInteger(pSection, soylentTag, &soylentVal)) - { - this->ResourceSoylents[i] = std::max(0, soylentVal); + char soylentTag[64]; + sprintf_s(soylentTag, "Soylent.%s", (const char*)pResource->Name); + int soylentVal = 0; + if (exINI.ReadInteger(pSection, soylentTag, &soylentVal)) + { + this->ResourceSoylents[i] = std::max(0, soylentVal); + } } // Check ResourceCollector. diff --git a/src/Misc/Hooks.Crates.cpp b/src/Misc/Hooks.Crates.cpp index bcbaff8f5c..0de522bee4 100644 --- a/src/Misc/Hooks.Crates.cpp +++ b/src/Misc/Hooks.Crates.cpp @@ -1,4 +1,10 @@ #include +#include +#include +#include +#include +#include +#include DEFINE_HOOK(0x56BD8B, MapClass_PlaceRandomCrate_Sampling, 0x5) { @@ -106,3 +112,75 @@ DEFINE_HOOK(0x481F9D, CellClass_SpringCrate_RevealMap, 0x8) return 0x481FC8; } + +DEFINE_HOOK(0x4824CA, CellClass_GoodieCheck_MoneyCrate_CustomResources, 0xF) +{ + GET_BASE(FootClass*, pUnit, 0x8); + GET(int, moneyAmount, EDI); + GET(CellClass*, pCell, ESI); + + if (pUnit && pUnit->Owner) + { + const auto pOwner = pUnit->Owner; + const auto pHouseExt = HouseExt::TryFetch(pOwner); + + // Build lottery candidates with equal weight + // Candidate -1: Vanilla Money + std::vector candidates; + candidates.push_back(-1); + + for (size_t i = 0; i < ResourceTypeClass::Array.size(); ++i) + { + const auto& pRes = ResourceTypeClass::Array[i]; + if (pRes && !pRes->IsMoneyResource() && !pRes->IsPowerResource() && pRes->Crate_Amount.Get() > 0) + { + if (!pHouseExt || pHouseExt->IsResourceEnabled(static_cast(i))) + { + candidates.push_back(static_cast(i)); + } + } + } + + int chosenIdx = -1; + if (candidates.size() > 1) + { + const int roll = ScenarioClass::Instance->Random.RandomRanged(0, static_cast(candidates.size() - 1)); + chosenIdx = candidates[roll]; + } + + if (chosenIdx == -1) + { + // Vanilla Money won + if (moneyAmount > 0) + { + pOwner->GiveMoney(moneyAmount); + } + } + else if (pHouseExt) + { + // Custom resource won + const auto& pRes = ResourceTypeClass::Array[chosenIdx]; + const int amount = pRes->Crate_Amount.Get(); + pHouseExt->UpdateResourceAmount(chosenIdx, amount); + + const CoordStruct coords = pCell->GetCenterCoords(); + + // Play custom sound if specified + if (pRes->Crate_Sound.Get() >= 0) + { + VocClass::PlayAt(pRes->Crate_Sound.Get(), coords); + } + + // Spawn custom animation if specified + if (pRes->Crate_Anim.Get() >= 0) + { + if (const auto pAnimType = AnimTypeClass::Array.GetItemOrDefault(pRes->Crate_Anim.Get())) + { + GameCreate(pAnimType, coords); + } + } + } + } + + return 0x4824D9; +} diff --git a/src/Misc/Hooks.UI.cpp b/src/Misc/Hooks.UI.cpp index f1161358ec..43380724b6 100644 --- a/src/Misc/Hooks.UI.cpp +++ b/src/Misc/Hooks.UI.cpp @@ -99,6 +99,24 @@ DEFINE_HOOK(0x4A2729, CreditClass_AI_CreditsStepClamp, 0x5) return Continue; } +DEFINE_HOOK(0x4A24DB, CreditsClass_Draw_SkipIfMoneyResource1, 0x5) +{ + if (ResourceTypeClass::ShouldSkipWestwoodCredits()) + { + return 0x4A24E0; + } + return 0; +} + +DEFINE_HOOK(0x4A25DB, CreditsClass_Draw_SkipIfMoneyResource2, 0x5) +{ + if (ResourceTypeClass::ShouldSkipWestwoodCredits()) + { + return 0x4A25E0; + } + return 0; +} + DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) { auto const pPlayer = HouseClass::CurrentPlayer; @@ -106,116 +124,10 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) return 0; RectangleStruct vRect = DSurface::Sidebar->GetRect(); - auto pHouseExt = HouseExt::Fetch(pPlayer); auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); - // Calculate base HUD coordinate based on Anchor - int baseX = 0; - int baseY = 0; - const Point2D baseOffset = ResourceTypeClass::Global_Display_BaseOffset.Get(); - - switch (ResourceTypeClass::Global_Display_Anchor.Get()) - { - case ResourceDisplayAnchor::TopLeft: - baseX = 10 + baseOffset.X; - baseY = 2 + baseOffset.Y; - break; - case ResourceDisplayAnchor::TopRight: - baseX = DSurface::Sidebar->GetWidth() - 100 + baseOffset.X; - baseY = 2 + baseOffset.Y; - break; - case ResourceDisplayAnchor::BottomLeft: - baseX = 10 + baseOffset.X; - baseY = DSurface::Sidebar->GetHeight() - 30 + baseOffset.Y; - break; - case ResourceDisplayAnchor::BottomRight: - baseX = DSurface::Sidebar->GetWidth() - 100 + baseOffset.X; - baseY = DSurface::Sidebar->GetHeight() - 30 + baseOffset.Y; - break; - case ResourceDisplayAnchor::Sidebar: - default: - baseX = DSurface::Sidebar->GetWidth() / 2 - 70 + pSideExt->Sidebar_ResourceTypes_Offset.Get().X + baseOffset.X; - baseY = 2 + pSideExt->Sidebar_ResourceTypes_Offset.Get().Y + baseOffset.Y; - break; - } - - int const spacing = ResourceTypeClass::Global_Display_Spacing.Get(); - int stackIndex = 0; - - const bool hasExplicitTypes = !pSideExt->Sidebar_ResourceTypes_Types.empty(); - const size_t count = hasExplicitTypes ? pSideExt->Sidebar_ResourceTypes_Types.size() : ResourceTypeClass::Array.size(); - - for (size_t iter = 0; iter < count; ++iter) - { - const int i = hasExplicitTypes ? pSideExt->Sidebar_ResourceTypes_Types[iter] : static_cast(iter); - if (i < 0 || i >= static_cast(ResourceTypeClass::Array.size())) - continue; - - const auto pResource = ResourceTypeClass::Array[i].get(); - if (!pResource) - continue; - - if (!pHouseExt->IsResourceEnabled(i)) - continue; - - // Check display condition - if (pResource->Display_Condition.Get() == ResourceDisplayCondition::Never) - continue; - - const int amount = pHouseExt->GetResourceAmount(i); - const bool hasCollector = (i < static_cast(pHouseExt->ResourceCollectorCounts.size())) && (pHouseExt->ResourceCollectorCounts[i] > 0); - - if (!hasCollector) - { - if (pResource->Display_Condition.Get() == ResourceDisplayCondition::GreaterThanZero && amount <= 0) - continue; - - if (pResource->Display_Condition.Get() == ResourceDisplayCondition::HasCollector) - continue; - } - - wchar_t counter[0x40]; - const wchar_t* label = pResource->Display_Label.Get(); - if (label && *label) - { - const bool useSpace = pResource->Display_Label_UseSpace.Get(); - if (pResource->Display_Label_InvertPosition.Get()) - swprintf_s(counter, useSpace ? L"%d %ls" : L"%d%ls", amount, label); - else - swprintf_s(counter, useSpace ? L"%ls %d" : L"%ls%d", label, amount); - } - else - { - swprintf_s(counter, L"%d", amount); - } - - Point2D vPos; - if (pResource->Display_Offset.isset()) - { - vPos = pResource->Display_Offset.Get(); - } - else - { - if (ResourceTypeClass::Global_Display_Orientation.Get() == ResourceDisplayOrientation::Horizontal) - { - vPos.X = baseX + stackIndex * spacing; - vPos.Y = baseY; - } - else - { - vPos.X = baseX; - vPos.Y = baseY + stackIndex * spacing; - } - ++stackIndex; - } - - const ColorStruct resColor = pResource->Display_Color.Get(); - const ColorStruct clrToolTip = (resColor != ColorStruct { 0, 0, 0 }) ? resColor : pSideExt->Sidebar_ResourceTypes_Color.Get(Drawing::TooltipColor); - auto const TextFlags = static_cast(static_cast(TextPrintType::UseGradPal | TextPrintType::Metal12) - | static_cast(pSideExt->Sidebar_ResourceTypes_Align.Get())); - - DSurface::Sidebar->DrawText(counter, &vRect, &vPos, Drawing::RGB_To_Int(clrToolTip), 0, TextFlags); - } + // Draw custom resources in sidebar if Anchor=Sidebar + ResourceTypeClass::DrawResourceHUD(DSurface::Sidebar, true); if (Phobos::UI::HarvesterCounter_Show && Phobos::Config::ShowHarvesterCounter) { @@ -244,7 +156,7 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) TextPrintType::UseGradPal | TextPrintType::Center | TextPrintType::Metal12); } - if (Phobos::UI::PowerDelta_Show && Phobos::Config::ShowPowerDelta && pPlayer->Buildings.Count) + if (Phobos::UI::PowerDelta_Show && Phobos::Config::ShowPowerDelta && pPlayer->Buildings.Count && !ResourceTypeClass::HasPowerResource()) { wchar_t counter[0x20]; diff --git a/src/Misc/PhobosToolTip.cpp b/src/Misc/PhobosToolTip.cpp index ef2d29c333..20b68a920f 100644 --- a/src/Misc/PhobosToolTip.cpp +++ b/src/Misc/PhobosToolTip.cpp @@ -133,19 +133,14 @@ void PhobosToolTip::HelpText_Techno(TechnoTypeClass* pType) const int cost = pType->GetActualCost(HouseClass::CurrentPlayer); std::wostringstream oss; - oss << pType->UIName << L"\n" - << (cost < 0 ? L"+" : L"") - << Phobos::UI::CostLabel << std::abs(cost) << L" " - << Phobos::UI::TimeLabel - << std::setw(2) << std::setfill(L'0') << nMin << L":" - << std::setw(2) << std::setfill(L'0') << nSec; + oss << pType->UIName << L"\n"; - if (auto const nPower = this->GetPower(pType)) + bool hasPreceding = false; + + if (cost != 0) { - oss << L" " << Phobos::UI::PowerLabel; - if (nPower > 0) - oss << L"+"; - oss << std::setw(1) << nPower; + oss << Phobos::UI::CostLabel << std::abs(cost); + hasPreceding = true; } for (size_t i = 0; i < pData->ResourceCosts.size(); ++i) @@ -156,23 +151,43 @@ void PhobosToolTip::HelpText_Techno(TechnoTypeClass* pType) const auto pResource = ResourceTypeClass::Array[i].get(); if (pResource) { + if (hasPreceding) + oss << L" "; + const wchar_t* label = pResource->Display_Label.Get(); const bool useSpace = pResource->Display_Label_UseSpace.Get(); if (label && *label) { if (pResource->Display_Label_InvertPosition.Get()) - oss << L" " << resCost << (useSpace ? L" " : L"") << label; + oss << resCost << (useSpace ? L" " : L"") << label; else - oss << L" " << label << (useSpace ? L" " : L"") << resCost; + oss << label << (useSpace ? L" " : L"") << resCost; } else { - oss << L" " << resCost; + oss << resCost; } + + hasPreceding = true; } } } + if (hasPreceding) + oss << L" "; + + oss << Phobos::UI::TimeLabel + << std::setw(2) << std::setfill(L'0') << nMin << L":" + << std::setw(2) << std::setfill(L'0') << nSec; + + if (auto const nPower = this->GetPower(pType)) + { + oss << L" " << Phobos::UI::PowerLabel; + if (nPower > 0) + oss << L"+"; + oss << std::setw(1) << nPower; + } + if (auto const pDesc = this->GetUIDescription(pData)) oss << L"\n" << pDesc; @@ -187,17 +202,18 @@ void PhobosToolTip::HelpText_Super(int swidx) std::wostringstream oss; oss << pType->UIName; - bool showSth = false; + bool lineStarted = false; - if (const int nCost = std::abs(pData->Money_Amount)) + if (const int nCost = pData->Money_Amount) { oss << L"\n"; + lineStarted = true; - if (pData->Money_Amount > 0) - oss << '+'; + // Negative money amount means granting money upon launch (+) + if (nCost < 0) + oss << L"+"; - oss << Phobos::UI::CostLabel << nCost; - showSth = true; + oss << Phobos::UI::CostLabel << std::abs(nCost); } for (size_t i = 0; i < pData->SW_ResourceAmounts.size(); ++i) @@ -205,43 +221,57 @@ void PhobosToolTip::HelpText_Super(int swidx) const int nAmount = pData->SW_ResourceAmounts[i]; if (nAmount != 0) { - oss << L"\n"; + if (!lineStarted) + { + oss << L"\n"; + lineStarted = true; + } + else + { + oss << L" "; + } + const auto pResource = (i < ResourceTypeClass::Array.size()) ? ResourceTypeClass::Array[i].get() : nullptr; const wchar_t* label = pResource ? pResource->Display_Label.Get() : L""; const bool useSpace = pResource ? pResource->Display_Label_UseSpace.Get() : true; - if (nAmount > 0) + // Negative resource amount means granting resource upon launch (+) + const bool isGrant = (nAmount < 0); + + if (pResource && pResource->Display_Label_InvertPosition.Get()) { - if (pResource && pResource->Display_Label_InvertPosition.Get()) - oss << L"+" << std::abs(nAmount) << (useSpace ? L" " : L"") << label; - else - oss << label << (useSpace ? L" " : L"") << L"+" << std::abs(nAmount); + if (isGrant) + oss << L"+"; + oss << std::abs(nAmount) << (useSpace ? L" " : L"") << label; } else { - if (pResource && pResource->Display_Label_InvertPosition.Get()) - oss << L"-" << std::abs(nAmount) << (useSpace ? L" " : L"") << label; - else - oss << label << (useSpace ? L" " : L"") << L"-" << std::abs(nAmount); + if (isGrant) + oss << L"+"; + oss << label << (useSpace ? L" " : L"") << std::abs(nAmount); } - - showSth = true; } } const int rechargeTime = TickTimeToSeconds(pSuper->GetRechargeTime()); if (rechargeTime > 0) { - if (!showSth) + if (!lineStarted) + { oss << L"\n"; + lineStarted = true; + } + else + { + oss << L" "; + } const int nSec = rechargeTime % 60; const int nMin = rechargeTime / 60; - oss << (showSth ? L" " : L"") << Phobos::UI::TimeLabel + oss << Phobos::UI::TimeLabel << std::setw(2) << std::setfill(L'0') << nMin << L":" << std::setw(2) << std::setfill(L'0') << nSec; - showSth = true; } auto const& sw_ext = HouseExt::Fetch(HouseClass::CurrentPlayer)->SuperExts[swidx]; @@ -249,12 +279,19 @@ void PhobosToolTip::HelpText_Super(int swidx) const int remain_shots = pData->SW_Shots - sw_ext.ShotCount; if (sw_shots > 0) { - if (!showSth) + if (!lineStarted) + { oss << L"\n"; + lineStarted = true; + } + else + { + oss << L" "; + } wchar_t buffer[64]; swprintf_s(buffer, Phobos::UI::SWShotsFormat, remain_shots, sw_shots); - oss << (showSth ? L" " : L"") << buffer; + oss << buffer; } if (auto const pDesc = this->GetUIDescription(pData)) diff --git a/src/New/Type/ResourceTypeClass.cpp b/src/New/Type/ResourceTypeClass.cpp index 1e43a203e0..ed50eecfed 100644 --- a/src/New/Type/ResourceTypeClass.cpp +++ b/src/New/Type/ResourceTypeClass.cpp @@ -1,5 +1,14 @@ #include "ResourceTypeClass.h" +#include +#include +#include +#include +#include +#include +#include +#include + template<> const char* Enumerable::GetMainSection() { @@ -10,13 +19,18 @@ Valueable ResourceTypeClass::Global_Display_Orientat Valueable ResourceTypeClass::Global_Display_Anchor { ResourceDisplayAnchor::TopRight }; Valueable ResourceTypeClass::Global_Display_BaseOffset { Point2D::Empty }; Valueable ResourceTypeClass::Global_Display_Spacing { 14 }; +Nullable ResourceTypeClass::Global_Display_Align {}; +PhobosPCXFile ResourceTypeClass::Global_Display_Background_PCX {}; +Valueable ResourceTypeClass::Global_Display_Background_SHP { nullptr }; +CustomPalette ResourceTypeClass::Global_Display_Background_Palette {}; +Valueable ResourceTypeClass::Global_Display_Background_Offset { Point2D::Empty }; ResourceTypeClass::ResourceTypeClass(const char* pTitle) : Enumerable(pTitle) , Display_Label {} , Display_Label_InvertPosition { false } - , Display_Label_UseSpace { true } - , Display_Color { { 255, 255, 255 } } - , Display_Condition { ResourceDisplayCondition::GreaterThanZero } + , Display_Label_UseSpace { pTitle ? (_strcmpi(pTitle, "Money") != 0 && _strcmpi(pTitle, "Power") != 0) : true } + , Display_Color {} + , Display_Condition { (pTitle && _strcmpi(pTitle, "Power") == 0) ? ResourceDisplayCondition::Never : ResourceDisplayCondition::Always } , Display_Offset {} , InitialValue { 0 } , RequiresCollector { false } @@ -25,9 +39,62 @@ ResourceTypeClass::ResourceTypeClass(const char* pTitle) : EnumerableIsMoneyResource()) + return true; + } + return false; +} + +bool ResourceTypeClass::HasPowerResource() +{ + for (const auto& pResource : ResourceTypeClass::Array) + { + if (pResource && pResource->IsPowerResource()) + return true; + } + return false; +} + +bool ResourceTypeClass::ShouldSkipWestwoodCredits() +{ + if (!ResourceTypeClass::HasMoneyResource()) + return false; + + const auto globalAnchor = ResourceTypeClass::Global_Display_Anchor.Get(); + if (globalAnchor != ResourceDisplayAnchor::Sidebar) + return true; + + auto const pPlayer = HouseClass::CurrentPlayer; + if (!pPlayer || pPlayer->Defeated) + return false; + + auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); + if (pSideExt && !pSideExt->Sidebar_ResourceTypes_Types.empty()) + { + for (const auto idx : pSideExt->Sidebar_ResourceTypes_Types) + { + if (idx >= 0 && idx < static_cast(ResourceTypeClass::Array.size())) + { + if (ResourceTypeClass::Array[idx]->IsMoneyResource()) + return true; + } + } + } + + return false; +} + void ResourceTypeClass::LoadGlobalsFromINI(CCINIClass* pINI) { INI_EX exINI(pINI); @@ -36,11 +103,36 @@ void ResourceTypeClass::LoadGlobalsFromINI(CCINIClass* pINI) Global_Display_Anchor.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Anchor"); Global_Display_BaseOffset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BaseOffset"); Global_Display_Spacing.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Spacing"); + Global_Display_Align.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Align"); + + Global_Display_Background_PCX.Read(pINI, GameStrings::AudioVisual, "Display.ResourceTypes.BackgroundPCX"); + Global_Display_Background_PCX.Read(pINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.PCX"); + + Global_Display_Background_SHP.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background"); + Global_Display_Background_SHP.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.SHP"); + Global_Display_Background_SHP.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.Shape"); + + Global_Display_Background_Offset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.Offset"); + Global_Display_Background_Offset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BackgroundOffset"); + Global_Display_Background_Offset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BackgroundPCX.Offset"); + Global_Display_Background_Offset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BackgroundPCXOffset"); + Global_Display_Background_Palette.LoadFromINI(pINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.Palette"); + Global_Display_Background_Palette.LoadFromINI(pINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.SHP.Palette"); } void ResourceTypeClass::LoadFromINI(CCINIClass* pINI) { const char* section = this->Name; + if (this->IsMoneyResource() || this->IsPowerResource()) + { + this->Display_Label_UseSpace = false; + } + + if (this->IsPowerResource()) + { + this->Display_Condition = ResourceDisplayCondition::Never; + } + if (!pINI->GetSection(section)) return; @@ -61,6 +153,19 @@ void ResourceTypeClass::LoadFromINI(CCINIClass* pINI) this->Bounty_DefaultFriendlyValue.Read(exINI, section, "Bounty.DefaultFriendlyValue"); this->Bounty_CanUseStandardPoints.Read(exINI, section, "Bounty.CanUseStandardPoints"); this->Bounty_MoneyConversion.Read(exINI, section, "Bounty.MoneyConversion"); + + this->Crate_Amount.Read(exINI, section, "Crate.Amount"); + this->Crate_Sound.Read(exINI, section, "Crate.Sound"); + this->Crate_Anim.Read(exINI, section, "Crate.Anim"); + + this->Display_Power_Mode.Read(exINI, section, "Display.Power.Mode"); + this->Display_Power_Mode.Read(exINI, section, "Display.Power.Format"); + + bool showTotal = true; + if (exINI.ReadBool(section, "Display.Power.ShowTotal", &showTotal)) + { + this->Display_Power_Mode = showTotal ? ResourcePowerDisplayMode::NetAndTotal : ResourcePowerDisplayMode::Net; + } } template @@ -79,7 +184,11 @@ void ResourceTypeClass::Serialize(T& Stm) .Process(this->Bounty_DefaultValue) .Process(this->Bounty_DefaultFriendlyValue) .Process(this->Bounty_CanUseStandardPoints) - .Process(this->Bounty_MoneyConversion); + .Process(this->Bounty_MoneyConversion) + .Process(this->Crate_Amount) + .Process(this->Crate_Sound) + .Process(this->Crate_Anim) + .Process(this->Display_Power_Mode); } void ResourceTypeClass::LoadFromStream(PhobosStreamReader& Stm) @@ -91,3 +200,375 @@ void ResourceTypeClass::SaveToStream(PhobosStreamWriter& Stm) { this->Serialize(Stm); } + +void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) +{ + if (!pSurface) + return; + + auto const pPlayer = HouseClass::CurrentPlayer; + if (!pPlayer || pPlayer->Defeated) + return; + + auto pHouseExt = HouseExt::Fetch(pPlayer); + auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); + + const auto globalAnchor = ResourceTypeClass::Global_Display_Anchor.Get(); + + // If global anchor is Sidebar: only draw during sidebar pass. + // If global anchor is NOT Sidebar (TopLeft, TopRight, etc.): only draw during tactical game screen pass. + if (globalAnchor == ResourceDisplayAnchor::Sidebar) + { + if (!isSidebar) + return; + } + else + { + if (isSidebar) + return; + } + + RectangleStruct vRect = pSurface->GetRect(); + + int baseX = 0; + int baseY = 0; + const Point2D baseOffset = ResourceTypeClass::Global_Display_BaseOffset.Get(); + + const int marginX = Phobos::Config::MessageDisplayInCenter ? 28 : 10; + const int screenW = pSurface->GetWidth(); + const int screenH = pSurface->GetHeight(); + + if (isSidebar) + { + baseX = 20 + pSideExt->Sidebar_ResourceTypes_Offset.Get().X + baseOffset.X; + baseY = 2 + pSideExt->Sidebar_ResourceTypes_Offset.Get().Y + baseOffset.Y; + } + else + { + switch (globalAnchor) + { + case ResourceDisplayAnchor::TopLeft: + baseX = marginX + baseOffset.X; + baseY = 5 + baseOffset.Y; + break; + case ResourceDisplayAnchor::TopRight: + baseX = screenW - marginX + baseOffset.X; + baseY = 5 + baseOffset.Y; + break; + case ResourceDisplayAnchor::TopCenter: + baseX = (screenW / 2) + baseOffset.X; + baseY = 5 + baseOffset.Y; + break; + case ResourceDisplayAnchor::BottomLeft: + baseX = marginX + baseOffset.X; + baseY = screenH - 20 + baseOffset.Y; + break; + case ResourceDisplayAnchor::BottomRight: + baseX = screenW - marginX + baseOffset.X; + baseY = screenH - 20 + baseOffset.Y; + break; + case ResourceDisplayAnchor::BottomCenter: + baseX = (screenW / 2) + baseOffset.X; + baseY = screenH - 20 + baseOffset.Y; + break; + default: + baseX = marginX + baseOffset.X; + baseY = 5 + baseOffset.Y; + break; + } + } + + int const spacing = ResourceTypeClass::Global_Display_Spacing.Get(); + int stackIndex = 0; + int runningRightX = baseX; + int runningLeftX = baseX; + + TextAlign effectiveAlign; + if (ResourceTypeClass::Global_Display_Align.isset()) + { + effectiveAlign = ResourceTypeClass::Global_Display_Align.Get(); + } + else + { + if (isSidebar) + effectiveAlign = pSideExt->Sidebar_ResourceTypes_Align.Get(); + else if (globalAnchor == ResourceDisplayAnchor::TopCenter || globalAnchor == ResourceDisplayAnchor::BottomCenter) + effectiveAlign = TextAlign::Center; + else if (globalAnchor == ResourceDisplayAnchor::TopRight || globalAnchor == ResourceDisplayAnchor::BottomRight) + effectiveAlign = TextAlign::Right; + else + effectiveAlign = TextAlign::Left; + } + + const bool isRightAlign = (effectiveAlign == TextAlign::Right); + const bool isCenterAlign = (effectiveAlign == TextAlign::Center); + const bool isBottomAnchor = !isSidebar && (globalAnchor == ResourceDisplayAnchor::BottomLeft || globalAnchor == ResourceDisplayAnchor::BottomRight || globalAnchor == ResourceDisplayAnchor::BottomCenter); + + const bool hasExplicitSidebarTypes = !pSideExt->Sidebar_ResourceTypes_Types.empty(); + const size_t count = (isSidebar && hasExplicitSidebarTypes) ? pSideExt->Sidebar_ResourceTypes_Types.size() : ResourceTypeClass::Array.size(); + + bool bgDrawn = false; + auto DrawBackgroundOnce = [&]() + { + if (bgDrawn || isSidebar) + return; + bgDrawn = true; + + int bgWidth = 0; + int bgHeight = 0; + BSurface* pPCX = ResourceTypeClass::Global_Display_Background_PCX.GetSurface(); + SHPStruct* pSHP = ResourceTypeClass::Global_Display_Background_SHP.Get(); + + if (pPCX) + { + bgWidth = pPCX->GetWidth(); + bgHeight = pPCX->GetHeight(); + } + else if (pSHP) + { + bgWidth = pSHP->Width; + bgHeight = pSHP->Height; + } + else + { + return; + } + + Point2D const bgOffset = ResourceTypeClass::Global_Display_Background_Offset.Get(); + Point2D bgPos; + if (isRightAlign) + bgPos.X = (baseX - bgWidth) + bgOffset.X; + else if (isCenterAlign) + bgPos.X = (baseX - bgWidth / 2) + bgOffset.X; + else + bgPos.X = baseX + bgOffset.X; + + bgPos.Y = (isBottomAnchor ? (baseY - bgHeight) : baseY) + bgOffset.Y; + + if (pPCX) + { + RectangleStruct drawRect { bgPos.X, bgPos.Y, bgWidth, bgHeight }; + PCX::Instance.BlitToSurface(&drawRect, pSurface, pPCX); + } + else if (pSHP) + { + ConvertClass* pConvert = ResourceTypeClass::Global_Display_Background_Palette.GetOrDefaultConvert(FileSystem::PALETTE_PAL); + int const frame = pSHP->Frames > 1 ? (Unsorted::CurrentFrame % pSHP->Frames) : 0; + + pSurface->DrawSHP + ( + pConvert, + pSHP, + frame, + &bgPos, + &vRect, + BlitterFlags::None, + 0, + 0, + ZGradient::Ground, + 1000, + 0, + nullptr, + 0, + 0, + 0 + ); + } + }; + + for (size_t iter = 0; iter < count; ++iter) + { + const int i = (isSidebar && hasExplicitSidebarTypes) ? pSideExt->Sidebar_ResourceTypes_Types[iter] : static_cast(iter); + if (i < 0 || i >= static_cast(ResourceTypeClass::Array.size())) + continue; + + const auto pResource = ResourceTypeClass::Array[i].get(); + if (!pResource || !pHouseExt->IsResourceEnabled(i)) + continue; + + if (pResource->Display_Condition.Get() == ResourceDisplayCondition::Never) + continue; + + // If on sidebar without an explicit Sidebar.ResourceTypes.Types list, Money remains at its default Westwood position + if (isSidebar && !hasExplicitSidebarTypes && pResource->IsMoneyResource()) + continue; + + bool hasCollector = false; + if (pResource->IsPowerResource()) + { + hasCollector = pPlayer->Buildings.Count > 0; + } + else if (pResource->IsMoneyResource()) + { + const bool hasCustomCollector = (i < static_cast(pHouseExt->ResourceCollectorCounts.size())) && (pHouseExt->ResourceCollectorCounts[i] > 0); + hasCollector = hasCustomCollector || (HouseExt::ActiveHarvesterCount(pPlayer) > 0); + } + else + { + hasCollector = (i < static_cast(pHouseExt->ResourceCollectorCounts.size())) && (pHouseExt->ResourceCollectorCounts[i] > 0); + } + + if (!hasCollector) + { + if (pResource->Display_Condition.Get() == ResourceDisplayCondition::GreaterThanZero) + { + if (pResource->IsPowerResource()) + { + if ((pPlayer->PowerOutput - pPlayer->PowerDrain) <= 0) + continue; + } + else if (pHouseExt->GetResourceAmount(i) <= 0) + { + continue; + } + } + + if (pResource->Display_Condition.Get() == ResourceDisplayCondition::HasCollector) + continue; + } + + DrawBackgroundOnce(); + + wchar_t counter[0x40]; + const wchar_t* label = pResource->Display_Label.Get(); + ColorStruct clrToolTip; + + if (pResource->IsPowerResource()) + { + if (!label || !*label) + label = Phobos::UI::PowerLabel; + + const int output = pPlayer->PowerOutput; + const int drain = pPlayer->PowerDrain; + const int net = output - drain; + + wchar_t powerVal[0x40]; + switch (pResource->Display_Power_Mode.Get()) + { + case ResourcePowerDisplayMode::NetAndTotal: + swprintf_s(powerVal, L"%d / %d", net, output); + break; + case ResourcePowerDisplayMode::Net: + swprintf_s(powerVal, L"%d", net); + break; + case ResourcePowerDisplayMode::DrainAndTotal: + swprintf_s(powerVal, L"%d / %d", drain, output); + break; + case ResourcePowerDisplayMode::Drain: + swprintf_s(powerVal, L"%d", drain); + break; + case ResourcePowerDisplayMode::Total: + swprintf_s(powerVal, L"%d", output); + break; + } + + if (label && *label) + { + const bool useSpace = pResource->Display_Label_UseSpace.Get(); + if (pResource->Display_Label_InvertPosition.Get()) + swprintf_s(counter, useSpace ? L"%ls %ls" : L"%ls%ls", powerVal, label); + else + swprintf_s(counter, useSpace ? L"%ls %ls" : L"%ls%ls", label, powerVal); + } + else + { + wcscpy_s(counter, powerVal); + } + + if (pResource->Display_Color.isset()) + { + clrToolTip = pResource->Display_Color.Get(); + } + else + { + if (pPlayer->PowerBlackoutTimer.InProgress()) + { + clrToolTip = pSideExt->Sidebar_PowerDelta_ColorGrey.Get(); + } + else + { + const double percent = output != 0 + ? (double)drain / (double)output + : (drain != 0 ? Phobos::UI::PowerDelta_ConditionRed * 2.f : Phobos::UI::PowerDelta_ConditionYellow); + + clrToolTip = percent < Phobos::UI::PowerDelta_ConditionYellow + ? pSideExt->Sidebar_PowerDelta_ColorGreen.Get() + : LESS_EQUAL(percent, Phobos::UI::PowerDelta_ConditionRed) + ? pSideExt->Sidebar_PowerDelta_ColorYellow.Get() + : pSideExt->Sidebar_PowerDelta_ColorRed.Get(); + } + } + } + else + { + if (pResource->IsMoneyResource() && (!label || !*label)) + label = Phobos::UI::CostLabel; + + const int amount = pHouseExt->GetResourceAmount(i); + if (label && *label) + { + const bool useSpace = pResource->Display_Label_UseSpace.Get(); + if (pResource->Display_Label_InvertPosition.Get()) + swprintf_s(counter, useSpace ? L"%d %ls" : L"%d%ls", amount, label); + else + swprintf_s(counter, useSpace ? L"%ls %d" : L"%ls%d", label, amount); + } + else + { + swprintf_s(counter, L"%d", amount); + } + + clrToolTip = pResource->Display_Color.Get(pSideExt->Sidebar_ResourceTypes_Color.Get(Drawing::TooltipColor)); + } + + Point2D vPos; + if (pResource->Display_Offset.isset()) + { + vPos = pResource->Display_Offset.Get(); + } + else + { + auto const dim = Drawing::GetTextDimensions(counter, { 0, 0 }, 0, 2, 0); + + if (ResourceTypeClass::Global_Display_Orientation.Get() == ResourceDisplayOrientation::Horizontal) + { + if (isRightAlign) + { + vPos.X = runningRightX - dim.Width; + vPos.Y = baseY; + runningRightX -= (dim.Width + spacing); + } + else + { + vPos.X = runningLeftX; + vPos.Y = baseY; + runningLeftX += (dim.Width + spacing); + } + } + else + { + if (isRightAlign) + vPos.X = baseX - dim.Width; + else if (isCenterAlign) + vPos.X = baseX - (dim.Width / 2); + else + vPos.X = baseX; + + vPos.Y = isBottomAnchor ? (baseY - stackIndex * spacing) : (baseY + stackIndex * spacing); + ++stackIndex; + } + } + + if (isSidebar) + { + auto const TextFlags = static_cast(static_cast(TextPrintType::UseGradPal | TextPrintType::Metal12) + | static_cast(effectiveAlign)); + pSurface->DrawText(counter, &vRect, &vPos, Drawing::RGB_To_Int(clrToolTip), 0, TextFlags); + } + else + { + Point2D loc { vPos.X, vPos.Y }; + pSurface->DrawText(counter, &loc, Drawing::RGB_To_Int(clrToolTip)); + } + } +} diff --git a/src/New/Type/ResourceTypeClass.h b/src/New/Type/ResourceTypeClass.h index 449baff0c5..09b5c5b63b 100644 --- a/src/New/Type/ResourceTypeClass.h +++ b/src/New/Type/ResourceTypeClass.h @@ -12,12 +12,17 @@ class ResourceTypeClass final : public Enumerable static Valueable Global_Display_Anchor; static Valueable Global_Display_BaseOffset; static Valueable Global_Display_Spacing; + static Nullable Global_Display_Align; + static PhobosPCXFile Global_Display_Background_PCX; + static Valueable Global_Display_Background_SHP; + static CustomPalette Global_Display_Background_Palette; + static Valueable Global_Display_Background_Offset; // Per-Resource Display Configuration Valueable Display_Label; Valueable Display_Label_InvertPosition; Valueable Display_Label_UseSpace; - Valueable Display_Color; + Nullable Display_Color; Valueable Display_Condition; Nullable Display_Offset; @@ -32,11 +37,26 @@ class ResourceTypeClass final : public Enumerable Valueable Bounty_CanUseStandardPoints; Valueable Bounty_MoneyConversion; + // Crates + Valueable Crate_Amount; + NullableIdx Crate_Sound; + NullableIdx Crate_Anim; + + // Pseudo-Resource Types (Money and Power) + Valueable Display_Power_Mode; + + bool IsMoneyResource() const { return _strcmpi(this->Name, "Money") == 0; } + bool IsPowerResource() const { return _strcmpi(this->Name, "Power") == 0; } + static bool HasMoneyResource(); + static bool HasPowerResource(); + static bool ShouldSkipWestwoodCredits(); + ResourceTypeClass(const char* pTitle = NONE_STR); virtual ~ResourceTypeClass() = default; void LoadFromINI(CCINIClass* pINI); static void LoadGlobalsFromINI(CCINIClass* pINI); + static void DrawResourceHUD(DSurface* pSurface, bool isSidebar); void LoadFromStream(PhobosStreamReader& Stm); void SaveToStream(PhobosStreamWriter& Stm); diff --git a/src/Phobos.cpp b/src/Phobos.cpp index e019239820..c8b9fb3c9f 100644 --- a/src/Phobos.cpp +++ b/src/Phobos.cpp @@ -12,6 +12,7 @@ #include "Utilities/Parser.h" #include +#include #ifdef TESTING_BUILD bool HideWarning = false; @@ -340,6 +341,8 @@ DEFINE_HOOK(0x4F4583, GScreenClass_DrawText, 0x6) } #endif // !RELEASE + ResourceTypeClass::DrawResourceHUD(DSurface::Composite, false); + if (!Phobos::Config::ShowGameTime || !RulesExt::Global()->ShowGameTime || HouseClass::CurrentPlayer->IsObserver()) // already has a timer return 0; diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h index 948e7124c9..6ba5324a9d 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -468,12 +468,24 @@ enum class ResourceDisplayOrientation : unsigned char Horizontal = 1 }; +enum class ResourcePowerDisplayMode : unsigned char +{ + NetAndTotal = 0, + Net = 1, + DrainAndTotal = 2, + Drain = 3, + Total = 4 +}; + + enum class ResourceDisplayAnchor : unsigned char { TopRight = 0, TopLeft = 1, BottomRight = 2, BottomLeft = 3, - Sidebar = 4 + Sidebar = 4, + TopCenter = 5, + BottomCenter = 6 }; diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index a3ca2875cb..415cbdba7f 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -1170,6 +1170,42 @@ namespace detail return false; } + template <> + inline bool read(ResourcePowerDisplayMode& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + if (_strcmpi(str, "netandtotal") == 0 || _strcmpi(str, "net/total") == 0 || _strcmpi(str, "surplus/total") == 0 || _strcmpi(str, "nettotal") == 0) + { + value = ResourcePowerDisplayMode::NetAndTotal; + return true; + } + else if (_strcmpi(str, "net") == 0 || _strcmpi(str, "delta") == 0 || _strcmpi(str, "surplus") == 0 || _strcmpi(str, "remaining") == 0) + { + value = ResourcePowerDisplayMode::Net; + return true; + } + else if (_strcmpi(str, "drainandtotal") == 0 || _strcmpi(str, "drain/total") == 0 || _strcmpi(str, "used/total") == 0 || _strcmpi(str, "demand/total") == 0 || _strcmpi(str, "usedandtotal") == 0) + { + value = ResourcePowerDisplayMode::DrainAndTotal; + return true; + } + else if (_strcmpi(str, "drain") == 0 || _strcmpi(str, "used") == 0 || _strcmpi(str, "demand") == 0 || _strcmpi(str, "consumption") == 0) + { + value = ResourcePowerDisplayMode::Drain; + return true; + } + else if (_strcmpi(str, "total") == 0 || _strcmpi(str, "output") == 0 || _strcmpi(str, "production") == 0) + { + value = ResourcePowerDisplayMode::Total; + return true; + } + Debug::INIParseFailed(pSection, pKey, parser.value(), "Power display mode can be NetAndTotal, Net, DrainAndTotal, Drain, or Total"); + } + return false; + } + template <> inline bool read(ResourceDisplayAnchor& value, INI_EX& parser, const char* pSection, const char* pKey) { @@ -1201,7 +1237,17 @@ namespace detail value = ResourceDisplayAnchor::Sidebar; return true; } - Debug::INIParseFailed(pSection, pKey, parser.value(), "Resource display anchor can be TopRight, TopLeft, BottomRight, BottomLeft or Sidebar"); + else if (_strcmpi(str, "topcenter") == 0 || _strcmpi(str, "topcentre") == 0) + { + value = ResourceDisplayAnchor::TopCenter; + return true; + } + else if (_strcmpi(str, "bottomcenter") == 0 || _strcmpi(str, "bottomcentre") == 0) + { + value = ResourceDisplayAnchor::BottomCenter; + return true; + } + Debug::INIParseFailed(pSection, pKey, parser.value(), "Resource display anchor can be TopRight, TopLeft, TopCenter, BottomRight, BottomLeft, BottomCenter or Sidebar"); } return false; } From 3b8c6c6b53d7e11e82dd52a33ca742d8b6e695f3 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 27 Aug 2026 13:54:20 +0200 Subject: [PATCH 17/19] Horizontal bar improvements and Weeds & harvesters support --- docs/New-or-Enhanced-Logics.md | 72 ++++- src/Ext/Rules/Body.cpp | 15 +- src/Ext/Side/Body.cpp | 32 ++ src/Ext/Side/Body.h | 20 ++ src/Misc/Hooks.UI.cpp | 4 +- src/New/Type/ResourceTypeClass.cpp | 493 +++++++++++++++++++++-------- src/New/Type/ResourceTypeClass.h | 27 +- src/Utilities/Enum.h | 7 + src/Utilities/TemplateDef.h | 41 ++- 9 files changed, 547 insertions(+), 164 deletions(-) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index cb48380b20..a2e4c35680 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -292,6 +292,7 @@ Similarly, unless you really have a need, `UseGlobalRadApplicationDelay` should - `Display.ResourceTypes.Background` optional SHP background image rendered behind the tactical HUD counters (without `.shp` extension; multi-frame SHPs animate continuously). - `Display.ResourceTypes.Background.Offset` optional X,Y pixel offset for the background image (applies to both PCX and SHP). - `Display.ResourceTypes.Background.Palette` optional palette for SHP background. + - `Display.ResourceTypes.Background.Horizontal.ResourcesInside` when true and `Display.ResourceTypes.Orientation=Horizontal`, dynamically divides the background graphic's width proportionally among all currently visible resources so they stay nicely framed inside the background without overflowing (defaults to `false`). - `Display.Label` specifies the CSF label key displayed alongside the counter. - `Display.Label.InvertPosition` when set to true, places the label after the value (`100 ★` vs `★ 100`). - `Display.Label.UseSpace` when set to false, omits the separating space between label and amount (`100★` or `★100` vs `100 ★` / `★ 100`). Defaults to `true`. @@ -319,31 +320,50 @@ Similarly, unless you really have a need, `UseGlobalRadApplicationDelay` should - `ResourceType..Amount` specifies resource cost (positive) or grant (negative) upon firing. - `ResourceType` and `ResourceValue` configure Tiberium/Ore harvesting to yield custom resources. When `ResourceValue=` is omitted, `Value=` is used for the custom resource and no money is granted (replaces money). When `ResourceValue=` is defined, `ResourceValue` is used for the custom resource AND `Value=` is used to grant standard money (dual income). - `Sidebar.ResourceTypes.Types` list of resources to show on this side's sidebar (if omitted, all non-hidden resources are shown). -- `[ResourceTypes]` also supports **`Money`** and **`Power`** pseudo-resources. Simply declare them in the list (e.g. `0=Money`, `1=Power`) to customize their HUD display (labels, positioning, formatting, or moving them out of the sidebar to the tactical screen). +- `[ResourceTypes]` also supports **`Money`**, **`Power`**, **`Harvesters`**, and **`Weeds`** pseudo-resources. Simply declare them in the list (e.g. `0=Money`, `1=Power`, `2=Harvesters`, `3=Weeds`) to customize their HUD display (labels, positioning, formatting, status colors, or moving them between the sidebar and the tactical screen). - When `Money` is declared, it automatically tracks player credits and replaces the default sidebar counter with the Resource HUD. It also supports `Production.Money` and `ResourceCollector.Money`. - - `Display.Power.Format` controls the format of the displayed power value (`NetAndTotal`, `Net`, `DrainAndTotal`, `Drain`, `Total`). + - `Display.Power.Format` controls the format of the displayed power value (`DrainAndTotal`, `Drain`, `NetAndTotal`, `Net`, `Total`; defaults to `NetAndTotal`). + - `Display.Power.ColorGreen`, `Display.Power.ColorYellow`, `Display.Power.ColorRed`, `Display.Power.ColorGrey` (or `Display.ColorGreen`, `ColorYellow`, `ColorRed`, `ColorGrey`) optional R,G,B text colors for power states: + - `ColorGreen`: normal surplus power (defaults to `0,255,0`). + - `ColorYellow`: high drain / low power (defaults to `255,255,0`). + - `ColorRed`: power deficit / low power offline (defaults to `255,0,0`). + - `ColorGrey`: blackout active (defaults to `128,128,128`). + - Note: If `Display.Color` is defined instead, it forces a single fixed static color regardless of power status. + - When `Harvesters` is declared, it automatically tracks active and total harvesters and replaces the legacy sidebar harvester counter. + - `Display.Harvesters.Format` controls the format of the displayed harvester counter (`ActiveAndTotal`, `Active`, `Total`; defaults to `ActiveAndTotal`). + - `Display.Harvesters.ColorGreen`, `Display.Harvesters.ColorYellow`, `Display.Harvesters.ColorRed` (or `Display.ColorGreen`, `ColorYellow`, `ColorRed`) optional R,G,B text colors for harvester activity states: + - `ColorGreen`: all harvesters are actively harvesting or unloading (defaults to `0,255,0`). + - `ColorYellow`: some harvesters are active while at least one is idle (defaults to `255,255,0`). + - `ColorRed`: no harvesters are active / all idle (defaults to `255,0,0`). + - Note: If `Display.Color` is defined instead, it forces a single fixed static color regardless of harvester activity. + - When `Weeds` is declared, it automatically tracks the player's stored Veinhole monster Tiberium / Weeds (`pPlayer->OwnedWeed.GetTotalAmount()`) and replaces the legacy sidebar weeds counter. In `rulesmd.ini`: ```ini [ResourceTypes] 0=SOMERESOURCETYPE -[AudioVisual] +[GDI] ; SideClass Display.ResourceTypes.Orientation=Vertical ; ResourceDisplayOrientation (Vertical | Horizontal) Display.ResourceTypes.Anchor=TopRight ; ResourceDisplayAnchor (TopRight | TopLeft | TopCenter | BottomRight | BottomLeft | BottomCenter | Sidebar) Display.ResourceTypes.BaseOffset=0,0 ; Point2D - X,Y Display.ResourceTypes.Spacing=14 ; integer - pixels Display.ResourceTypes.Align= ; TextAlign (Left | Center | Right, default depends on Anchor) -Display.ResourceTypes.BackgroundPCX= ; filename - .pcx -Display.ResourceTypes.Background= ; filename - without .shp extension +Display.ResourceTypes.BackgroundPCX= ; filename - .pcx (hidden automatically if no tactical resources are visible) +Display.ResourceTypes.Background= ; filename - without .shp extension (hidden automatically if no tactical resources are visible) Display.ResourceTypes.Background.Offset=0,0 ; Point2D - X,Y (applies to both PCX and SHP) Display.ResourceTypes.Background.Palette= ; CustomPalette (palette for SHP background) +Display.ResourceTypes.Background.Horizontal.ResourcesInside=false ; boolean +Sidebar.ResourceTypes.Types= ; list of ResourceTypes +Sidebar.ResourceTypes.Offset=0,0 ; Point2D - X,Y +Sidebar.ResourceTypes.Align=Left ; TextAlign (Left | Center | Right) +Sidebar.ResourceTypes.Color= ; integer - Red,Green,Blue [SOMERESOURCETYPE] ; ResourceType Display.Label= ; CSF entry key Display.Label.InvertPosition=false ; boolean Display.Label.UseSpace=true ; boolean -Display.Color=255,255,255 ; integer - Red,Green,Blue +Display.Color=255,255,255 ; integer - Red,Green,Blue (static color) Display.Condition=Always ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) Display.Offset= ; Point2D - X,Y InitialValue=0 ; integer @@ -358,22 +378,46 @@ Crate.Anim= ; AnimationType Crate.Sound= ; Sound [Money] ; Money Pseudo-Resource (tracks player credits) -Display.Label= ; CSF entry key, default to [ToolTips] -> CostLabel from uimd.ini ($) +Display.Label= ; CSF entry key (defaults to $) Display.Label.InvertPosition=false ; boolean Display.Label.UseSpace=false ; boolean -Display.Color=255,255,0 ; integer - Red,Green,Blue +Display.Color=255,255,0 ; integer - Red,Green,Blue (static color) Display.Condition=Always ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) Display.Offset= ; Point2D - X,Y RequiresCollector=false ; boolean [Power] ; Power Pseudo-Resource (tracks power status) -Display.Label= ; CSF entry key, default to [ToolTips] -> PowerLabel from uimd.ini (⚡) +Display.Label= ; CSF entry key (defaults to ⚡) Display.Label.InvertPosition=false ; boolean Display.Label.UseSpace=false ; boolean -Display.Color= ; integer - Red,Green,Blue (uses dynamic grid colors if omitted) -Display.Condition=Never ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) +Display.Condition=Always ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) Display.Offset= ; Point2D - X,Y Display.Power.Format=NetAndTotal ; ResourcePowerDisplayMode (NetAndTotal | Net | DrainAndTotal | Drain | Total) +Display.Power.ColorGreen=0,255,0 ; integer - Red,Green,Blue (normal power surplus, default 0,255,0) +Display.Power.ColorYellow=255,255,0 ; integer - Red,Green,Blue (low power / high drain, default 255,255,0) +Display.Power.ColorRed=255,0,0 ; integer - Red,Green,Blue (power deficit / offline, default 255,0,0) +Display.Power.ColorGrey=128,128,128 ; integer - Red,Green,Blue (blackout status, default 128,128,128) +Display.Color= ; integer - Red,Green,Blue (forces a single static color if set, overriding status colors) + +[Harvesters] ; Harvesters Pseudo-Resource (tracks active and total harvesters) +Display.Label= ; CSF entry key (defaults to ⛏) +Display.Label.InvertPosition=false ; boolean +Display.Label.UseSpace=false ; boolean +Display.Condition=Never ; ResourceDisplayCondition (Always | GreaterThanZero | Never; HasCollector is ignored) +Display.Offset= ; Point2D - X,Y +Display.Harvesters.Format=ActiveAndTotal ; ResourceHarvesterDisplayMode (ActiveAndTotal | Active | Total) +Display.Harvesters.ColorGreen=0,255,0 ; integer - Red,Green,Blue (100% harvesters active, default 0,255,0) +Display.Harvesters.ColorYellow=255,255,0 ; integer - Red,Green,Blue (some harvesters active, default 255,255,0) +Display.Harvesters.ColorRed=255,0,0 ; integer - Red,Green,Blue (no harvesters active, default 255,0,0) +Display.Color= ; integer - Red,Green,Blue (forces a single static color if set, overriding status colors) + +[Weeds] ; Weeds Pseudo-Resource (tracks stored Veinhole monster Tiberium / Weeds) +Display.Label= ; CSF entry key +Display.Label.InvertPosition=false ; boolean +Display.Label.UseSpace=false ; boolean +Display.Color=0,255,128 ; integer - Red,Green,Blue (static color) +Display.Condition=Never ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) +Display.Offset= ; Point2D - X,Y [SOMETECHNO] ; TechnoType Cost.=0 ; integer @@ -393,12 +437,6 @@ ResourceValue= ; integer, default to Value [SOMESUPERWEAPON] ; SuperWeaponType ResourceType..Amount=0 ; integer - -[SOMESIDE] ; SideClass -Sidebar.ResourceTypes.Types= ; List of ResourceTypes -Sidebar.ResourceTypes.Offset=0,0 ; Point2D - X,Y -Sidebar.ResourceTypes.Color= ; integer - Red,Green,Blue -Sidebar.ResourceTypes.Align=Left ; TextAlign (Left | Right | Center) ``` ### Laser Trails diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 7168485162..f8c4b649a7 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -12,6 +12,7 @@ #include #include #include +#include std::unique_ptr RulesExt::Data = nullptr; @@ -33,7 +34,19 @@ void RulesExt::LoadFromINIFile(RulesClass* pThis, CCINIClass* pINI) void RulesExt::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) { ResourceTypeClass::LoadFromINIList(pINI); - ResourceTypeClass::LoadGlobalsFromINI(pINI); + + // Re-read Sidebar.ResourceTypes.Types for all sides now that ResourceTypes are registered + { + INI_EX exINI(pINI); + for (const auto pSide : SideClass::Array) + { + if (auto pSideExt = SideExt::TryFetch(pSide)) + { + pSideExt->Sidebar_ResourceTypes_Types.Read(exINI, pSide->ID, "Sidebar.ResourceTypes.Types"); + } + } + } + DigitalDisplayTypeClass::LoadFromINIList(pINI); SelectBoxTypeClass::LoadFromINIList(pINI); RadTypeClass::LoadFromINIList(pINI); diff --git a/src/Ext/Side/Body.cpp b/src/Ext/Side/Body.cpp index 2ec456b45e..f867f82389 100644 --- a/src/Ext/Side/Body.cpp +++ b/src/Ext/Side/Body.cpp @@ -60,6 +60,28 @@ void SideExt::LoadFromINIFile(CCINIClass* pINI) this->SuperWeaponSidebar_TopPCX.Read(pINI, pSection, "SuperWeaponSidebar.TopPCX"); this->SuperWeaponSidebar_CenterPCX.Read(pINI, pSection, "SuperWeaponSidebar.CenterPCX"); this->SuperWeaponSidebar_BottomPCX.Read(pINI, pSection, "SuperWeaponSidebar.BottomPCX"); + + this->Display_ResourceTypes_Orientation.Read(exINI, pSection, "Display.ResourceTypes.Orientation"); + this->Display_ResourceTypes_Anchor.Read(exINI, pSection, "Display.ResourceTypes.Anchor"); + this->Display_ResourceTypes_BaseOffset.Read(exINI, pSection, "Display.ResourceTypes.BaseOffset"); + this->Display_ResourceTypes_Spacing.Read(exINI, pSection, "Display.ResourceTypes.Spacing"); + this->Display_ResourceTypes_Align.Read(exINI, pSection, "Display.ResourceTypes.Align"); + + this->Display_ResourceTypes_Background_PCX.Read(pINI, pSection, "Display.ResourceTypes.BackgroundPCX"); + this->Display_ResourceTypes_Background_PCX.Read(pINI, pSection, "Display.ResourceTypes.Background.PCX"); + + this->Display_ResourceTypes_Background_SHP.Read(exINI, pSection, "Display.ResourceTypes.Background"); + this->Display_ResourceTypes_Background_SHP.Read(exINI, pSection, "Display.ResourceTypes.Background.SHP"); + this->Display_ResourceTypes_Background_SHP.Read(exINI, pSection, "Display.ResourceTypes.Background.Shape"); + + this->Display_ResourceTypes_Background_Offset.Read(exINI, pSection, "Display.ResourceTypes.Background.Offset"); + this->Display_ResourceTypes_Background_Offset.Read(exINI, pSection, "Display.ResourceTypes.BackgroundOffset"); + this->Display_ResourceTypes_Background_Offset.Read(exINI, pSection, "Display.ResourceTypes.BackgroundPCX.Offset"); + this->Display_ResourceTypes_Background_Offset.Read(exINI, pSection, "Display.ResourceTypes.BackgroundPCXOffset"); + this->Display_ResourceTypes_Background_Palette.LoadFromINI(pINI, pSection, "Display.ResourceTypes.Background.Palette"); + this->Display_ResourceTypes_Background_Palette.LoadFromINI(pINI, pSection, "Display.ResourceTypes.Background.SHP.Palette"); + + this->Display_ResourceTypes_Background_Horizontal_ResourcesInside.Read(exINI, pSection, "Display.ResourceTypes.Background.Horizontal.ResourcesInside"); } // ============= @@ -102,6 +124,16 @@ void SideExt::Serialize(T& Stm) .Process(this->SuperWeaponSidebar_TopPCX) .Process(this->SuperWeaponSidebar_CenterPCX) .Process(this->SuperWeaponSidebar_BottomPCX) + .Process(this->Display_ResourceTypes_Orientation) + .Process(this->Display_ResourceTypes_Anchor) + .Process(this->Display_ResourceTypes_BaseOffset) + .Process(this->Display_ResourceTypes_Spacing) + .Process(this->Display_ResourceTypes_Align) + .Process(this->Display_ResourceTypes_Background_PCX) + .Process(this->Display_ResourceTypes_Background_SHP) + .Process(this->Display_ResourceTypes_Background_Palette) + .Process(this->Display_ResourceTypes_Background_Offset) + .Process(this->Display_ResourceTypes_Background_Horizontal_ResourcesInside) ; } diff --git a/src/Ext/Side/Body.h b/src/Ext/Side/Body.h index 32f65dabc5..6f8bb270ad 100644 --- a/src/Ext/Side/Body.h +++ b/src/Ext/Side/Body.h @@ -56,6 +56,16 @@ class SideExt final : public AbstractTypeExt PhobosPCXFile SuperWeaponSidebar_TopPCX; PhobosPCXFile SuperWeaponSidebar_CenterPCX; PhobosPCXFile SuperWeaponSidebar_BottomPCX; + Valueable Display_ResourceTypes_Orientation; + Valueable Display_ResourceTypes_Anchor; + Valueable Display_ResourceTypes_BaseOffset; + Valueable Display_ResourceTypes_Spacing; + Nullable Display_ResourceTypes_Align; + PhobosPCXFile Display_ResourceTypes_Background_PCX; + Valueable Display_ResourceTypes_Background_SHP; + CustomPalette Display_ResourceTypes_Background_Palette; + Valueable Display_ResourceTypes_Background_Offset; + Valueable Display_ResourceTypes_Background_Horizontal_ResourcesInside; SideExt(SideClass* OwnerObject) : AbstractTypeExt(OwnerObject) , ArrayIndex { -1 } @@ -91,6 +101,16 @@ class SideExt final : public AbstractTypeExt , SuperWeaponSidebar_TopPCX {} , SuperWeaponSidebar_CenterPCX {} , SuperWeaponSidebar_BottomPCX {} + , Display_ResourceTypes_Orientation { ResourceDisplayOrientation::Vertical } + , Display_ResourceTypes_Anchor { ResourceDisplayAnchor::TopRight } + , Display_ResourceTypes_BaseOffset { { 0, 0 } } + , Display_ResourceTypes_Spacing { 14 } + , Display_ResourceTypes_Align {} + , Display_ResourceTypes_Background_PCX {} + , Display_ResourceTypes_Background_SHP { nullptr } + , Display_ResourceTypes_Background_Palette {} + , Display_ResourceTypes_Background_Offset { { 0, 0 } } + , Display_ResourceTypes_Background_Horizontal_ResourcesInside { false } { } virtual ~SideExt() = default; diff --git a/src/Misc/Hooks.UI.cpp b/src/Misc/Hooks.UI.cpp index 43380724b6..f634776b08 100644 --- a/src/Misc/Hooks.UI.cpp +++ b/src/Misc/Hooks.UI.cpp @@ -129,7 +129,7 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) // Draw custom resources in sidebar if Anchor=Sidebar ResourceTypeClass::DrawResourceHUD(DSurface::Sidebar, true); - if (Phobos::UI::HarvesterCounter_Show && Phobos::Config::ShowHarvesterCounter) + if (Phobos::UI::HarvesterCounter_Show && Phobos::Config::ShowHarvesterCounter && !ResourceTypeClass::HasHarvesterResource()) { wchar_t counter[0x20]; const int nActive = HouseExt::ActiveHarvesterCount(pPlayer); @@ -193,7 +193,7 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) DSurface::Sidebar->DrawText(counter, &vRect, &vPos, Drawing::RGB_To_Int(clrToolTip), 0, TextFlags); } - if (Phobos::UI::WeedsCounter_Show && Phobos::Config::ShowWeedsCounter) + if (Phobos::UI::WeedsCounter_Show && Phobos::Config::ShowWeedsCounter && !ResourceTypeClass::HasWeedsResource()) { wchar_t counter[0x20]; const ColorStruct clrToolTip = pSideExt->Sidebar_WeedsCounter_Color.Get(Drawing::TooltipColor); diff --git a/src/New/Type/ResourceTypeClass.cpp b/src/New/Type/ResourceTypeClass.cpp index ed50eecfed..9bd7438603 100644 --- a/src/New/Type/ResourceTypeClass.cpp +++ b/src/New/Type/ResourceTypeClass.cpp @@ -15,22 +15,12 @@ const char* Enumerable::GetMainSection() return "ResourceTypes"; } -Valueable ResourceTypeClass::Global_Display_Orientation { ResourceDisplayOrientation::Vertical }; -Valueable ResourceTypeClass::Global_Display_Anchor { ResourceDisplayAnchor::TopRight }; -Valueable ResourceTypeClass::Global_Display_BaseOffset { Point2D::Empty }; -Valueable ResourceTypeClass::Global_Display_Spacing { 14 }; -Nullable ResourceTypeClass::Global_Display_Align {}; -PhobosPCXFile ResourceTypeClass::Global_Display_Background_PCX {}; -Valueable ResourceTypeClass::Global_Display_Background_SHP { nullptr }; -CustomPalette ResourceTypeClass::Global_Display_Background_Palette {}; -Valueable ResourceTypeClass::Global_Display_Background_Offset { Point2D::Empty }; - ResourceTypeClass::ResourceTypeClass(const char* pTitle) : Enumerable(pTitle) , Display_Label {} , Display_Label_InvertPosition { false } - , Display_Label_UseSpace { pTitle ? (_strcmpi(pTitle, "Money") != 0 && _strcmpi(pTitle, "Power") != 0) : true } + , Display_Label_UseSpace { pTitle ? (_strcmpi(pTitle, "Money") != 0 && _strcmpi(pTitle, "Power") != 0 && _strcmpi(pTitle, "Harvesters") != 0 && _strcmpi(pTitle, "Weeds") != 0) : true } , Display_Color {} - , Display_Condition { (pTitle && _strcmpi(pTitle, "Power") == 0) ? ResourceDisplayCondition::Never : ResourceDisplayCondition::Always } + , Display_Condition { (pTitle && (_strcmpi(pTitle, "Harvesters") == 0 || _strcmpi(pTitle, "Weeds") == 0)) ? ResourceDisplayCondition::Never : ResourceDisplayCondition::Always } , Display_Offset {} , InitialValue { 0 } , RequiresCollector { false } @@ -38,11 +28,19 @@ ResourceTypeClass::ResourceTypeClass(const char* pTitle) : EnumerableIsHarvesterResource()) + return true; + } + return false; +} + +bool ResourceTypeClass::HasWeedsResource() +{ + for (const auto& pResource : ResourceTypeClass::Array) + { + if (pResource && pResource->IsWeedsResource()) + return true; + } + return false; +} + bool ResourceTypeClass::ShouldSkipWestwoodCredits() { if (!ResourceTypeClass::HasMoneyResource()) return false; - const auto globalAnchor = ResourceTypeClass::Global_Display_Anchor.Get(); - if (globalAnchor != ResourceDisplayAnchor::Sidebar) - return true; - auto const pPlayer = HouseClass::CurrentPlayer; if (!pPlayer || pPlayer->Defeated) return false; auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); - if (pSideExt && !pSideExt->Sidebar_ResourceTypes_Types.empty()) + if (!pSideExt) + return false; + + const bool hasExplicitSidebarTypes = !pSideExt->Sidebar_ResourceTypes_Types.empty(); + const auto sideAnchor = pSideExt->Display_ResourceTypes_Anchor.Get(); + + if (hasExplicitSidebarTypes) { for (const auto idx : pSideExt->Sidebar_ResourceTypes_Types) { @@ -90,49 +110,20 @@ bool ResourceTypeClass::ShouldSkipWestwoodCredits() return true; } } + return (sideAnchor != ResourceDisplayAnchor::Sidebar); } - return false; -} - -void ResourceTypeClass::LoadGlobalsFromINI(CCINIClass* pINI) -{ - INI_EX exINI(pINI); - - Global_Display_Orientation.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Orientation"); - Global_Display_Anchor.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Anchor"); - Global_Display_BaseOffset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BaseOffset"); - Global_Display_Spacing.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Spacing"); - Global_Display_Align.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Align"); - - Global_Display_Background_PCX.Read(pINI, GameStrings::AudioVisual, "Display.ResourceTypes.BackgroundPCX"); - Global_Display_Background_PCX.Read(pINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.PCX"); - - Global_Display_Background_SHP.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background"); - Global_Display_Background_SHP.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.SHP"); - Global_Display_Background_SHP.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.Shape"); - - Global_Display_Background_Offset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.Offset"); - Global_Display_Background_Offset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BackgroundOffset"); - Global_Display_Background_Offset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BackgroundPCX.Offset"); - Global_Display_Background_Offset.Read(exINI, GameStrings::AudioVisual, "Display.ResourceTypes.BackgroundPCXOffset"); - Global_Display_Background_Palette.LoadFromINI(pINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.Palette"); - Global_Display_Background_Palette.LoadFromINI(pINI, GameStrings::AudioVisual, "Display.ResourceTypes.Background.SHP.Palette"); + return (sideAnchor != ResourceDisplayAnchor::Sidebar); } void ResourceTypeClass::LoadFromINI(CCINIClass* pINI) { const char* section = this->Name; - if (this->IsMoneyResource() || this->IsPowerResource()) + if (this->IsMoneyResource() || this->IsPowerResource() || this->IsHarvesterResource() || this->IsWeedsResource()) { this->Display_Label_UseSpace = false; } - if (this->IsPowerResource()) - { - this->Display_Condition = ResourceDisplayCondition::Never; - } - if (!pINI->GetSection(section)) return; @@ -143,6 +134,10 @@ void ResourceTypeClass::LoadFromINI(CCINIClass* pINI) this->Display_Label_UseSpace.Read(exINI, section, "Display.Label.UseSpace"); this->Display_Color.Read(exINI, section, "Display.Color"); this->Display_Condition.Read(exINI, section, "Display.Condition"); + if (this->IsHarvesterResource() && this->Display_Condition.Get() == ResourceDisplayCondition::HasCollector) + { + this->Display_Condition = ResourceDisplayCondition::Always; + } this->Display_Offset.Read(exINI, section, "Display.Offset"); this->InitialValue.Read(exINI, section, "InitialValue"); @@ -161,6 +156,25 @@ void ResourceTypeClass::LoadFromINI(CCINIClass* pINI) this->Display_Power_Mode.Read(exINI, section, "Display.Power.Mode"); this->Display_Power_Mode.Read(exINI, section, "Display.Power.Format"); + this->Display_Power_ColorGreen.Read(exINI, section, "Display.Power.ColorGreen"); + this->Display_Power_ColorGreen.Read(exINI, section, "Display.ColorGreen"); + this->Display_Power_ColorYellow.Read(exINI, section, "Display.Power.ColorYellow"); + this->Display_Power_ColorYellow.Read(exINI, section, "Display.ColorYellow"); + this->Display_Power_ColorRed.Read(exINI, section, "Display.Power.ColorRed"); + this->Display_Power_ColorRed.Read(exINI, section, "Display.ColorRed"); + this->Display_Power_ColorGrey.Read(exINI, section, "Display.Power.ColorGrey"); + this->Display_Power_ColorGrey.Read(exINI, section, "Display.ColorGrey"); + + this->Display_Harvester_Mode.Read(exINI, section, "Display.Harvesters.Mode"); + this->Display_Harvester_Mode.Read(exINI, section, "Display.Harvesters.Format"); + + this->Display_Harvester_ColorGreen.Read(exINI, section, "Display.Harvesters.ColorGreen"); + this->Display_Harvester_ColorGreen.Read(exINI, section, "Display.ColorGreen"); + this->Display_Harvester_ColorYellow.Read(exINI, section, "Display.Harvesters.ColorYellow"); + this->Display_Harvester_ColorYellow.Read(exINI, section, "Display.ColorYellow"); + this->Display_Harvester_ColorRed.Read(exINI, section, "Display.Harvesters.ColorRed"); + this->Display_Harvester_ColorRed.Read(exINI, section, "Display.ColorRed"); + bool showTotal = true; if (exINI.ReadBool(section, "Display.Power.ShowTotal", &showTotal)) { @@ -188,7 +202,15 @@ void ResourceTypeClass::Serialize(T& Stm) .Process(this->Crate_Amount) .Process(this->Crate_Sound) .Process(this->Crate_Anim) - .Process(this->Display_Power_Mode); + .Process(this->Display_Power_Mode) + .Process(this->Display_Power_ColorGreen) + .Process(this->Display_Power_ColorYellow) + .Process(this->Display_Power_ColorRed) + .Process(this->Display_Power_ColorGrey) + .Process(this->Display_Harvester_Mode) + .Process(this->Display_Harvester_ColorGreen) + .Process(this->Display_Harvester_ColorYellow) + .Process(this->Display_Harvester_ColorRed); } void ResourceTypeClass::LoadFromStream(PhobosStreamReader& Stm) @@ -213,18 +235,21 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) auto pHouseExt = HouseExt::Fetch(pPlayer); auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); - const auto globalAnchor = ResourceTypeClass::Global_Display_Anchor.Get(); + const auto sideAnchor = pSideExt ? pSideExt->Display_ResourceTypes_Anchor.Get() : ResourceDisplayAnchor::TopRight; + const auto sideOrientation = pSideExt ? pSideExt->Display_ResourceTypes_Orientation.Get() : ResourceDisplayOrientation::Vertical; + const Point2D sideBaseOffset = pSideExt ? pSideExt->Display_ResourceTypes_BaseOffset.Get() : Point2D::Empty; + const int spacing = pSideExt ? pSideExt->Display_ResourceTypes_Spacing.Get() : 14; + const bool resourcesInside = pSideExt && pSideExt->Display_ResourceTypes_Background_Horizontal_ResourcesInside.Get(); + const bool hasExplicitSidebarTypes = pSideExt && !pSideExt->Sidebar_ResourceTypes_Types.empty(); - // If global anchor is Sidebar: only draw during sidebar pass. - // If global anchor is NOT Sidebar (TopLeft, TopRight, etc.): only draw during tactical game screen pass. - if (globalAnchor == ResourceDisplayAnchor::Sidebar) + if (isSidebar) { - if (!isSidebar) + if (sideAnchor != ResourceDisplayAnchor::Sidebar && !hasExplicitSidebarTypes) return; } else { - if (isSidebar) + if (sideAnchor == ResourceDisplayAnchor::Sidebar) return; } @@ -232,7 +257,6 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) int baseX = 0; int baseY = 0; - const Point2D baseOffset = ResourceTypeClass::Global_Display_BaseOffset.Get(); const int marginX = Phobos::Config::MessageDisplayInCenter ? 28 : 10; const int screenW = pSurface->GetWidth(); @@ -240,61 +264,67 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) if (isSidebar) { - baseX = 20 + pSideExt->Sidebar_ResourceTypes_Offset.Get().X + baseOffset.X; - baseY = 2 + pSideExt->Sidebar_ResourceTypes_Offset.Get().Y + baseOffset.Y; + int defaultBaseX = 20; + if (pSideExt->Sidebar_ResourceTypes_Align.Get() == TextAlign::Right) + defaultBaseX = 140; + else if (pSideExt->Sidebar_ResourceTypes_Align.Get() == TextAlign::Center) + defaultBaseX = pSurface->GetWidth() / 2; + + const Point2D sideOffset = pSideExt->Sidebar_ResourceTypes_Offset.Get(); + baseX = (sideOffset.X != 0 ? sideOffset.X : defaultBaseX) + (sideAnchor == ResourceDisplayAnchor::Sidebar ? sideBaseOffset.X : 0); + baseY = 2 + sideOffset.Y + (sideAnchor == ResourceDisplayAnchor::Sidebar ? sideBaseOffset.Y : 0); } else { - switch (globalAnchor) + switch (sideAnchor) { case ResourceDisplayAnchor::TopLeft: - baseX = marginX + baseOffset.X; - baseY = 5 + baseOffset.Y; + baseX = marginX + sideBaseOffset.X; + baseY = 5 + sideBaseOffset.Y; break; case ResourceDisplayAnchor::TopRight: - baseX = screenW - marginX + baseOffset.X; - baseY = 5 + baseOffset.Y; + baseX = screenW - marginX + sideBaseOffset.X; + baseY = 5 + sideBaseOffset.Y; break; case ResourceDisplayAnchor::TopCenter: - baseX = (screenW / 2) + baseOffset.X; - baseY = 5 + baseOffset.Y; + baseX = (screenW / 2) + sideBaseOffset.X; + baseY = 5 + sideBaseOffset.Y; break; case ResourceDisplayAnchor::BottomLeft: - baseX = marginX + baseOffset.X; - baseY = screenH - 20 + baseOffset.Y; + baseX = marginX + sideBaseOffset.X; + baseY = screenH - 20 + sideBaseOffset.Y; break; case ResourceDisplayAnchor::BottomRight: - baseX = screenW - marginX + baseOffset.X; - baseY = screenH - 20 + baseOffset.Y; + baseX = screenW - marginX + sideBaseOffset.X; + baseY = screenH - 20 + sideBaseOffset.Y; break; case ResourceDisplayAnchor::BottomCenter: - baseX = (screenW / 2) + baseOffset.X; - baseY = screenH - 20 + baseOffset.Y; + baseX = (screenW / 2) + sideBaseOffset.X; + baseY = screenH - 20 + sideBaseOffset.Y; break; default: - baseX = marginX + baseOffset.X; - baseY = 5 + baseOffset.Y; + baseX = marginX + sideBaseOffset.X; + baseY = 5 + sideBaseOffset.Y; break; } } - int const spacing = ResourceTypeClass::Global_Display_Spacing.Get(); int stackIndex = 0; int runningRightX = baseX; int runningLeftX = baseX; TextAlign effectiveAlign; - if (ResourceTypeClass::Global_Display_Align.isset()) + if (isSidebar) { - effectiveAlign = ResourceTypeClass::Global_Display_Align.Get(); + effectiveAlign = pSideExt ? pSideExt->Sidebar_ResourceTypes_Align.Get() : TextAlign::Left; } else { - if (isSidebar) - effectiveAlign = pSideExt->Sidebar_ResourceTypes_Align.Get(); - else if (globalAnchor == ResourceDisplayAnchor::TopCenter || globalAnchor == ResourceDisplayAnchor::BottomCenter) + if (pSideExt && pSideExt->Display_ResourceTypes_Align.isset()) + effectiveAlign = pSideExt->Display_ResourceTypes_Align.Get(); + else if (sideAnchor == ResourceDisplayAnchor::TopCenter || sideAnchor == ResourceDisplayAnchor::BottomCenter) effectiveAlign = TextAlign::Center; - else if (globalAnchor == ResourceDisplayAnchor::TopRight || globalAnchor == ResourceDisplayAnchor::BottomRight) + else if (sideAnchor == ResourceDisplayAnchor::TopRight || sideAnchor == ResourceDisplayAnchor::BottomRight) effectiveAlign = TextAlign::Right; else effectiveAlign = TextAlign::Left; @@ -302,49 +332,46 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) const bool isRightAlign = (effectiveAlign == TextAlign::Right); const bool isCenterAlign = (effectiveAlign == TextAlign::Center); - const bool isBottomAnchor = !isSidebar && (globalAnchor == ResourceDisplayAnchor::BottomLeft || globalAnchor == ResourceDisplayAnchor::BottomRight || globalAnchor == ResourceDisplayAnchor::BottomCenter); + const bool isRightAnchor = (sideAnchor == ResourceDisplayAnchor::TopRight || sideAnchor == ResourceDisplayAnchor::BottomRight); + const bool isCenterAnchor = (sideAnchor == ResourceDisplayAnchor::TopCenter || sideAnchor == ResourceDisplayAnchor::BottomCenter); + const bool isBottomAnchor = !isSidebar && (sideAnchor == ResourceDisplayAnchor::BottomLeft || sideAnchor == ResourceDisplayAnchor::BottomRight || sideAnchor == ResourceDisplayAnchor::BottomCenter); - const bool hasExplicitSidebarTypes = !pSideExt->Sidebar_ResourceTypes_Types.empty(); const size_t count = (isSidebar && hasExplicitSidebarTypes) ? pSideExt->Sidebar_ResourceTypes_Types.size() : ResourceTypeClass::Array.size(); + int bgWidth = 0; + int bgHeight = 0; + BSurface* pPCX = pSideExt ? pSideExt->Display_ResourceTypes_Background_PCX.GetSurface() : nullptr; + SHPStruct* pSHP = pSideExt ? pSideExt->Display_ResourceTypes_Background_SHP.Get() : nullptr; + + if (pPCX) + { + bgWidth = pPCX->GetWidth(); + bgHeight = pPCX->GetHeight(); + } + else if (pSHP) + { + bgWidth = pSHP->Width; + bgHeight = pSHP->Height; + } + + Point2D const bgOffset = pSideExt ? pSideExt->Display_ResourceTypes_Background_Offset.Get() : Point2D::Empty; + Point2D bgPos; + if (isRightAnchor) + bgPos.X = (baseX - bgWidth) + bgOffset.X; + else if (isCenterAnchor) + bgPos.X = (baseX - bgWidth / 2) + bgOffset.X; + else + bgPos.X = baseX + bgOffset.X; + + bgPos.Y = (isBottomAnchor ? (baseY - bgHeight) : baseY) + bgOffset.Y; + bool bgDrawn = false; auto DrawBackgroundOnce = [&]() { - if (bgDrawn || isSidebar) + if (bgDrawn || isSidebar || (bgWidth == 0 && bgHeight == 0) || !pSideExt) return; bgDrawn = true; - int bgWidth = 0; - int bgHeight = 0; - BSurface* pPCX = ResourceTypeClass::Global_Display_Background_PCX.GetSurface(); - SHPStruct* pSHP = ResourceTypeClass::Global_Display_Background_SHP.Get(); - - if (pPCX) - { - bgWidth = pPCX->GetWidth(); - bgHeight = pPCX->GetHeight(); - } - else if (pSHP) - { - bgWidth = pSHP->Width; - bgHeight = pSHP->Height; - } - else - { - return; - } - - Point2D const bgOffset = ResourceTypeClass::Global_Display_Background_Offset.Get(); - Point2D bgPos; - if (isRightAlign) - bgPos.X = (baseX - bgWidth) + bgOffset.X; - else if (isCenterAlign) - bgPos.X = (baseX - bgWidth / 2) + bgOffset.X; - else - bgPos.X = baseX + bgOffset.X; - - bgPos.Y = (isBottomAnchor ? (baseY - bgHeight) : baseY) + bgOffset.Y; - if (pPCX) { RectangleStruct drawRect { bgPos.X, bgPos.Y, bgWidth, bgHeight }; @@ -352,7 +379,7 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) } else if (pSHP) { - ConvertClass* pConvert = ResourceTypeClass::Global_Display_Background_Palette.GetOrDefaultConvert(FileSystem::PALETTE_PAL); + ConvertClass* pConvert = pSideExt->Display_ResourceTypes_Background_Palette.GetOrDefaultConvert(FileSystem::PALETTE_PAL); int const frame = pSHP->Frames > 1 ? (Unsorted::CurrentFrame % pSHP->Frames) : 0; pSurface->DrawSHP @@ -376,6 +403,10 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) } }; + std::vector visibleResourceIndices; + int dynamicSidebarCount = 0; + int dynamicTacticalCount = 0; + for (size_t iter = 0; iter < count; ++iter) { const int i = (isSidebar && hasExplicitSidebarTypes) ? pSideExt->Sidebar_ResourceTypes_Types[iter] : static_cast(iter); @@ -386,6 +417,22 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) if (!pResource || !pHouseExt->IsResourceEnabled(i)) continue; + // If drawing tactical HUD (!isSidebar), skip any resource that is already being drawn on the sidebar by this side's explicit list + if (!isSidebar && hasExplicitSidebarTypes) + { + bool alreadyInSidebar = false; + for (const auto sIdx : pSideExt->Sidebar_ResourceTypes_Types) + { + if (sIdx == i) + { + alreadyInSidebar = true; + break; + } + } + if (alreadyInSidebar) + continue; + } + if (pResource->Display_Condition.Get() == ResourceDisplayCondition::Never) continue; @@ -398,6 +445,15 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) { hasCollector = pPlayer->Buildings.Count > 0; } + else if (pResource->IsHarvesterResource()) + { + hasCollector = HouseExt::TotalHarvesterCount(pPlayer) > 0; + } + else if (pResource->IsWeedsResource()) + { + const bool hasCustomCollector = (i < static_cast(pHouseExt->ResourceCollectorCounts.size())) && (pHouseExt->ResourceCollectorCounts[i] > 0); + hasCollector = hasCustomCollector || (pPlayer->OwnedWeed.GetTotalAmount() > 0) || (HouseExt::ActiveHarvesterCount(pPlayer) > 0); + } else if (pResource->IsMoneyResource()) { const bool hasCustomCollector = (i < static_cast(pHouseExt->ResourceCollectorCounts.size())) && (pHouseExt->ResourceCollectorCounts[i] > 0); @@ -417,6 +473,16 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) if ((pPlayer->PowerOutput - pPlayer->PowerDrain) <= 0) continue; } + else if (pResource->IsHarvesterResource()) + { + if (HouseExt::TotalHarvesterCount(pPlayer) <= 0) + continue; + } + else if (pResource->IsWeedsResource()) + { + if (pPlayer->OwnedWeed.GetTotalAmount() <= 0) + continue; + } else if (pHouseExt->GetResourceAmount(i) <= 0) { continue; @@ -427,6 +493,26 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) continue; } + visibleResourceIndices.push_back(i); + if (isSidebar) + { + if (!pResource->Display_Offset.isset()) + ++dynamicSidebarCount; + } + else + { + if (!pResource->Display_Offset.isset()) + ++dynamicTacticalCount; + } + } + + int dynamicSidebarIdx = 0; + int dynamicTacticalIdx = 0; + + for (const int i : visibleResourceIndices) + { + const auto pResource = ResourceTypeClass::Array[i].get(); + DrawBackgroundOnce(); wchar_t counter[0x40]; @@ -436,7 +522,7 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) if (pResource->IsPowerResource()) { if (!label || !*label) - label = Phobos::UI::PowerLabel; + label = L"\u26a1"; const int output = pPlayer->PowerOutput; const int drain = pPlayer->PowerDrain; @@ -475,7 +561,12 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) wcscpy_s(counter, powerVal); } - if (pResource->Display_Color.isset()) + const bool hasSpecificPowerColors = pResource->Display_Power_ColorGreen.isset() + || pResource->Display_Power_ColorYellow.isset() + || pResource->Display_Power_ColorRed.isset() + || pResource->Display_Power_ColorGrey.isset(); + + if (pResource->Display_Color.isset() && !hasSpecificPowerColors) { clrToolTip = pResource->Display_Color.Get(); } @@ -483,7 +574,7 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) { if (pPlayer->PowerBlackoutTimer.InProgress()) { - clrToolTip = pSideExt->Sidebar_PowerDelta_ColorGrey.Get(); + clrToolTip = pResource->Display_Power_ColorGrey.Get(ColorStruct { 128, 128, 128 }); } else { @@ -491,18 +582,103 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) ? (double)drain / (double)output : (drain != 0 ? Phobos::UI::PowerDelta_ConditionRed * 2.f : Phobos::UI::PowerDelta_ConditionYellow); - clrToolTip = percent < Phobos::UI::PowerDelta_ConditionYellow - ? pSideExt->Sidebar_PowerDelta_ColorGreen.Get() - : LESS_EQUAL(percent, Phobos::UI::PowerDelta_ConditionRed) - ? pSideExt->Sidebar_PowerDelta_ColorYellow.Get() - : pSideExt->Sidebar_PowerDelta_ColorRed.Get(); + if (percent < Phobos::UI::PowerDelta_ConditionYellow) + { + clrToolTip = pResource->Display_Power_ColorGreen.Get(ColorStruct { 0, 255, 0 }); + } + else if (LESS_EQUAL(percent, Phobos::UI::PowerDelta_ConditionRed)) + { + clrToolTip = pResource->Display_Power_ColorYellow.Get(ColorStruct { 255, 255, 0 }); + } + else + { + clrToolTip = pResource->Display_Power_ColorRed.Get(ColorStruct { 255, 0, 0 }); + } + } + } + } + else if (pResource->IsHarvesterResource()) + { + if (!label || !*label) + label = L"\u26cf"; + + const int nActive = HouseExt::ActiveHarvesterCount(pPlayer); + const int nTotal = HouseExt::TotalHarvesterCount(pPlayer); + const double nPercentage = nTotal == 0 ? 1.0 : (double)nActive / (double)nTotal; + + wchar_t harvesterVal[0x40]; + switch (pResource->Display_Harvester_Mode.Get()) + { + case ResourceHarvesterDisplayMode::ActiveAndTotal: + swprintf_s(harvesterVal, L"%d / %d", nActive, nTotal); + break; + case ResourceHarvesterDisplayMode::Active: + swprintf_s(harvesterVal, L"%d", nActive); + break; + case ResourceHarvesterDisplayMode::Total: + swprintf_s(harvesterVal, L"%d", nTotal); + break; + } + + if (label && *label) + { + const bool useSpace = pResource->Display_Label_UseSpace.Get(); + if (pResource->Display_Label_InvertPosition.Get()) + swprintf_s(counter, useSpace ? L"%ls %ls" : L"%ls%ls", harvesterVal, label); + else + swprintf_s(counter, useSpace ? L"%ls %ls" : L"%ls%ls", label, harvesterVal); + } + else + { + wcscpy_s(counter, harvesterVal); + } + + const bool hasSpecificHarvesterColors = pResource->Display_Harvester_ColorGreen.isset() + || pResource->Display_Harvester_ColorYellow.isset() + || pResource->Display_Harvester_ColorRed.isset(); + + if (pResource->Display_Color.isset() && !hasSpecificHarvesterColors) + { + clrToolTip = pResource->Display_Color.Get(); + } + else + { + if (nActive >= nTotal || nPercentage > Phobos::UI::HarvesterCounter_ConditionYellow) + { + clrToolTip = pResource->Display_Harvester_ColorGreen.Get(ColorStruct { 0, 255, 0 }); + } + else if (nPercentage > Phobos::UI::HarvesterCounter_ConditionRed) + { + clrToolTip = pResource->Display_Harvester_ColorYellow.Get(ColorStruct { 255, 255, 0 }); + } + else + { + clrToolTip = pResource->Display_Harvester_ColorRed.Get(ColorStruct { 255, 0, 0 }); } } } + else if (pResource->IsWeedsResource()) + { + const int weedAmount = static_cast(pPlayer->OwnedWeed.GetTotalAmount()); + if (label && *label) + { + const bool useSpace = pResource->Display_Label_UseSpace.Get(); + if (pResource->Display_Label_InvertPosition.Get()) + swprintf_s(counter, useSpace ? L"%d %ls" : L"%d%ls", weedAmount, label); + else + swprintf_s(counter, useSpace ? L"%ls %d" : L"%ls%d", label, weedAmount); + } + else + { + swprintf_s(counter, L"%d", weedAmount); + } + + clrToolTip = pResource->Display_Color.Get(pSideExt ? pSideExt->Sidebar_ResourceTypes_Color.Get(Drawing::TooltipColor) : Drawing::TooltipColor); + } else { if (pResource->IsMoneyResource() && (!label || !*label)) - label = Phobos::UI::CostLabel; + label = L"$"; const int amount = pHouseExt->GetResourceAmount(i); if (label && *label) @@ -526,11 +702,66 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) { vPos = pResource->Display_Offset.Get(); } + else if (isSidebar) + { + if (dynamicSidebarCount <= 1) + { + vPos.X = baseX; + vPos.Y = baseY; + } + else + { + int posX = 0; + if (effectiveAlign == TextAlign::Left) + posX = (dynamicSidebarIdx * screenW) / dynamicSidebarCount + 10; + else if (effectiveAlign == TextAlign::Right) + posX = ((dynamicSidebarIdx + 1) * screenW) / dynamicSidebarCount - 10; + else + posX = (2 * dynamicSidebarIdx + 1) * screenW / (2 * dynamicSidebarCount); + + const Point2D sideOffset = pSideExt->Sidebar_ResourceTypes_Offset.Get(); + vPos.X = posX + sideOffset.X; + vPos.Y = baseY; + ++dynamicSidebarIdx; + } + } + else if (bgWidth > 0 && resourcesInside && sideOrientation == ResourceDisplayOrientation::Horizontal) + { + auto const dim = Drawing::GetTextDimensions(counter, { 0, 0 }, 0, 2, 0); + + if (dynamicTacticalCount <= 1) + { + if (effectiveAlign == TextAlign::Left) + vPos.X = bgPos.X + 10; + else if (effectiveAlign == TextAlign::Right) + vPos.X = (bgPos.X + bgWidth) - dim.Width - 10; + else + vPos.X = bgPos.X + (bgWidth - dim.Width) / 2; + } + else + { + int posX = 0; + if (effectiveAlign == TextAlign::Left) + posX = bgPos.X + (dynamicTacticalIdx * bgWidth) / dynamicTacticalCount + 10; + else if (effectiveAlign == TextAlign::Right) + posX = bgPos.X + ((dynamicTacticalIdx + 1) * bgWidth) / dynamicTacticalCount - dim.Width - 10; + else + { + int slotCenter = bgPos.X + (2 * dynamicTacticalIdx + 1) * bgWidth / (2 * dynamicTacticalCount); + posX = slotCenter - (dim.Width / 2); + } + + vPos.X = posX; + ++dynamicTacticalIdx; + } + + vPos.Y = bgPos.Y + (bgHeight > dim.Height ? (bgHeight - dim.Height) / 2 : 0); + } else { auto const dim = Drawing::GetTextDimensions(counter, { 0, 0 }, 0, 2, 0); - if (ResourceTypeClass::Global_Display_Orientation.Get() == ResourceDisplayOrientation::Horizontal) + if (sideOrientation == ResourceDisplayOrientation::Horizontal) { if (isRightAlign) { @@ -538,6 +769,12 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) vPos.Y = baseY; runningRightX -= (dim.Width + spacing); } + else if (isCenterAlign) + { + vPos.X = runningLeftX - (dim.Width / 2); + vPos.Y = baseY; + runningLeftX += (dim.Width + spacing); + } else { vPos.X = runningLeftX; @@ -554,7 +791,7 @@ void ResourceTypeClass::DrawResourceHUD(DSurface* pSurface, bool isSidebar) else vPos.X = baseX; - vPos.Y = isBottomAnchor ? (baseY - stackIndex * spacing) : (baseY + stackIndex * spacing); + vPos.Y = isBottomAnchor ? (baseY - bgHeight > 0 ? baseY - stackIndex * spacing : baseY) : (baseY + stackIndex * spacing); ++stackIndex; } } diff --git a/src/New/Type/ResourceTypeClass.h b/src/New/Type/ResourceTypeClass.h index 09b5c5b63b..4f95ced71d 100644 --- a/src/New/Type/ResourceTypeClass.h +++ b/src/New/Type/ResourceTypeClass.h @@ -7,17 +7,6 @@ class ResourceTypeClass final : public Enumerable { public: - // Global HUD Configuration - static Valueable Global_Display_Orientation; - static Valueable Global_Display_Anchor; - static Valueable Global_Display_BaseOffset; - static Valueable Global_Display_Spacing; - static Nullable Global_Display_Align; - static PhobosPCXFile Global_Display_Background_PCX; - static Valueable Global_Display_Background_SHP; - static CustomPalette Global_Display_Background_Palette; - static Valueable Global_Display_Background_Offset; - // Per-Resource Display Configuration Valueable Display_Label; Valueable Display_Label_InvertPosition; @@ -42,20 +31,32 @@ class ResourceTypeClass final : public Enumerable NullableIdx Crate_Sound; NullableIdx Crate_Anim; - // Pseudo-Resource Types (Money and Power) + // Pseudo-Resource Types (Money, Power, and Harvesters) Valueable Display_Power_Mode; + Nullable Display_Power_ColorGreen; + Nullable Display_Power_ColorYellow; + Nullable Display_Power_ColorRed; + Nullable Display_Power_ColorGrey; + + Valueable Display_Harvester_Mode; + Nullable Display_Harvester_ColorGreen; + Nullable Display_Harvester_ColorYellow; + Nullable Display_Harvester_ColorRed; bool IsMoneyResource() const { return _strcmpi(this->Name, "Money") == 0; } bool IsPowerResource() const { return _strcmpi(this->Name, "Power") == 0; } + bool IsHarvesterResource() const { return _strcmpi(this->Name, "Harvesters") == 0; } + bool IsWeedsResource() const { return _strcmpi(this->Name, "Weeds") == 0; } static bool HasMoneyResource(); static bool HasPowerResource(); + static bool HasHarvesterResource(); + static bool HasWeedsResource(); static bool ShouldSkipWestwoodCredits(); ResourceTypeClass(const char* pTitle = NONE_STR); virtual ~ResourceTypeClass() = default; void LoadFromINI(CCINIClass* pINI); - static void LoadGlobalsFromINI(CCINIClass* pINI); static void DrawResourceHUD(DSurface* pSurface, bool isSidebar); void LoadFromStream(PhobosStreamReader& Stm); diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h index 6ba5324a9d..36ccd9c1af 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -477,6 +477,13 @@ enum class ResourcePowerDisplayMode : unsigned char Total = 4 }; +enum class ResourceHarvesterDisplayMode : unsigned char +{ + ActiveAndTotal = 0, + Active = 1, + Total = 2 +}; + enum class ResourceDisplayAnchor : unsigned char { diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index 415cbdba7f..5ce654af27 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -1206,6 +1206,32 @@ namespace detail return false; } + template <> + inline bool read(ResourceHarvesterDisplayMode& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + if (_strcmpi(str, "activeandtotal") == 0 || _strcmpi(str, "active/total") == 0 || _strcmpi(str, "all") == 0 || _strcmpi(str, "both") == 0) + { + value = ResourceHarvesterDisplayMode::ActiveAndTotal; + return true; + } + else if (_strcmpi(str, "active") == 0 || _strcmpi(str, "activeonly") == 0 || _strcmpi(str, "working") == 0) + { + value = ResourceHarvesterDisplayMode::Active; + return true; + } + else if (_strcmpi(str, "total") == 0 || _strcmpi(str, "totalonly") == 0 || _strcmpi(str, "max") == 0) + { + value = ResourceHarvesterDisplayMode::Total; + return true; + } + Debug::INIParseFailed(pSection, pKey, parser.value(), "Harvester display mode can be ActiveAndTotal, Active, or Total"); + } + return false; + } + template <> inline bool read(ResourceDisplayAnchor& value, INI_EX& parser, const char* pSection, const char* pKey) { @@ -1877,12 +1903,21 @@ if(_strcmpi(parser.value(), #name) == 0){ value = __uuidof(name ## LocomotionCla template void parse_indexes(std::vector& vector, INI_EX& parser, const char* pSection, const char* pKey) { + char* comment = strchr(parser.value(), ';'); + if (comment) + *comment = 0; + for (auto&& part : std::string_view { parser.value() } | std::views::split(',')) { std::string_view&& cur { part.begin(),part.end() }; - // you forgot to trim, suckers - auto pCur = cur.data() + cur.find_first_not_of(" \t\r"); - *const_cast(cur.data() + cur.find_last_not_of(" \t\r") + 1) = 0; + if (cur.empty()) + continue; + auto first = cur.find_first_not_of(" \t\r"); + if (first == std::string_view::npos) + continue; + auto last = cur.find_last_not_of(" \t\r"); + auto pCur = cur.data() + first; + *const_cast(cur.data() + last + 1) = 0; int idx = Lookuper::FindIndex(pCur); if (idx != -1) vector.push_back(idx); From b667e9629a6f26d193594828b897352100ae5486 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 27 Aug 2026 14:01:59 +0200 Subject: [PATCH 18/19] remove fallbacks --- docs/New-or-Enhanced-Logics.md | 26 +++++++++++++------------- src/New/Type/ResourceTypeClass.cpp | 15 +++------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index a2e4c35680..6c76a2e4ab 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -322,16 +322,16 @@ Similarly, unless you really have a need, `UseGlobalRadApplicationDelay` should - `Sidebar.ResourceTypes.Types` list of resources to show on this side's sidebar (if omitted, all non-hidden resources are shown). - `[ResourceTypes]` also supports **`Money`**, **`Power`**, **`Harvesters`**, and **`Weeds`** pseudo-resources. Simply declare them in the list (e.g. `0=Money`, `1=Power`, `2=Harvesters`, `3=Weeds`) to customize their HUD display (labels, positioning, formatting, status colors, or moving them between the sidebar and the tactical screen). - When `Money` is declared, it automatically tracks player credits and replaces the default sidebar counter with the Resource HUD. It also supports `Production.Money` and `ResourceCollector.Money`. - - `Display.Power.Format` controls the format of the displayed power value (`DrainAndTotal`, `Drain`, `NetAndTotal`, `Net`, `Total`; defaults to `NetAndTotal`). - - `Display.Power.ColorGreen`, `Display.Power.ColorYellow`, `Display.Power.ColorRed`, `Display.Power.ColorGrey` (or `Display.ColorGreen`, `ColorYellow`, `ColorRed`, `ColorGrey`) optional R,G,B text colors for power states: + - `Display.Format` controls the format of the displayed power value (`NetAndTotal`, `Net`, `DrainAndTotal`, `Drain`, `Total`; defaults to `NetAndTotal`). + - `Display.ColorGreen`, `Display.ColorYellow`, `Display.ColorRed`, `Display.ColorGrey` optional R,G,B text colors for power states: - `ColorGreen`: normal surplus power (defaults to `0,255,0`). - `ColorYellow`: high drain / low power (defaults to `255,255,0`). - `ColorRed`: power deficit / low power offline (defaults to `255,0,0`). - `ColorGrey`: blackout active (defaults to `128,128,128`). - Note: If `Display.Color` is defined instead, it forces a single fixed static color regardless of power status. - When `Harvesters` is declared, it automatically tracks active and total harvesters and replaces the legacy sidebar harvester counter. - - `Display.Harvesters.Format` controls the format of the displayed harvester counter (`ActiveAndTotal`, `Active`, `Total`; defaults to `ActiveAndTotal`). - - `Display.Harvesters.ColorGreen`, `Display.Harvesters.ColorYellow`, `Display.Harvesters.ColorRed` (or `Display.ColorGreen`, `ColorYellow`, `ColorRed`) optional R,G,B text colors for harvester activity states: + - `Display.Format` controls the format of the displayed harvester counter (`ActiveAndTotal`, `Active`, `Total`; defaults to `ActiveAndTotal`). + - `Display.ColorGreen`, `Display.ColorYellow`, `Display.ColorRed` optional R,G,B text colors for harvester activity states: - `ColorGreen`: all harvesters are actively harvesting or unloading (defaults to `0,255,0`). - `ColorYellow`: some harvesters are active while at least one is idle (defaults to `255,255,0`). - `ColorRed`: no harvesters are active / all idle (defaults to `255,0,0`). @@ -392,11 +392,11 @@ Display.Label.InvertPosition=false ; boolean Display.Label.UseSpace=false ; boolean Display.Condition=Always ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never) Display.Offset= ; Point2D - X,Y -Display.Power.Format=NetAndTotal ; ResourcePowerDisplayMode (NetAndTotal | Net | DrainAndTotal | Drain | Total) -Display.Power.ColorGreen=0,255,0 ; integer - Red,Green,Blue (normal power surplus, default 0,255,0) -Display.Power.ColorYellow=255,255,0 ; integer - Red,Green,Blue (low power / high drain, default 255,255,0) -Display.Power.ColorRed=255,0,0 ; integer - Red,Green,Blue (power deficit / offline, default 255,0,0) -Display.Power.ColorGrey=128,128,128 ; integer - Red,Green,Blue (blackout status, default 128,128,128) +Display.Format=NetAndTotal ; ResourcePowerDisplayMode (NetAndTotal | Net | DrainAndTotal | Drain | Total) +Display.ColorGreen=0,255,0 ; integer - Red,Green,Blue (normal power surplus, default 0,255,0) +Display.ColorYellow=255,255,0 ; integer - Red,Green,Blue (low power / high drain, default 255,255,0) +Display.ColorRed=255,0,0 ; integer - Red,Green,Blue (power deficit / offline, default 255,0,0) +Display.ColorGrey=128,128,128 ; integer - Red,Green,Blue (blackout status, default 128,128,128) Display.Color= ; integer - Red,Green,Blue (forces a single static color if set, overriding status colors) [Harvesters] ; Harvesters Pseudo-Resource (tracks active and total harvesters) @@ -405,10 +405,10 @@ Display.Label.InvertPosition=false ; boolean Display.Label.UseSpace=false ; boolean Display.Condition=Never ; ResourceDisplayCondition (Always | GreaterThanZero | Never; HasCollector is ignored) Display.Offset= ; Point2D - X,Y -Display.Harvesters.Format=ActiveAndTotal ; ResourceHarvesterDisplayMode (ActiveAndTotal | Active | Total) -Display.Harvesters.ColorGreen=0,255,0 ; integer - Red,Green,Blue (100% harvesters active, default 0,255,0) -Display.Harvesters.ColorYellow=255,255,0 ; integer - Red,Green,Blue (some harvesters active, default 255,255,0) -Display.Harvesters.ColorRed=255,0,0 ; integer - Red,Green,Blue (no harvesters active, default 255,0,0) +Display.Format=ActiveAndTotal ; ResourceHarvesterDisplayMode (ActiveAndTotal | Active | Total) +Display.ColorGreen=0,255,0 ; integer - Red,Green,Blue (100% harvesters active, default 0,255,0) +Display.ColorYellow=255,255,0 ; integer - Red,Green,Blue (some harvesters active, default 255,255,0) +Display.ColorRed=255,0,0 ; integer - Red,Green,Blue (no harvesters active, default 255,0,0) Display.Color= ; integer - Red,Green,Blue (forces a single static color if set, overriding status colors) [Weeds] ; Weeds Pseudo-Resource (tracks stored Veinhole monster Tiberium / Weeds) diff --git a/src/New/Type/ResourceTypeClass.cpp b/src/New/Type/ResourceTypeClass.cpp index 9bd7438603..a05b6d3501 100644 --- a/src/New/Type/ResourceTypeClass.cpp +++ b/src/New/Type/ResourceTypeClass.cpp @@ -153,30 +153,21 @@ void ResourceTypeClass::LoadFromINI(CCINIClass* pINI) this->Crate_Sound.Read(exINI, section, "Crate.Sound"); this->Crate_Anim.Read(exINI, section, "Crate.Anim"); - this->Display_Power_Mode.Read(exINI, section, "Display.Power.Mode"); - this->Display_Power_Mode.Read(exINI, section, "Display.Power.Format"); + this->Display_Power_Mode.Read(exINI, section, "Display.Format"); - this->Display_Power_ColorGreen.Read(exINI, section, "Display.Power.ColorGreen"); this->Display_Power_ColorGreen.Read(exINI, section, "Display.ColorGreen"); - this->Display_Power_ColorYellow.Read(exINI, section, "Display.Power.ColorYellow"); this->Display_Power_ColorYellow.Read(exINI, section, "Display.ColorYellow"); - this->Display_Power_ColorRed.Read(exINI, section, "Display.Power.ColorRed"); this->Display_Power_ColorRed.Read(exINI, section, "Display.ColorRed"); - this->Display_Power_ColorGrey.Read(exINI, section, "Display.Power.ColorGrey"); this->Display_Power_ColorGrey.Read(exINI, section, "Display.ColorGrey"); - this->Display_Harvester_Mode.Read(exINI, section, "Display.Harvesters.Mode"); - this->Display_Harvester_Mode.Read(exINI, section, "Display.Harvesters.Format"); + this->Display_Harvester_Mode.Read(exINI, section, "Display.Format"); - this->Display_Harvester_ColorGreen.Read(exINI, section, "Display.Harvesters.ColorGreen"); this->Display_Harvester_ColorGreen.Read(exINI, section, "Display.ColorGreen"); - this->Display_Harvester_ColorYellow.Read(exINI, section, "Display.Harvesters.ColorYellow"); this->Display_Harvester_ColorYellow.Read(exINI, section, "Display.ColorYellow"); - this->Display_Harvester_ColorRed.Read(exINI, section, "Display.Harvesters.ColorRed"); this->Display_Harvester_ColorRed.Read(exINI, section, "Display.ColorRed"); bool showTotal = true; - if (exINI.ReadBool(section, "Display.Power.ShowTotal", &showTotal)) + if (exINI.ReadBool(section, "Display.ShowTotal", &showTotal)) { this->Display_Power_Mode = showTotal ? ResourcePowerDisplayMode::NetAndTotal : ResourcePowerDisplayMode::Net; } From 430f72a5cf2084bdd60ae774066c0e9099645bfb Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 27 Aug 2026 16:07:39 +0200 Subject: [PATCH 19/19] missing docs --- docs/AI-Scripting-and-Mapping.md | 33 ++++++++++++++++++++++++++++++++ docs/New-or-Enhanced-Logics.md | 1 + 2 files changed, 34 insertions(+) diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 074d3917e2..099a1cf56d 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -841,6 +841,24 @@ ID=ActionCount,[Action1],611,4,[Map Filename],0,0,0,0,A,[ActionX] ... ``` +### `620-622` Modify Custom Resource + +- Modifies the stored amount of a custom resource for a house. + - `620`: Sets the resource amount to a specific value. + - `621`: Adds the specified amount to the current resource amount. + - `622`: Subtracts the specified amount from the current resource amount. + - `HouseIndex` specifies the target house (`-1` or omitted uses the trigger owner / current player). + +In `mycampaign.map`: +```ini +[Actions] +... +ID=ActionCount,[Action1],620,4,[ResourceTypeID],[Amount],[HouseIndex],0,0,A,[ActionX] +ID=ActionCount,[Action1],621,4,[ResourceTypeID],[Amount],[HouseIndex],0,0,A,[ActionX] +ID=ActionCount,[Action1],622,4,[ResourceTypeID],[Amount],[HouseIndex],0,0,A,[ActionX] +... +``` + ### `800-802` Display Banner - Display a 'banner' at a fixed location that is relative to the screen. @@ -1039,6 +1057,21 @@ ID=EventCount,...,606,2,0,[AttachEffectType],... ... ``` +### `610-611` House Has Custom Resource Greater/Less Than + +- Checks if the trigger owner house has more or less than the specified amount of a custom resource. + - `610`: Springs when the house owns strictly greater than the specified amount of the resource. + - `611`: Springs when the house owns strictly less than the specified amount of the resource. + +In `mycampaign.map`: +```ini +[Events] +... +ID=EventCount,...,610,2,[Amount],[ResourceTypeID],... +ID=EventCount,...,611,2,[Amount],[ResourceTypeID],... +... +``` + ## Teams ### Adjust recruitable status on team member liberate diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 7fa57bf003..98d650ed5b 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -337,6 +337,7 @@ Similarly, unless you really have a need, `UseGlobalRadApplicationDelay` should - `ColorRed`: no harvesters are active / all idle (defaults to `255,0,0`). - Note: If `Display.Color` is defined instead, it forces a single fixed static color regardless of harvester activity. - When `Weeds` is declared, it automatically tracks the player's stored Veinhole monster Tiberium / Weeds (`pPlayer->OwnedWeed.GetTotalAmount()`) and replaces the legacy sidebar weeds counter. +- Supports map trigger actions `620 Set Custom Resource`, `621 Add Custom Resource`, `622 Subtract Custom Resource`, and trigger events `610 House Has Custom Resource Greater Than`, `611 House Has Custom Resource Less Than` (see [AI Scripting and Mapping](AI-Scripting-and-Mapping.md)). In `rulesmd.ini`: ```ini