diff --git a/CREDITS.md b/CREDITS.md
index 0cfbf108b3..284754e406 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -357,6 +357,7 @@ This page lists all the individual contributions to the project by their author.
- Fix the issue where computer players did not search for new enemies after defeating them or forming alliances with them
- Customize the damage taken when falling from a bridge
- `600 The shield of the attached object is broken` bug fix for the triggered event
+ - Can use more weapons
- **NetsuNegi**:
- Forbidding parallel AI queues by type
- Jumpjet crash speed fix when crashing onto building
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index e036e4902c..c5c6902e26 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -24,6 +24,8 @@
+
+
diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md
index 7d3f228f5f..9ccfa0bddc 100644
--- a/docs/New-or-Enhanced-Logics.md
+++ b/docs/New-or-Enhanced-Logics.md
@@ -1533,6 +1533,24 @@ MindControlLink.VisibleToHouse=all ; Affected House Enumeration (none|owner/s
MultiMindControl.ReleaseVictim=false ; boolean
```
+### Multi Weapon
+
+
+*Multi Weapon used to release different weapons against different targets in **Zero Boundary** by @[Stormsulfur](https://space.bilibili.com/11638715/lists/5358986)*
+
+- You are free to decide whether to use Weapon x or not, instead of passively using Primary/secondary.
+ - TechnoType reads `WeaponX` as their weapon when `MultiWeapon=yes`, be careful not to forget `WeaponCount`.
+ - `MultiWeapon.IsSecondary` can only be used for infantry and is responsible for determining which weapons should use `SecondaryFire` in the `Sequence`.
+ - `MultiWeapon.SelectWeapon` determines how many weapons can be selected. maximum value is 4, minimum value is 1, defaults to 2.
+
+In `rulesmd.ini`:
+```ini
+[SOMETECHNO] ; TechnoType
+MultiWeapon= ; boolean
+MultiWeapon.IsSecondary= ; List of integers
+MultiWeapon.SelectWeapon= ; integer
+```
+
### No Manual Move
- You can now specify whether a TechnoType is unable to receive move command.
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 7e34b3afe1..0f8eca348b 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -376,6 +376,7 @@ New:
- [Use `InsigniaType` to set the properties of insignia in a batch](Miscellanous.md#insignia-type) (by Ollerus)
- [Tiberium eater logic](New-or-Enhanced-Logics.md#tiberium-eater) (by NetsuNegi)
- [Customize the damage taken when falling from a bridge](Fixed-or-Improved-Logics.md#customize-bridge-falling-down-damage) (by FlyStar)
+- Can use more weapons (by FlyStar)
Vanilla fixes:
- Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya)
diff --git a/docs/_static/images/multiweapons.gif b/docs/_static/images/multiweapons.gif
new file mode 100644
index 0000000000..bd9bc560bd
Binary files /dev/null and b/docs/_static/images/multiweapons.gif differ
diff --git a/src/Ext/Techno/Body.MultiWeapon.cpp b/src/Ext/Techno/Body.MultiWeapon.cpp
new file mode 100644
index 0000000000..197e5fef40
--- /dev/null
+++ b/src/Ext/Techno/Body.MultiWeapon.cpp
@@ -0,0 +1,143 @@
+#include "Body.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+bool TechnoExt::CheckMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget, WeaponTypeClass* const pWeaponType)
+{
+ if (!pThis || !pTarget)
+ return false;
+
+ if (!pWeaponType || pWeaponType->NeverUse)
+ return false;
+
+ if (pThis->InOpenToppedTransport && !pWeaponType->FireInTransport)
+ return false;
+
+ const auto pBulletType = pWeaponType->Projectile;
+ const auto pWhatAmI = pTarget->WhatAmI();
+ const bool isBuilding = pWhatAmI != AbstractType::Building;
+ const auto pWH = pWeaponType->Warhead;
+ const auto pOwner = pThis->Owner;
+
+ if (pTarget->IsInAir())
+ {
+ if (!pBulletType->AA)
+ return false;
+
+ if (pWH->Airstrike)
+ return false;
+ }
+ else
+ {
+ const auto pBulletTypeExt = BulletTypeExt::ExtMap.Find(pBulletType);
+
+ if (pBulletTypeExt->AAOnly.Get())
+ {
+ return false;
+ }
+ else if(pWH->ElectricAssault)
+ {
+ if (isBuilding)
+ return false;
+
+ const auto pBuilding = static_cast(pTarget);
+
+ if (!pOwner->IsAlliedWith(pBuilding->Owner) ||
+ !pBuilding->Type || !pBuilding->Type->Overpowerable)
+ return false;
+ }
+ else if (pWH->IsLocomotor)
+ {
+ if (isBuilding)
+ return false;
+ }
+ }
+
+ if (pTarget->AbstractFlags & AbstractFlags::Techno)
+ {
+ TechnoClass* pTechno = static_cast(pTarget);
+
+ if (pTechno->Health <= 0 || !pTechno->IsAlive)
+ return false;
+
+ if (pTechno->AttachedBomb)
+ {
+ if (pWH->IvanBomb)
+ return false;
+ }
+ else if (pWH->BombDisarm)
+ {
+ return false;
+ }
+
+ const auto pTechnoType = pTechno->GetTechnoType();
+
+ if (pWH->MindControl && (pTechnoType->ImmuneToPsionics ||
+ pTechno->IsMindControlled() || pOwner == pTechno->Owner))
+ return false;
+
+ if (pWH->Parasite && (isBuilding ||
+ static_cast(pTechno)->ParasiteEatingMe))
+ return false;
+
+ if (!pWH->Temporal && pTechno->BeingWarpedOut)
+ return false;
+
+ if (pWeaponType->DrainWeapon && (!pTechnoType->Drainable ||
+ (pTechno->DrainingMe || pOwner->IsAlliedWith(pTechno->Owner))))
+ return false;
+
+ if (const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeaponType))
+ {
+ if (!pWeaponExt->HasRequiredAttachedEffects(pTechno, pThis))
+ return false;
+ }
+
+ const auto pTargetExt = TechnoExt::ExtMap.Find(pTechno);
+ const auto pShield = pTargetExt->Shield.get();
+
+ if (pShield && pShield->IsActive() &&
+ !pShield->CanBeTargeted(pWeaponType))
+ {
+ return false;
+ }
+ else if (GeneralUtils::GetWarheadVersusArmor(pWH, pTechno->GetTechnoType()->Armor) == 0.0)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (pWhatAmI == AbstractType::Terrain)
+ {
+ if (!pWH->Wood)
+ return false;
+ }
+ else if (pWhatAmI == AbstractType::Cell)
+ {
+ const auto pCell = static_cast(pTarget);
+
+ if (pCell && pCell->OverlayTypeIndex >= 0)
+ {
+ auto overlayType = OverlayTypeClass::Array.GetItem(pCell->OverlayTypeIndex);
+
+ if (overlayType->Wall && !pWH->Wall)
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h
index 11e4c189a7..c297ebd5e6 100644
--- a/src/Ext/Techno/Body.h
+++ b/src/Ext/Techno/Body.h
@@ -225,4 +225,6 @@ class TechnoExt
static int GetWeaponIndexAgainstWall(TechnoClass* pThis, OverlayTypeClass* pWallOverlayType);
static void ApplyKillWeapon(TechnoClass* pThis, TechnoClass* pSource, WarheadTypeClass* pWH);
static void ApplyRevengeWeapon(TechnoClass* pThis, TechnoClass* pSource, WarheadTypeClass* pWH);
+
+ static bool CheckMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget, WeaponTypeClass* const pWeaponType);
};
diff --git a/src/Ext/Techno/Hooks.Firing.cpp b/src/Ext/Techno/Hooks.Firing.cpp
index af7b5dce16..72bc50cf4c 100644
--- a/src/Ext/Techno/Hooks.Firing.cpp
+++ b/src/Ext/Techno/Hooks.Firing.cpp
@@ -41,6 +41,20 @@ DEFINE_HOOK(0x6F3339, TechnoClass_WhatWeaponShouldIUse_Interceptor, 0x8)
return SkipGameCode;
}
+DEFINE_HOOK(0x6F3360, TechnoClass_WhatWeaponShouldIUse_MultiWeapon, 0x6)
+{
+ GET(TechnoClass*, pThis, ESI);
+ enum { SkipGameCode = 0x6F3379 };
+
+ auto const pTypeExt = TechnoTypeExt::ExtMap.Find(pThis->GetTechnoType());
+
+ if (pTypeExt->MultiWeapon.Get() &&
+ (pThis->WhatAmI() != AbstractType::Unit || !pThis->GetTechnoType()->Gunner))
+ return SkipGameCode;
+
+ return 0;
+}
+
DEFINE_HOOK(0x6F33CD, TechnoClass_WhatWeaponShouldIUse_ForceFire, 0x6)
{
enum { ReturnWeaponIndex = 0x6F37AF };
@@ -124,6 +138,13 @@ DEFINE_HOOK(0x6F3428, TechnoClass_WhatWeaponShouldIUse_ForceWeapon, 0x6)
R->EAX(forceWeaponIndex);
return UseWeaponIndex;
}
+
+ int multiWeaponIndex = pTypeExt->SelectMultiWeapon(pThis, pTarget);
+ if (multiWeaponIndex >= 0)
+ {
+ R->EAX(multiWeaponIndex);
+ return UseWeaponIndex;
+ }
}
return 0;
diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp
index ecdd782c05..506ebc706b 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -33,6 +33,111 @@ void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor)
mtx->Translate(x, y, z);
}
+bool TechnoTypeExt::ExtData::IsSecondary(const int& nWeaponIndex)
+{
+ const auto pThis = this->OwnerObject();
+
+ if (pThis->IsGattling)
+ return nWeaponIndex != 0 && nWeaponIndex % 2 != 0;
+
+ if (this->MultiWeapon.Get() &&
+ !this->MultiWeapon_IsSecondary.empty())
+ {
+ int index = nWeaponIndex + 1;
+ return this->MultiWeapon_IsSecondary.Contains(index);
+ }
+
+ return nWeaponIndex != 0;
+}
+
+int TechnoTypeExt::ExtData::SelectMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget)
+{
+ if (!pThis || !pTarget)
+ return -1;
+
+ const auto pType = this->OwnerObject();
+ int WeaponCount = pType->WeaponCount;
+
+ if (WeaponCount > 0 && pType->HasMultipleTurrets() &&
+ (pType->IsGattling || pType->Gunner))
+ return -1;
+
+ if (!this->MultiWeapon.Get() || WeaponCount <= 2)
+ return -1;
+
+ // considering the issue of performance loss, it is sufficient to expand it to four.
+ int selectweaponCount = this->MultiWeapon_SelectWeapon.Get();
+ int weaponCount = WeaponCount;
+
+ if (weaponCount > selectweaponCount)
+ weaponCount = selectweaponCount;
+
+ if (weaponCount < 2)
+ return 0;
+ else if (weaponCount == 2)
+ return -1;
+ else if (weaponCount > 4)
+ weaponCount = 4;
+
+ bool isElite = pThis->Veterancy.IsElite();
+ const auto secondary = pType->GetWeapon(1, isElite).WeaponType;
+ const auto secondaryWH = secondary->Warhead;
+ bool secondaryCanTarget = TechnoExt::CheckMultiWeapon(pThis, pTarget, secondary);
+
+ if (const auto pTargetTechno = abstract_cast(pTarget))
+ {
+ if (secondaryCanTarget)
+ {
+ bool isAllies = pThis->Owner->IsAlliedWith(pTargetTechno->Owner);
+
+ if (pTarget->IsInAir())
+ return 1;
+ else if (secondaryWH->Airstrike)
+ return 1;
+ else if (secondary->DrainWeapon &&
+ pTargetTechno->GetTechnoType()->Drainable &&
+ !pThis->DrainTarget && !isAllies)
+ return 1;
+ else if (secondaryWH->ElectricAssault && isAllies &&
+ pTargetTechno->WhatAmI() == AbstractType::Building &&
+ static_cast(pTargetTechno)->Type->Overpowerable)
+ return 1;
+ }
+
+ if (const auto pCell = pTargetTechno->GetCell())
+ {
+ bool targetOnWater = pCell->LandType == LandType::Water || pCell->LandType == LandType::Beach;
+
+ if (!pTargetTechno->OnBridge && targetOnWater)
+ {
+ int result = pThis->SelectNavalTargeting(pTargetTechno);
+
+ if (result == -1)
+ return 1;
+ }
+ }
+ }
+
+ for (int i = 0; i < weaponCount; i++)
+ {
+ if (i == 1)
+ {
+ if (secondaryCanTarget)
+ return i;
+
+ continue;
+ }
+
+ const auto pWeaponType = pType->GetWeapon(i, isElite).WeaponType;
+ if (!TechnoExt::CheckMultiWeapon(pThis, pTarget, pWeaponType))
+ continue;
+
+ return i;
+ }
+
+ return 0;
+}
+
// Ares 0.A source
const char* TechnoTypeExt::ExtData::GetSelectionGroupID() const
{
@@ -60,7 +165,7 @@ void TechnoTypeExt::ExtData::ParseBurstFLHs(INI_EX& exArtINI, const char* pArtSe
char tempBuffer[32];
char tempBufferFLH[48];
auto pThis = this->OwnerObject();
- bool parseMultiWeapons = pThis->TurretCount > 0 && pThis->WeaponCount > 0;
+ bool parseMultiWeapons = this->MultiWeapon.Get() || (pThis->TurretCount > 0 && pThis->WeaponCount > 0);
auto weaponCount = parseMultiWeapons ? pThis->WeaponCount : 2;
nFLH.resize(weaponCount);
nEFlh.resize(weaponCount);
@@ -1114,6 +1219,11 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm)
.Process(this->FallingDownDamage)
.Process(this->FallingDownDamage_Water)
+
+ .Process(this->MultiWeapon)
+ .Process(this->MultiWeapon_IsSecondary)
+ .Process(this->MultiWeapon_SelectWeapon)
+ .Process(this->LastMultiWeapon)
;
}
void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm)
diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h
index 68dda0e8e1..52b109bcd7 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -351,6 +351,11 @@ class TechnoTypeExt
Valueable Harvester_CanGuardArea;
Nullable HarvesterScanAfterUnload;
+
+ Valueable MultiWeapon;
+ ValueableVector MultiWeapon_IsSecondary;
+ Valueable MultiWeapon_SelectWeapon;
+ bool LastMultiWeapon;
Valueable FallingDownDamage;
Nullable FallingDownDamage_Water;
@@ -656,12 +661,17 @@ class TechnoTypeExt
, Overload_DeathSound {}
, Overload_ParticleSys {}
, Overload_ParticleSysCount { 5 }
-
+
, Harvester_CanGuardArea { false }
, HarvesterScanAfterUnload {}
, FallingDownDamage { 1.0 }
, FallingDownDamage_Water {}
+
+ , MultiWeapon { false }
+ , MultiWeapon_IsSecondary {}
+ , MultiWeapon_SelectWeapon { 2 }
+ , LastMultiWeapon { false }
{ }
virtual ~ExtData() = default;
@@ -675,6 +685,9 @@ class TechnoTypeExt
void ApplyTurretOffset(Matrix3D* mtx, double factor = 1.0);
+ bool IsSecondary(const int& nWeaponIndex);
+ int SelectMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget);
+
// Ares 0.A
const char* GetSelectionGroupID() const;
diff --git a/src/Ext/TechnoType/Hooks.MultiWeapon.cpp b/src/Ext/TechnoType/Hooks.MultiWeapon.cpp
new file mode 100644
index 0000000000..86293d1cd0
--- /dev/null
+++ b/src/Ext/TechnoType/Hooks.MultiWeapon.cpp
@@ -0,0 +1,67 @@
+#include "Body.h"
+#include
+
+DEFINE_HOOK(0x7128B2, TechnoTypeClass_ReadINI_MultiWeapon, 0x6)
+{
+ GET(TechnoTypeClass*, pThis, EBP);
+ GET(CCINIClass*, pINI, ESI);
+ enum { ReadWeaponX = 0x7128C0 };
+
+ INI_EX exINI(pINI);
+ const char* pSection = pThis->ID;
+
+ const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pThis);
+ pTypeExt->MultiWeapon.Read(exINI, pSection, "MultiWeapon");
+ bool multiWeapon = pThis->HasMultipleTurrets() || pTypeExt->MultiWeapon.Get();
+
+ if (pTypeExt->LastMultiWeapon != multiWeapon)
+ {
+ auto clearWeapon = [pThis](int index)
+ {
+ auto& pWeapon = pThis->GetWeapon(index, false);
+ auto& pEliteWeapon = pThis->GetWeapon(index, true);
+
+ pWeapon = WeaponStruct();
+ pEliteWeapon = WeaponStruct();
+ };
+
+ clearWeapon(0);
+ clearWeapon(1);
+
+ pTypeExt->LastMultiWeapon = multiWeapon;
+ }
+
+ if (pTypeExt->MultiWeapon.Get())
+ {
+ pTypeExt->MultiWeapon_IsSecondary.Read(exINI, pSection, "MultiWeapon.IsSecondary");
+ pTypeExt->MultiWeapon_SelectWeapon.Read(exINI, pSection, "MultiWeapon.SelectWeapon");
+ }
+
+ return multiWeapon ? ReadWeaponX : 0;
+}
+
+DEFINE_HOOK(0x715B10, TechnoTypeClass_ReadINI_MultiWeapon2, 0x7)
+{
+ GET(TechnoTypeClass*, pThis, EBP);
+ enum { ReadWeaponX = 0x715B1F, Continue = 0x715B17 };
+
+ const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pThis);
+
+ if (pTypeExt->LastMultiWeapon)
+ return ReadWeaponX;
+
+ R->AL(pThis->HasMultipleTurrets());
+ return Continue;
+}
+
+// Do you think the infantry's way of determining that weapons are secondary is stupid ?
+// I think it's kind of stupid.
+DEFINE_HOOK(0x520888, InfantryClass_UpdateFiring_IsSecondary, 0x8)
+{
+ GET(InfantryClass*, pThis, EBP);
+ GET(int, weaponIdx, EDI);
+ enum { Primary = 0x5208D6, Secondary = 0x520890 };
+
+ R->AL(pThis->Crawling);
+ return TechnoTypeExt::ExtMap.Find(pThis->GetTechnoType())->IsSecondary(weaponIdx) ? Secondary : Primary;
+}
diff --git a/src/Ext/Unit/Hooks.Unload.cpp b/src/Ext/Unit/Hooks.Unload.cpp
index 5981aae963..12452e11be 100644
--- a/src/Ext/Unit/Hooks.Unload.cpp
+++ b/src/Ext/Unit/Hooks.Unload.cpp
@@ -290,5 +290,3 @@ DEFINE_HOOK(0x4DA9C1, FootClass_AI_DeployToLand, 0x6)
return 0;
}
-
-#pragma endregion