diff --git a/CREDITS.md b/CREDITS.md
index e3ab516ac..e8d9ca1c7 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -223,6 +223,7 @@ This page lists all the individual contributions to the project by their author.
- Projectile return weapon
- Aircraft landing / docking direction
- `DeploysInto` cursor desync fix
+ - Minor crate logic improvements
- **Morton (MortonPL)**:
- `XDrawOffset` for animations
- Shield passthrough & absorption
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index 1ff7abb8a..f30aa9d9b 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -63,6 +63,7 @@
+
diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md
index 9d43e1308..7d32cdd5a 100644
--- a/docs/Fixed-or-Improved-Logics.md
+++ b/docs/Fixed-or-Improved-Logics.md
@@ -153,7 +153,7 @@ This page describes all ingame logics that are fixed or improved in Phobos witho
- `PowerUpN` building animations can now use `Powered` & `PoweredLight/Effect/Special` keys.
- Fixed a desync potentially caused by displaying of cursor over selected `DeploysInto` units.
- Skipped drawing rally point line when undeploying a factory.
-
+
## Fixes / interactions with other extensions
- All forms of type conversion (including Ares') now correctly update `OpenTopped` state of passengers in transport that is converted.
@@ -1158,15 +1158,26 @@ In `rulesmd.ini`:
RadialIndicatorVisibility=allies ; list of Affected House Enumeration (owner/self | allies/ally | enemies/enemy | all)
```
-## Crate generation
+## Crate improvements
-The statistic distribution of the randomly generated crates is now more uniform within the visible map region by using an optimized sampling procedure.
-- You can now limit the crates' spawn region to land only.
+There are some improvements on goodie crate logic:
+- The statistic distribution of the randomly generated crates is now more uniform within the visible map region by using an optimized sampling procedure.
+- You can now limit the crates' spawn region to land only by setting `[CrateRules]` -> `CreateOnlyOnLand` to true.
+- The limit of vehicles a player can own before unit crates start giving money instead can now be customized by setting `UnitCrateVehicleCap`. Negative numbers disable the cap entirely.
+- `FreeMCV` setting is now actually respected and can be used to disable the forced unit selected from `[General]` -> `BaseUnit` that is given if player picks a crate and has enough credits but no existing buildings or `BaseUnit` vehicles.
+ - The previously hardcoded credits threshold that must be passed can also now be customized via `FreeMCV.CreditsThreshold`.
+- It is possible to influence weighting of units given from crates (`CrateGoodie=true`) via `CrateGoodie.RerollChance`, which determines the chance that if this type of unit is rolled, it will reroll again for another type of unit.
In `rulesmd.ini`:
```ini
[CrateRules]
-CrateOnlyOnLand=no ; boolean
+CrateOnlyOnLand=false ; boolean
+UnitCrateVehicleCap=50 ; integer
+FreeMCV=true ; boolean
+FreeMCV.CreditsThreshold=1500 ; integer
+
+[SOMEVEHICLE] ; VehicleType
+CrateGoodie.RerollChance=0.0 ; floating point value, percents or absolute (0.0-1.0)
```
## DropPod
diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md
index f2373407b..fee3c6800 100644
--- a/docs/New-or-Enhanced-Logics.md
+++ b/docs/New-or-Enhanced-Logics.md
@@ -1236,6 +1236,20 @@ In `rulesmd.ini`:
BigGap=false ; boolean
```
+### Spawn powerup crate
+
+- Warheads can now spawn powerup crates of specified type(s) on their impact cells (if free, or nearby cells if occupied something other than a crate) akin to map trigger action 108 ('Create Crate').
+ - `SpawnsCrateN` where N is a number starting from 0, parsed until no key is found can be used to define the type of crate spawned.
+ - `SpawnsCrateN.Weight` is a number that determines relative weighting of spawning corresponding crate type vs. other listed ones (0 is no chance, higher means higher probability) defaulting to 1 if not defined.
+ - `SpawnsCrate.Type/Weight` is an alias for `SpawnsCrate0.Type/Weight` if latter is not set.
+
+In `rulesmd.ini`:
+```ini
+[SOMEWARHEAD] ; Warhead
+SpawnsCrate(N).Type= ; Powerup crate type enum (money|unit|healbase|cloak|explosion|napalm|squad|reveal|armor|speed|firepower|icbm|invulnerability|veteran|ionstorm|gas|tiberium|pod)
+SpawnsCrate(N).Weight=1 ; integer
+```
+
### Trigger specific NotHuman infantry Death anim sequence
- Warheads are now able to trigger specific `NotHuman=yes` infantry `Death` anim sequence using the corresponding tag. It's value represents sequences from `Die1` to `Die5`.
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index c520dbb1d..9ed611e57 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -10,9 +10,10 @@ You can use the migration utility (can be found on [Phobos supplementaries repo]
### From vanilla
+- `[CrateRules]` -> `FreeMCV` now controls whether or not player is forced to receive unit from `[General]` -> `BaseUnit` from goodie crate if they own no buildings or any existing `BaseUnit` vehicles and own more than `[CrateRules]` -> `FreeMCV.CreditsThreshold` (defaults to 1500) credits.- Iron Curtain status is now preserved by default when converting between TechnoTypes via `DeploysInto`/`UndeploysInto`. This behavior can be turned off per-TechnoType and global basis using `[SOMETECHNOTYPE]/[CombatDamage]->IronCurtain.KeptOnDeploy=no`.
- Translucent RLE SHPs will now be drawn using a more precise and performant algorithm that has no green tint and banding. Can be disabled with `rulesmd.ini->[General]->FixTransparencyBlitters=no`.
- Iron Curtain status is now preserved by default when converting between TechnoTypes via `DeploysInto`/`UndeploysInto`. This behavior can be turned off per-TechnoType and global basis using `[SOMETECHNOTYPE]/[CombatDamage]->IronCurtain.KeptOnDeploy=no`.
-- The obsolete `[General] WarpIn` has been enabled for the default anim type when technos are warping in. If you want to restore the vanilla behavior, use the same anim type as `WarpOut`.
+- - The obsolete `[General] WarpIn` has been enabled for the default anim type when technos are warping in. If you want to restore the vanilla behavior, use the same anim type as `WarpOut`.
- Vehicles with `Crusher=true` + `OmniCrusher=true` / `MovementZone=CrusherAll` were hardcoded to tilt when crushing vehicles / walls respectively. This now obeys `TiltsWhenCrushes` but can be customized individually for these two scenarios using `TiltsWhenCrusher.Vehicles` and `TiltsWhenCrusher.Overlays`, which both default to `TiltsWhenCrushes`.
### From older Phobos versions
@@ -394,6 +395,10 @@ New:
- Toggleable height-based shadow scaling for voxel air units (by Trsdy & Starkku)
- User setting toggles for harvester counter & power delta indicator (by Starkku)
- Shrapnel weapon target filtering toggle (by Starkku)
+- Restore functionality of `[CrateRules]` -> `FreeMCV` with customizable credits threshold (by Starkku)
+- Allow customizing the number of vehicles required for unit crates to turn into money crates (by Starkku)
+- Per-VehicleType reroll chance for `CrateGoodie=true` (by Starkku)
+- Warheads spawning powerup crates (by Starkku)
Vanilla fixes:
- Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy)
diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp
index 58bb324be..987b1d7aa 100644
--- a/src/Ext/Rules/Body.cpp
+++ b/src/Ext/Rules/Body.cpp
@@ -137,6 +137,8 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
this->IronCurtain_KillOrganicsWarhead.Read(exINI, GameStrings::CombatDamage, "IronCurtain.KillOrganicsWarhead");
this->CrateOnlyOnLand.Read(exINI, GameStrings::CrateRules, "CrateOnlyOnLand");
+ this->UnitCrateVehicleCap.Read(exINI, GameStrings::CrateRules, "UnitCrateVehicleCap");
+ this->FreeMCV_CreditsThreshold.Read(exINI, GameStrings::CrateRules, "FreeMCV.CreditsThreshold");
this->ROF_RandomDelay.Read(exINI, GameStrings::CombatDamage, "ROF.RandomDelay");
@@ -287,6 +289,8 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->DisplayIncome_AllowAI)
.Process(this->DisplayIncome_Houses)
.Process(this->CrateOnlyOnLand)
+ .Process(this->UnitCrateVehicleCap)
+ .Process(this->FreeMCV_CreditsThreshold)
.Process(this->RadialIndicatorVisibility)
.Process(this->DrawTurretShadow)
.Process(this->IsVoiceCreatedGlobal)
diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h
index c134e46ed..5773bdd14 100644
--- a/src/Ext/Rules/Body.h
+++ b/src/Ext/Rules/Body.h
@@ -103,6 +103,8 @@ class RulesExt
Valueable ToolTip_Background_BlurSize;
Valueable CrateOnlyOnLand;
+ Valueable UnitCrateVehicleCap;
+ Valueable FreeMCV_CreditsThreshold;
Valueable RadialIndicatorVisibility;
Valueable DrawTurretShadow;
ValueableIdx AnimRemapDefaultColorScheme;
@@ -185,6 +187,8 @@ class RulesExt
, DisplayIncome_AllowAI { true }
, DisplayIncome_Houses { AffectedHouse::All }
, CrateOnlyOnLand { false }
+ , UnitCrateVehicleCap { 50 }
+ , FreeMCV_CreditsThreshold { 1500 }
, RadialIndicatorVisibility { AffectedHouse::Allies }
, DrawTurretShadow { false }
, IsVoiceCreatedGlobal { false }
diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp
index 28780eac4..b946295bc 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -279,6 +279,8 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->Convert_HumanToComputer.Read(exINI, pSection, "Convert.HumanToComputer");
this->Convert_ComputerToHuman.Read(exINI, pSection, "Convert.ComputerToHuman");
+ this->CrateGoodie_RerollChance.Read(exINI, pSection, "CrateGoodie.RerollChance");
+
// Ares 0.2
this->RadarJamRadius.Read(exINI, pSection, "RadarJamRadius");
@@ -605,6 +607,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm)
.Process(this->DroppodType)
.Process(this->Convert_HumanToComputer)
.Process(this->Convert_ComputerToHuman)
+
+ .Process(this->CrateGoodie_RerollChance)
;
}
void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm)
diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h
index 1e0c798b0..fea9f3424 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -191,6 +191,8 @@ class TechnoTypeExt
Valueable Convert_HumanToComputer;
Valueable Convert_ComputerToHuman;
+ Valueable CrateGoodie_RerollChance;
+
struct LaserTrailDataEntry
{
ValueableIdx idxType;
@@ -377,6 +379,8 @@ class TechnoTypeExt
, DroppodType {}
, Convert_HumanToComputer { }
, Convert_ComputerToHuman { }
+
+ , CrateGoodie_RerollChance { 0.0 }
{ }
virtual ~ExtData() = default;
diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp
index bc177cb1d..4e0a73eb4 100644
--- a/src/Ext/WarheadType/Body.cpp
+++ b/src/Ext/WarheadType/Body.cpp
@@ -243,6 +243,44 @@ void WarheadTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
|| this->InflictLocomotor
|| this->RemoveInflictedLocomotor
);
+
+ char tempBuffer[32];
+ Nullable crateType;
+ Nullable weight;
+
+ for (size_t i = 0; ; i++)
+ {
+ crateType.Reset();
+ weight.Reset();
+
+ _snprintf_s(tempBuffer, sizeof(tempBuffer), "SpawnsCrate%u.Type", i);
+ crateType.Read(exINI, pSection, tempBuffer);
+
+ if (i == 0 && !crateType.isset())
+ crateType.Read(exINI, pSection, "SpawnsCrate.Type");
+
+ if (!crateType.isset())
+ break;
+
+ if (this->SpawnsCrate_Types.size() < i)
+ this->SpawnsCrate_Types[i] = crateType;
+ else
+ this->SpawnsCrate_Types.push_back(crateType);
+
+ _snprintf_s(tempBuffer, sizeof(tempBuffer), "SpawnsCrate%u.Weight", i);
+ weight.Read(exINI, pSection, tempBuffer);
+
+ if (i == 0 && !weight.isset())
+ weight.Read(exINI, pSection, "SpawnsCrate.Weight");
+
+ if (!weight.isset())
+ weight = 1;
+
+ if (this->SpawnsCrate_Weights.size() < i)
+ this->SpawnsCrate_Weights[i] = weight;
+ else
+ this->SpawnsCrate_Weights.push_back(weight);
+ }
}
template
@@ -316,6 +354,9 @@ void WarheadTypeExt::ExtData::Serialize(T& Stm)
.Process(this->Shield_Respawn_Types)
.Process(this->Shield_SelfHealing_Types)
+ .Process(this->SpawnsCrate_Types)
+ .Process(this->SpawnsCrate_Weights)
+
.Process(this->NotHuman_DeathSequence)
.Process(this->LaunchSW)
.Process(this->LaunchSW_RealLaunch)
diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h
index f8936a3fe..d52c3cbeb 100644
--- a/src/Ext/WarheadType/Body.h
+++ b/src/Ext/WarheadType/Body.h
@@ -78,6 +78,9 @@ class WarheadTypeExt
Valueable Shield_SelfHealing_RestartInCombatDelay;
Valueable Shield_SelfHealing_RestartTimer;
+ std::vector SpawnsCrate_Types;
+ std::vector SpawnsCrate_Weights;
+
ValueableVector Shield_AttachTypes;
ValueableVector Shield_RemoveTypes;
Valueable Shield_ReplaceOnly;
@@ -115,7 +118,6 @@ class WarheadTypeExt
Valueable InflictLocomotor;
Valueable RemoveInflictedLocomotor;
-
// Ares tags
// http://ares-developers.github.io/Ares-docs/new/warheads/general.html
Valueable AffectsEnemies;
@@ -204,6 +206,9 @@ class WarheadTypeExt
, Shield_Respawn_Types {}
, Shield_SelfHealing_Types {}
+ , SpawnsCrate_Types {}
+ , SpawnsCrate_Weights {}
+
, NotHuman_DeathSequence { -1 }
, LaunchSW {}
, LaunchSW_RealLaunch { true }
diff --git a/src/Ext/WarheadType/Detonate.cpp b/src/Ext/WarheadType/Detonate.cpp
index ba6993749..bdc589157 100644
--- a/src/Ext/WarheadType/Detonate.cpp
+++ b/src/Ext/WarheadType/Detonate.cpp
@@ -62,6 +62,14 @@ void WarheadTypeExt::ExtData::Detonate(TechnoClass* pOwner, HouseClass* pHouse,
}
}
+ if (this->SpawnsCrate_Types.size() > 0)
+ {
+ int index = GeneralUtils::ChooseOneWeighted(ScenarioClass::Instance->Random.RandomDouble(), &this->SpawnsCrate_Weights);
+
+ if (index < static_cast(this->SpawnsCrate_Types.size()))
+ MapClass::Instance->PlacePowerupCrate(CellClass::Coord2Cell(coords), this->SpawnsCrate_Types.at(index));
+ }
+
for (const int swIdx : this->LaunchSW)
{
if (const auto pSuper = pHouse->Supers.GetItem(swIdx))
@@ -212,12 +220,12 @@ void WarheadTypeExt::ExtData::ApplyShieldModifiers(TechnoClass* pTarget)
if (pExt->Shield)
{
auto isShieldTypeEligible = [pExt](Iterator elements) -> bool
- {
- if (elements.size() > 0 && !elements.contains(pExt->Shield->GetType()))
- return false;
+ {
+ if (elements.size() > 0 && !elements.contains(pExt->Shield->GetType()))
+ return false;
- return true;
- };
+ return true;
+ };
if (this->Shield_Break && pExt->Shield->IsActive() && isShieldTypeEligible(this->Shield_Break_Types.GetElements(this->Shield_AffectTypes)))
pExt->Shield->BreakShield(this->Shield_BreakAnim.Get(nullptr), this->Shield_BreakWeapon.Get(nullptr));
diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp
index 5617137d5..7feaa7206 100644
--- a/src/Misc/Hooks.BugFixes.cpp
+++ b/src/Misc/Hooks.BugFixes.cpp
@@ -569,37 +569,6 @@ DEFINE_HOOK(0x70BCE6, TechnoClass_GetTargetCoords_BuildingFix, 0x6)
return 0;
}
-DEFINE_HOOK(0x56BD8B, MapClass_PlaceRandomCrate_Sampling, 0x5)
-{
- enum { SpawnCrate = 0x56BE7B, SkipSpawn = 0x56BE91 };
-
- int XP = 2 * MapClass::Instance->VisibleRect.X - MapClass::Instance->MapRect.Width
- + ScenarioClass::Instance->Random.RandomRanged(0, 2 * MapClass::Instance->VisibleRect.Width);
- int YP = 2 * MapClass::Instance->VisibleRect.Y + MapClass::Instance->MapRect.Width
- + ScenarioClass::Instance->Random.RandomRanged(0, 2 * MapClass::Instance->VisibleRect.Height + 2);
- CellStruct candidate { (short)((XP + YP) / 2),(short)((YP - XP) / 2) };
-
- auto pCell = MapClass::Instance->TryGetCellAt(candidate);
- if (!pCell)
- return SkipSpawn;
-
- if (!MapClass::Instance->IsWithinUsableArea(pCell, true))
- return SkipSpawn;
-
- bool isWater = pCell->LandType == LandType::Water;
- if (isWater && RulesExt::Global()->CrateOnlyOnLand.Get())
- return SkipSpawn;
-
- REF_STACK(CellStruct, cell, STACK_OFFSET(0x28, -0x18));
- cell = MapClass::Instance->NearByLocation(pCell->MapCoords,
- isWater ? SpeedType::Float : SpeedType::Track,
- -1, MovementZone::Normal, false, 1, 1, false, false, false, true, CellStruct::Empty, false, false);
-
- R->EAX(&cell);
-
- return SpawnCrate;
-}
-
// Fixes C4=no amphibious infantry being killed in water if Chronoshifted/Paradropped there.
DEFINE_HOOK(0x51A996, InfantryClass_PerCellProcess_KillOnImpassable, 0x5)
{
diff --git a/src/Misc/Hooks.Crates.cpp b/src/Misc/Hooks.Crates.cpp
new file mode 100644
index 000000000..b4285a419
--- /dev/null
+++ b/src/Misc/Hooks.Crates.cpp
@@ -0,0 +1,94 @@
+#include
+#include
+
+#include
+#include
+
+#include
+
+DEFINE_HOOK(0x56BD8B, MapClass_PlaceRandomCrate_Sampling, 0x5)
+{
+ enum { SpawnCrate = 0x56BE7B, SkipSpawn = 0x56BE91 };
+
+ int XP = 2 * MapClass::Instance->VisibleRect.X - MapClass::Instance->MapRect.Width
+ + ScenarioClass::Instance->Random.RandomRanged(0, 2 * MapClass::Instance->VisibleRect.Width);
+
+ int YP = 2 * MapClass::Instance->VisibleRect.Y + MapClass::Instance->MapRect.Width
+ + ScenarioClass::Instance->Random.RandomRanged(0, 2 * MapClass::Instance->VisibleRect.Height + 2);
+
+ CellStruct candidate { (short)((XP + YP) / 2),(short)((YP - XP) / 2) };
+ auto pCell = MapClass::Instance->TryGetCellAt(candidate);
+
+ if (!pCell)
+ return SkipSpawn;
+
+ if (!MapClass::Instance->IsWithinUsableArea(pCell, true))
+ return SkipSpawn;
+
+ bool isWater = pCell->LandType == LandType::Water;
+
+ if (isWater && RulesExt::Global()->CrateOnlyOnLand.Get())
+ return SkipSpawn;
+
+ REF_STACK(CellStruct, cell, STACK_OFFSET(0x28, -0x18));
+
+ cell = MapClass::Instance->NearByLocation(pCell->MapCoords,
+ isWater ? SpeedType::Float : SpeedType::Track,
+ -1, MovementZone::Normal, false, 1, 1, false, false, false, true, CellStruct::Empty, false, false);
+
+ R->EAX(&cell);
+
+ return SpawnCrate;
+}
+
+// Change RulesClass->FreeMCV default from 0 to 1.
+DEFINE_PATCH(0x6656B3, 0x89, 0x4E);
+
+DEFINE_HOOK(0x481BB8, CellClass_GoodieCheck_FreeMCV, 0x6)
+{
+ enum { SkipForcedMCV = 0x481C03, EnableForcedMCV = 0x481BF6 };
+
+ GET(HouseClass*, pHouse, EDI);
+ GET_STACK(UnitTypeClass*, pBaseUnit, STACK_OFFSET(0x188, -0x138));
+
+ if (RulesClass::Instance->FreeMCV && pHouse->Available_Money() > RulesExt::Global()->FreeMCV_CreditsThreshold &&
+ SessionClass::Instance->Config.Bases && !pHouse->OwnedBuildings && !pHouse->CountOwnedNow(pBaseUnit))
+ {
+ return EnableForcedMCV;
+ }
+
+ return SkipForcedMCV;
+}
+
+DEFINE_HOOK(0x481C27, CellClass_GoodieCheck_UnitCrateVehicleCap, 0x5)
+{
+ enum { Capped = 0x481C44, NotCapped = 0x481C4A };
+
+ GET(HouseClass*, pHouse, EDX);
+
+ if (RulesExt::Global()->UnitCrateVehicleCap < 0 || pHouse->OwnedUnits <= RulesExt::Global()->UnitCrateVehicleCap)
+ return NotCapped;
+
+ return Capped;
+}
+
+DEFINE_HOOK(0x4821BD, CellClass_GoodieCheck_CrateGoodie, 0x6)
+{
+ enum { SkipGameCode = 0x4821C3 };
+
+ GET(UnitTypeClass*, pUnitType, EDI);
+
+ bool crateGoodie = pUnitType->CrateGoodie;
+
+ if (crateGoodie)
+ {
+ auto const pTypeExt = TechnoTypeExt::ExtMap.Find(pUnitType);
+
+ if (pTypeExt->CrateGoodie_RerollChance > 0.0)
+ crateGoodie = pTypeExt->CrateGoodie_RerollChance < ScenarioClass::Instance->Random.RandomDouble();
+ }
+
+ R->CL(crateGoodie);
+
+ return SkipGameCode;
+}
diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h
index c158d0f05..c5e2202c7 100644
--- a/src/Utilities/TemplateDef.h
+++ b/src/Utilities/TemplateDef.h
@@ -47,6 +47,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -594,6 +595,39 @@ namespace detail
return false;
}
+ template <>
+ inline bool read(Powerup& value, INI_EX& parser, const char* pSection, const char* pKey)
+ {
+ if (parser.ReadString(pSection, pKey))
+ {
+ auto const& powerupNames = Powerups::Effects;
+ int index = -1;
+
+ for (size_t i = 0; i < powerupNames.size(); i++)
+ {
+ if (!_strcmpi(parser.value(), powerupNames[i]))
+ {
+ index = static_cast(i);
+ break;
+ }
+ }
+
+ if (index >= 0)
+ {
+ value = Powerup(index);
+ }
+ else
+ {
+ Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected a powerup crate type");
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
template <>
inline bool read(SuperWeaponAITargetingMode &value, INI_EX &parser, const char *pSection, const char *pKey)
{