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 ![Laser Trails](_static/images/lasertrails.gif) 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 +#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.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; diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index 491ab6dc0e..93b472b18c 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -3557,6 +3557,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.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 4bd96e843f..d4200ea707 100644 --- a/src/Misc/Hooks.UI.cpp +++ b/src/Misc/Hooks.UI.cpp @@ -10,6 +10,7 @@ #include #include +#include #include @@ -98,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; @@ -105,10 +124,13 @@ DEFINE_HOOK(0x4A25E0, CreditsClass_GraphicLogic_HarvesterCounter, 0x7) return 0; RectangleStruct vRect = DSurface::Sidebar->GetRect(); + auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); + + // 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()) { - 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); @@ -134,9 +156,8 @@ 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()) { - const auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); wchar_t counter[0x20]; ColorStruct clrToolTip; @@ -172,9 +193,8 @@ 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()) { - 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 eaed57198b..20b68a920f 100644 --- a/src/Misc/PhobosToolTip.cpp +++ b/src/Misc/PhobosToolTip.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -132,10 +133,50 @@ 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 + oss << pType->UIName << L"\n"; + + bool hasPreceding = false; + + if (cost != 0) + { + oss << Phobos::UI::CostLabel << std::abs(cost); + hasPreceding = true; + } + + 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) + { + 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 << resCost << (useSpace ? L" " : L"") << label; + else + oss << label << (useSpace ? L" " : L"") << resCost; + } + else + { + 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; @@ -161,32 +202,76 @@ 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; + + // Negative money amount means granting money upon launch (+) + if (nCost < 0) + oss << L"+"; + + oss << Phobos::UI::CostLabel << std::abs(nCost); + } + + for (size_t i = 0; i < pData->SW_ResourceAmounts.size(); ++i) + { + const int nAmount = pData->SW_ResourceAmounts[i]; + if (nAmount != 0) + { + 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 (pData->Money_Amount > 0) - oss << '+'; + // Negative resource amount means granting resource upon launch (+) + const bool isGrant = (nAmount < 0); - oss << Phobos::UI::CostLabel << nCost; - showSth = true; + if (pResource && pResource->Display_Label_InvertPosition.Get()) + { + if (isGrant) + oss << L"+"; + oss << std::abs(nAmount) << (useSpace ? L" " : L"") << label; + } + else + { + if (isGrant) + oss << L"+"; + oss << label << (useSpace ? L" " : L"") << std::abs(nAmount); + } + } } 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]; @@ -194,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 new file mode 100644 index 0000000000..a05b6d3501 --- /dev/null +++ b/src/New/Type/ResourceTypeClass.cpp @@ -0,0 +1,802 @@ +#include "ResourceTypeClass.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +template<> +const char* Enumerable::GetMainSection() +{ + return "ResourceTypes"; +} + +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 && _strcmpi(pTitle, "Harvesters") != 0 && _strcmpi(pTitle, "Weeds") != 0) : true } + , Display_Color {} + , Display_Condition { (pTitle && (_strcmpi(pTitle, "Harvesters") == 0 || _strcmpi(pTitle, "Weeds") == 0)) ? ResourceDisplayCondition::Never : ResourceDisplayCondition::Always } + , Display_Offset {} + , InitialValue { 0 } + , RequiresCollector { false } + , Bounty_Enabled { false } + , Bounty_DefaultValue { 0 } + , Bounty_DefaultFriendlyValue { 0 } + , Bounty_CanUseStandardPoints { false } + , Bounty_MoneyConversion { 100 } + , Crate_Amount { 0 } + , Crate_Sound {} + , Crate_Anim {} + , Display_Power_Mode { ResourcePowerDisplayMode::NetAndTotal } + , Display_Power_ColorGreen {} + , Display_Power_ColorYellow {} + , Display_Power_ColorRed {} + , Display_Power_ColorGrey {} + , Display_Harvester_Mode { ResourceHarvesterDisplayMode::ActiveAndTotal } + , Display_Harvester_ColorGreen {} + , Display_Harvester_ColorYellow {} + , Display_Harvester_ColorRed {} +{ +} + +bool ResourceTypeClass::HasMoneyResource() +{ + for (const auto& pResource : ResourceTypeClass::Array) + { + if (pResource && pResource->IsMoneyResource()) + return true; + } + return false; +} + +bool ResourceTypeClass::HasPowerResource() +{ + for (const auto& pResource : ResourceTypeClass::Array) + { + if (pResource && pResource->IsPowerResource()) + return true; + } + return false; +} + +bool ResourceTypeClass::HasHarvesterResource() +{ + for (const auto& pResource : ResourceTypeClass::Array) + { + if (pResource && pResource->IsHarvesterResource()) + 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; + + auto const pPlayer = HouseClass::CurrentPlayer; + if (!pPlayer || pPlayer->Defeated) + return false; + + auto pSideExt = SideExt::Fetch(SideClass::Array.GetItem(pPlayer->SideIndex)); + 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) + { + if (idx >= 0 && idx < static_cast(ResourceTypeClass::Array.size())) + { + if (ResourceTypeClass::Array[idx]->IsMoneyResource()) + return true; + } + } + return (sideAnchor != ResourceDisplayAnchor::Sidebar); + } + + return (sideAnchor != ResourceDisplayAnchor::Sidebar); +} + +void ResourceTypeClass::LoadFromINI(CCINIClass* pINI) +{ + const char* section = this->Name; + if (this->IsMoneyResource() || this->IsPowerResource() || this->IsHarvesterResource() || this->IsWeedsResource()) + { + this->Display_Label_UseSpace = false; + } + + 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"); + 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"); + 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"); + + 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.Format"); + + this->Display_Power_ColorGreen.Read(exINI, section, "Display.ColorGreen"); + this->Display_Power_ColorYellow.Read(exINI, section, "Display.ColorYellow"); + this->Display_Power_ColorRed.Read(exINI, section, "Display.ColorRed"); + this->Display_Power_ColorGrey.Read(exINI, section, "Display.ColorGrey"); + + this->Display_Harvester_Mode.Read(exINI, section, "Display.Format"); + + this->Display_Harvester_ColorGreen.Read(exINI, section, "Display.ColorGreen"); + this->Display_Harvester_ColorYellow.Read(exINI, section, "Display.ColorYellow"); + this->Display_Harvester_ColorRed.Read(exINI, section, "Display.ColorRed"); + + bool showTotal = true; + if (exINI.ReadBool(section, "Display.ShowTotal", &showTotal)) + { + this->Display_Power_Mode = showTotal ? ResourcePowerDisplayMode::NetAndTotal : ResourcePowerDisplayMode::Net; + } +} + +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) + .Process(this->Crate_Amount) + .Process(this->Crate_Sound) + .Process(this->Crate_Anim) + .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) +{ + this->Serialize(Stm); +} + +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 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 (isSidebar) + { + if (sideAnchor != ResourceDisplayAnchor::Sidebar && !hasExplicitSidebarTypes) + return; + } + else + { + if (sideAnchor == ResourceDisplayAnchor::Sidebar) + return; + } + + RectangleStruct vRect = pSurface->GetRect(); + + int baseX = 0; + int baseY = 0; + + const int marginX = Phobos::Config::MessageDisplayInCenter ? 28 : 10; + const int screenW = pSurface->GetWidth(); + const int screenH = pSurface->GetHeight(); + + if (isSidebar) + { + 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 (sideAnchor) + { + case ResourceDisplayAnchor::TopLeft: + baseX = marginX + sideBaseOffset.X; + baseY = 5 + sideBaseOffset.Y; + break; + case ResourceDisplayAnchor::TopRight: + baseX = screenW - marginX + sideBaseOffset.X; + baseY = 5 + sideBaseOffset.Y; + break; + case ResourceDisplayAnchor::TopCenter: + baseX = (screenW / 2) + sideBaseOffset.X; + baseY = 5 + sideBaseOffset.Y; + break; + case ResourceDisplayAnchor::BottomLeft: + baseX = marginX + sideBaseOffset.X; + baseY = screenH - 20 + sideBaseOffset.Y; + break; + case ResourceDisplayAnchor::BottomRight: + baseX = screenW - marginX + sideBaseOffset.X; + baseY = screenH - 20 + sideBaseOffset.Y; + break; + case ResourceDisplayAnchor::BottomCenter: + baseX = (screenW / 2) + sideBaseOffset.X; + baseY = screenH - 20 + sideBaseOffset.Y; + break; + default: + baseX = marginX + sideBaseOffset.X; + baseY = 5 + sideBaseOffset.Y; + break; + } + } + + int stackIndex = 0; + int runningRightX = baseX; + int runningLeftX = baseX; + + TextAlign effectiveAlign; + if (isSidebar) + { + effectiveAlign = pSideExt ? pSideExt->Sidebar_ResourceTypes_Align.Get() : TextAlign::Left; + } + else + { + 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 (sideAnchor == ResourceDisplayAnchor::TopRight || sideAnchor == ResourceDisplayAnchor::BottomRight) + effectiveAlign = TextAlign::Right; + else + effectiveAlign = TextAlign::Left; + } + + const bool isRightAlign = (effectiveAlign == TextAlign::Right); + const bool isCenterAlign = (effectiveAlign == TextAlign::Center); + 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 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 || (bgWidth == 0 && bgHeight == 0) || !pSideExt) + return; + bgDrawn = true; + + if (pPCX) + { + RectangleStruct drawRect { bgPos.X, bgPos.Y, bgWidth, bgHeight }; + PCX::Instance.BlitToSurface(&drawRect, pSurface, pPCX); + } + else if (pSHP) + { + ConvertClass* pConvert = pSideExt->Display_ResourceTypes_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 + ); + } + }; + + 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); + if (i < 0 || i >= static_cast(ResourceTypeClass::Array.size())) + continue; + + const auto pResource = ResourceTypeClass::Array[i].get(); + 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; + + // 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->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); + 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 (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; + } + } + + if (pResource->Display_Condition.Get() == ResourceDisplayCondition::HasCollector) + 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]; + const wchar_t* label = pResource->Display_Label.Get(); + ColorStruct clrToolTip; + + if (pResource->IsPowerResource()) + { + if (!label || !*label) + label = L"\u26a1"; + + 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); + } + + 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(); + } + else + { + if (pPlayer->PowerBlackoutTimer.InProgress()) + { + clrToolTip = pResource->Display_Power_ColorGrey.Get(ColorStruct { 128, 128, 128 }); + } + else + { + const double percent = output != 0 + ? (double)drain / (double)output + : (drain != 0 ? Phobos::UI::PowerDelta_ConditionRed * 2.f : Phobos::UI::PowerDelta_ConditionYellow); + + 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 = L"$"; + + 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 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 (sideOrientation == ResourceDisplayOrientation::Horizontal) + { + if (isRightAlign) + { + vPos.X = runningRightX - dim.Width; + 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; + 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 - bgHeight > 0 ? baseY - stackIndex * spacing : baseY) : (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 new file mode 100644 index 0000000000..4f95ced71d --- /dev/null +++ b/src/New/Type/ResourceTypeClass.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include + +class ResourceTypeClass final : public Enumerable +{ +public: + // Per-Resource Display Configuration + Valueable Display_Label; + Valueable Display_Label_InvertPosition; + Valueable Display_Label_UseSpace; + Nullable 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; + + // Crates + Valueable Crate_Amount; + NullableIdx Crate_Sound; + NullableIdx Crate_Anim; + + // 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 DrawResourceHUD(DSurface* pSurface, bool isSidebar); + + 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.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 5a2191df1c..2799dcd28b 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -460,3 +460,46 @@ 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 ResourcePowerDisplayMode : unsigned char +{ + NetAndTotal = 0, + Net = 1, + DrainAndTotal = 2, + Drain = 3, + Total = 4 +}; + +enum class ResourceHarvesterDisplayMode : unsigned char +{ + ActiveAndTotal = 0, + Active = 1, + Total = 2 +}; + + +enum class ResourceDisplayAnchor : unsigned char +{ + TopRight = 0, + TopLeft = 1, + BottomRight = 2, + BottomLeft = 3, + Sidebar = 4, + TopCenter = 5, + BottomCenter = 6 +}; + diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index 3c37f5f96f..1b9f01eee8 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -1118,6 +1118,166 @@ 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(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(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) + { + 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; + } + 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; + } + template <> inline bool read(IronCurtainEffect& value, INI_EX& parser, const char* pSection, const char* pKey) { @@ -1743,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);