diff --git a/CREDITS.md b/CREDITS.md
index f4c3fe9d3b..f094f031bb 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -158,6 +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
+ - 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 67dc28e6ee..9867961ef9 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -273,6 +273,7 @@
+
@@ -313,6 +314,7 @@
+
@@ -414,6 +416,7 @@
+
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 c56310bfc9..d11e3c9a3e 100644
--- a/docs/New-or-Enhanced-Logics.md
+++ b/docs/New-or-Enhanced-Logics.md
@@ -280,6 +280,166 @@ 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, 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.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`.
+ - `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`**, **`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.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.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`).
+ - 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
+[ResourceTypes]
+0=SOMERESOURCETYPE
+
+[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 (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 (static color)
+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 (defaults to $)
+Display.Label.InvertPosition=false ; boolean
+Display.Label.UseSpace=false ; boolean
+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 (defaults to ⚡)
+Display.Label.InvertPosition=false ; boolean
+Display.Label.UseSpace=false ; boolean
+Display.Condition=Always ; ResourceDisplayCondition (Always | GreaterThanZero | HasCollector | Never)
+Display.Offset= ; Point2D - X,Y
+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)
+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.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)
+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
+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
+```
+
### Laser Trails

diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 716072c2d4..d19fa7c5c5 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -421,6 +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)
+- [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)
- [AutoDeath based on player power status and player credits](New-or-Enhanced-Logics.md#kill-object-automatically) (by Flactine)
- [Roof production anim](New-or-Enhanced-Logics.md#roof-production-anim) (by Noble_Fish)
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/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 cb8eaa3996..da3bff3256 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;
@@ -40,6 +41,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 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);
+
+ 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 +244,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.Selling.cpp b/src/Ext/Building/Hooks.Selling.cpp
index 012b6a69fd..27156d7ed3 100644
--- a/src/Ext/Building/Hooks.Selling.cpp
+++ b/src/Ext/Building/Hooks.Selling.cpp
@@ -1,5 +1,6 @@
#include "Body.h"
-
+#include
+#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 01a6c959d1..272e05b27b 100644
--- a/src/Ext/Building/Hooks.cpp
+++ b/src/Ext/Building/Hooks.cpp
@@ -1,5 +1,6 @@
#include "Body.h"
+#include
#include
#include
#include
@@ -392,10 +393,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);
+ if (!pThis)
+ return 0;
- if (!pTypeExt->PowerPlantEnhancer_Buildings.empty() && (pTypeExt->PowerPlantEnhancer_Amount != 0 || pTypeExt->PowerPlantEnhancer_Factor != 1.0f))
+ const auto pTypeExt = BuildingTypeExt::TryFetch(pThis->Type);
+ const auto pOwnerExt = HouseExt::TryFetch(pThis->Owner);
+ const auto pBuildingExt = BuildingExt::TryFetch(pThis);
+
+ 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;
@@ -1544,3 +1552,169 @@ 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);
+
+ 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)
+ {
+ 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/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp
index 1d37508cc5..13ed5cb727 100644
--- a/src/Ext/BuildingType/Body.cpp
+++ b/src/Ext/BuildingType/Body.cpp
@@ -307,7 +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->DeployFireDelay.Read(exINI, pSection, "DeployFireDelay");
auto& preProdAnim = pThis->GetBuildingAnim(BuildingAnimSlot::PreProduction);
diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h
index 254c604546..971f281987 100644
--- a/src/Ext/BuildingType/Body.h
+++ b/src/Ext/BuildingType/Body.h
@@ -292,7 +292,6 @@ class BuildingTypeExt final : public TechnoTypeExt
void Serialize(T& Stm);
public:
-
class ExtContainer final : public Container
{
public:
diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp
index e53d1d2ad9..2282e49de7 100644
--- a/src/Ext/House/Body.cpp
+++ b/src/Ext/House/Body.cpp
@@ -2,6 +2,10 @@
#include
#include
+#include
+#include
+#include
+#include
//Static init
@@ -706,12 +710,15 @@ void HouseExt::Serialize(T& Stm)
.Process(this->SuspendedEMPulseSWs)
.Process(this->SuperExts)
.Process(this->ForceEnemyIndex)
+ .Process(this->CustomResources)
+ .Process(this->ResourceCollectorCounts)
.Process(this->ForceOnlyTargetHouseEnemy)
.Process(this->ForceOnlyTargetHouseEnemyMode)
.Process(this->TeamDelay)
.Process(this->FreeRadar)
.Process(this->ForceRadar)
.Process(this->PlayerAutoRepair)
+ .Process(this->FactoryResourceStates)
//.Process(this->BeaconsPlacedOrder) beacon is not saved, so this follows it.
;
}
@@ -1188,6 +1195,217 @@ bool HouseExt::ReachedBuildLimit(const HouseClass* pHouse, const TechnoTypeClass
}
#pragma endregion
+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;
+}
+
+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);
+
+ this->CustomResources[resourceIdx] = std::max(0, amount);
+}
+
+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);
+
+ this->CustomResources[resourceIdx] = std::max(0, this->CustomResources[resourceIdx] + delta);
+}
+
+bool HouseExt::CanAffordResource(int resourceIdx, int amount) const
+{
+ if (!this->IsResourceEnabled(resourceIdx))
+ return false;
+ return this->GetResourceAmount(resourceIdx) >= amount;
+}
+
+bool HouseExt::IsResourceEnabled(int resourceIdx) const
+{
+ if (resourceIdx < 0 || resourceIdx >= static_cast(ResourceTypeClass::Array.size()))
+ return false;
+
+ 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::CalculateResourceBounty(int resourceIdx, TechnoClass* pVictim) const
+{
+ 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 pVictimType = pVictim->GetTechnoType();
+ const auto pVictimTypeExt = TechnoTypeExt::TryFetch(pVictimType);
+
+ const bool isAllied = pThis->IsAlliedWith(pVictim);
+
+ if (isAllied)
+ {
+ // 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;
+ }
+
+ // 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();
+ }
+}
+
+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
@@ -1203,7 +1421,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 df5ba629b6..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:
@@ -65,6 +87,9 @@ class HouseExt final : public AbstractExt, public Detach::Listener> SuspendedEMPulseSWs;
+ std::vector CustomResources;
+ std::vector ResourceCollectorCounts;
+
// standalone? no need and not a good idea
struct SWExt
{
@@ -80,6 +105,7 @@ class HouseExt final : public AbstractExt, public Detach::Listener BeaconsPlacedOrder;
+ std::vector FactoryResourceStates;
HouseExt(HouseClass* OwnerObject) : AbstractExt(OwnerObject)
, PowerPlantEnhancers {}
@@ -107,6 +133,8 @@ 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,7 @@ DEFINE_HOOK(0x4AC9B2, MouseClass_ToggleBeaconMode_AllUsed, 0x6)
}
#pragma endregion
+
+#pragma endregion
+
+
diff --git a/src/Ext/HouseType/Body.h b/src/Ext/HouseType/Body.h
index c843286266..7f935fbe19 100644
--- a/src/Ext/HouseType/Body.h
+++ b/src/Ext/HouseType/Body.h
@@ -65,4 +65,3 @@ class HouseTypeExt final : public AbstractTypeExt
return AbstractExt::TryFetch(pThis);
}
};
-
diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp
index 856b0991a0..f8c4b649a7 100644
--- a/src/Ext/Rules/Body.cpp
+++ b/src/Ext/Rules/Body.cpp
@@ -11,6 +11,8 @@
#include
#include
#include
+#include
+#include
std::unique_ptr RulesExt::Data = nullptr;
@@ -31,6 +33,20 @@ void RulesExt::LoadFromINIFile(RulesClass* pThis, CCINIClass* pINI)
void RulesExt::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
{
+ ResourceTypeClass::LoadFromINIList(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);
@@ -1109,7 +1125,7 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->KeepAlive_Buildings)
.Process(this->KeepAlive_Defenses)
.Process(this->AutoTarget_InsignificantWhenMindControlled)
- ;
+ ;
}
void RulesExt::ExtData::LoadFromStream(PhobosStreamReader& Stm)
diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h
index 707de53c9b..54bbefd52b 100644
--- a/src/Ext/Rules/Body.h
+++ b/src/Ext/Rules/Body.h
@@ -345,11 +345,9 @@ class RulesExt
int TintColorIronCurtain;
int TintColorForceShield;
int TintColorBerserk;
-
Valueable InfantryAutoDeploy;
Valueable AdjacentWallDamage;
-
Valueable WarheadAnimZAdjust;
Valueable IvanBombAttachToCenter;
@@ -836,7 +834,6 @@ class RulesExt
, Parasite_AllowWaterExit {}
, InfantryAutoDeploy { false }
, AdjacentWallDamage { 200 }
-
, WarheadAnimZAdjust { -15 }
, IvanBombAttachToCenter { false }
diff --git a/src/Ext/SWType/Body.cpp b/src/Ext/SWType/Body.cpp
index 531a66fb0d..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,6 +101,7 @@ void SWTypeExt::Serialize(T& Stm)
.Process(this->SW_Link_RollChances)
.Process(this->Message_LinkedSWAcquired)
.Process(this->EVA_LinkedSWAcquired)
+ .Process(this->SW_ResourceAmounts)
.Process(this->Message_Activated_Owner)
.Process(this->Message_Activated_Allies)
.Process(this->Message_Activated_Enemies)
@@ -306,6 +308,24 @@ void SWTypeExt::LoadFromINIFile(CCINIClass* const pINI)
pNewSWType->Initialize(const_cast(this), OwnerObject());
pNewSWType->LoadFromINI(const_cast(this), OwnerObject(), pINI);
}
+
+ 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 e064cdcfdb..2639b10ba2 100644
--- a/src/Ext/SWType/Body.h
+++ b/src/Ext/SWType/Body.h
@@ -110,7 +110,7 @@ class SWTypeExt final : public AbstractTypeExt
Valueable EMPulse_SuspendOthers;
ValueableVector EMPulse_Cannons;
Valueable EMPulse_TargetSelf;
-
+ std::vector SW_ResourceAmounts;
ValueableIdxVector SW_Link;
Valueable SW_Link_Grant;
Valueable SW_Link_Ready;
@@ -213,6 +213,7 @@ class SWTypeExt final : public AbstractTypeExt
, EVA_Activated_Owner { -1 }
, EVA_Activated_Allies { -1 }
, EVA_Activated_Enemies { -1 }
+ , SW_ResourceAmounts {}
{ }
// Ares 0.A functions
@@ -237,6 +238,8 @@ class SWTypeExt final : public AbstractTypeExt
std::vector GetEMPulseCannons(HouseClass* pOwner, const CellStruct& cell) const;
std::pair GetEMPulseCannonRange(BuildingClass* pBuilding) const;
+ 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 eae199788a..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,6 +39,8 @@ 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);
+ pTypeExt->ApplyResourceAmounts(pSW);
+
pTypeExt->ApplyActivatedMessage(pSW);
pTypeExt->ApplyActivatedEva(pSW);
@@ -488,6 +492,46 @@ void SWTypeExt::ApplyLinkedSW(SuperClass* pSW)
}
}
+void SWTypeExt::ApplyResourceAmounts(SuperClass* pSW)
+{
+ 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
{
const auto pHouse = pSW->Owner;
diff --git a/src/Ext/SWType/Hooks.cpp b/src/Ext/SWType/Hooks.cpp
index 15ab51b00e..5c620f4b00 100644
--- a/src/Ext/SWType/Hooks.cpp
+++ b/src/Ext/SWType/Hooks.cpp
@@ -1,4 +1,5 @@
#include "Body.h"
+#include
//this hook just for phobos NewSWType
DEFINE_HOOK(0x6CC390, SuperClass_Launch, 0x6)
@@ -264,6 +265,47 @@ DEFINE_HOOK(0x6AC67A, SidebarClass_6AC5F0_TabIndex, 0x5)
DEFINE_JUMP(LJMP, 0x6A8D07, 0x6A8D17) // Skip tabIndex check
#pragma endregion
+// Full rewrite
+DEFINE_HOOK(0x6CC367, SuperClass_IsReady_Resources, 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 pExt = SWTypeExt::Fetch(pSuper->Type);
+ if (!pExt->AreResourcesSufficient(pSuper->Owner))
+ {
+ 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->CanFire();
+ }
+ }
+
+ return 0;
+}
+
DEFINE_HOOK(0x53B276, PsyDom_Fire_Select, 0x6)
{
enum { SkipGameCode = 0x53B29E };
diff --git a/src/Ext/Side/Body.cpp b/src/Ext/Side/Body.cpp
index e7a42294e4..f867f82389 100644
--- a/src/Ext/Side/Body.cpp
+++ b/src/Ext/Side/Body.cpp
@@ -45,6 +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_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");
@@ -55,9 +60,31 @@ 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");
}
-// =============================
+// =============
// load / save
template
@@ -81,6 +108,10 @@ void SideExt::Serialize(T& Stm)
.Process(this->Sidebar_PowerDelta_ColorRed)
.Process(this->Sidebar_PowerDelta_ColorGrey)
.Process(this->Sidebar_PowerDelta_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)
@@ -93,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 1ba19ec6e4..6f8bb270ad 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,6 +42,10 @@ class SideExt final : public AbstractTypeExt
Valueable Sidebar_PowerDelta_ColorRed;
Valueable Sidebar_PowerDelta_ColorGrey;
Valueable Sidebar_PowerDelta_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;
@@ -51,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 }
@@ -72,6 +87,10 @@ class SideExt final : public AbstractTypeExt
, Sidebar_PowerDelta_ColorRed { { 255, 0, 0 } }
, Sidebar_PowerDelta_ColorGrey { { 0x80,0x80,0x80 } }
, Sidebar_PowerDelta_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 { }
@@ -82,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/Ext/TAction/Body.cpp b/src/Ext/TAction/Body.cpp
index 201e8dfef7..5055ff87cb 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,55 @@ 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;
+
+ const auto pResource = ResourceTypeClass::Array[resIdx].get();
+ if (pResource && pResource->IsPowerResource())
+ 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 4b583ab3d4..5ea3ab9c2a 100644
--- a/src/Ext/Techno/Body.Internal.cpp
+++ b/src/Ext/Techno/Body.Internal.cpp
@@ -1,5 +1,15 @@
+#include "Body.h"
+
+#include
+
+#include
+#include
+#include
+#include
#include
#include
+#include
+#include
// Unsorted methods
@@ -15,11 +25,18 @@ void TechnoExt::InitializeLaserTrails()
this->LaserTrails.emplace_back(std::make_unique(entry.GetType(), this->OwnerObject()->Owner, entry.FLH, entry.IsOnTurret));
}
-void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller)
+void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller, HouseClass* pHouseKiller)
{
- auto const pKillerType = pKiller->GetTechnoType();
- auto const pObjectKiller = ((pKillerType->Spawned || pKillerType->MissileSpawn) && pKiller->SpawnOwner)
- ? pKiller->SpawnOwner : pKiller;
+ TechnoClass* pObjectKiller = nullptr;
+
+ if (pKiller)
+ {
+ pObjectKiller = ((pKiller->GetTechnoType()->Spawned || pKiller->GetTechnoType()->MissileSpawn) && pKiller->SpawnOwner) ?
+ pKiller->SpawnOwner : pKiller;
+
+ if (!pObjectKiller)
+ return;
+ }
if (pObjectKiller && pObjectKiller->BelongsToATeam())
{
@@ -29,6 +46,35 @@ void TechnoExt::ObjectKilledBy(TechnoClass* pVictim, TechnoClass* pKiller)
pKillerTechnoData->LastKillWasTeamTarget = pFootKiller->Team->Focus == pVictim;
}
}
+
+ HouseClass* pHouse = pKiller ? pKiller->Owner : pHouseKiller;
+
+ if (pHouse && pHouse != pVictim->Owner)
+ {
+ if (auto pHouseExt = HouseExt::TryFetch(pHouse))
+ {
+ const size_t resourceCount = ResourceTypeClass::Array.size();
+ for (size_t i = 0; i < resourceCount; ++i)
+ {
+ 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()
+ );
+ }
+ }
+ }
+ }
+ }
}
Matrix3D TechnoExt::GetTransform(TechnoClass* pThis, VoxelIndexKey* pKey, bool isShadow)
diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp
index e5782970f6..e4b0961dc3 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
@@ -14,6 +15,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 57a79aa69e..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();
@@ -231,7 +243,7 @@ class TechnoExt : public RadioExt, public Detach::Listener
static void ChangeOwnerMissionFix(FootClass* pThis);
static void KillSelf(TechnoClass* pThis, AutoDeathBehavior deathOption, const std::vector& 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 bool HasAdditionalAbility(TechnoClass* pThis, AdditionalAbility ability);
static double GetCurrentSpeedMultiplier(FootClass* pThis);
diff --git a/src/Ext/Techno/Hooks.cpp b/src/Ext/Techno/Hooks.cpp
index 6d0a24caab..5764c4d03c 100644
--- a/src/Ext/Techno/Hooks.cpp
+++ b/src/Ext/Techno/Hooks.cpp
@@ -660,6 +660,7 @@ DEFINE_HOOK(0x702E4E, TechnoClass_RegisterDestruction_SaveKillerInfo, 0x6)
// 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);
// Drop crate if is dead
@@ -671,6 +672,8 @@ DEFINE_HOOK(0x70337D, HouseClass_RegisterDestruction_SaveKillerInfo, 0x6)
TechnoExt::TryToCreateCrate(pVictim->Location, selectedPowerup);
}
+ TechnoExt::ObjectKilledBy(pVictim, nullptr, pHouse);
+
return 0;
}
diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp
index be39a449c9..286f514487 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -1145,6 +1145,160 @@ 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");
+ 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 || pResource->IsPowerResource())
+ continue;
+
+ if (!pResource->IsMoneyResource())
+ {
+ 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");
this->Ammo_AutoConvertType.Read(exINI, pSection, "Ammo.AutoConvertType");
@@ -1785,6 +1939,12 @@ void TechnoTypeExt::Serialize(T& Stm)
.Process(this->VoiceEliteWeaponAttacks)
.Process(this->TeamMember_ConsideredAs)
+ .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 fb39428e00..f7051b2823 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -33,6 +34,7 @@ class TechnoTypeExt : public ObjectTypeExt
return static_cast(this->GetAttachedObject());
}
+
Valueable HealthBar_Hide;
Valueable HealthBar_HidePips;
Valueable HealthBar_Permanent;
@@ -456,6 +458,13 @@ class TechnoTypeExt : public ObjectTypeExt
Nullable Unsellable;
Nullable KeepAlive;
+ 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 }
, HealthBar_HidePips { false }
@@ -861,6 +870,12 @@ class TechnoTypeExt : public ObjectTypeExt
// Ares 3.0
, Unsellable {}
, KeepAlive {}
+ , 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