Skip to content

Commit

Permalink
Support arbitrary ammosets for siege artillery
Browse files Browse the repository at this point in the history
The vanilla logic around sieges is fairly inflexible -- it mostly assumes
that only 81mm mortars are used, which effectively prevents using
modded artillery in sieges without hacks like the "raider artillery"
in our VFE:S patch that consumes the 81mm ammoset.

So, rework our patches around sieges to support artillery pieces
with arbitrary ammosets, and remove raider artillery.
  • Loading branch information
mszabo-wikia committed Nov 2, 2024
1 parent 572ff7c commit fe1d73c
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 305 deletions.
1 change: 1 addition & 0 deletions Defs/Ammo/Shell/155mmHowitzer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
</statBases>
<ammoClass>Smoke</ammoClass>
<detonateProjectile>Bullet_155mmHowitzerShell_Smoke</detonateProjectile>
<spawnAsSiegeAmmo>false</spawnAsSiegeAmmo>
</ThingDef>

<!-- ================== Projectiles ================== -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,4 @@
<applyDamageToExplosionCellsNeighbors>true</applyDamageToExplosionCellsNeighbors>
</projectile>
</ThingDef>

<!-- ========== Raider Artillery (To circumvent the issue with vanilla shells.) ========== -->
<CombatExtended.AmmoSetDef>
<defName>AmmoSet_RaiderArtillery</defName>
<label>Raider Artillery</label>
<ammoTypes>
<Shell_HighExplosive>Bullet_155mmHowitzerShell_HE</Shell_HighExplosive>
<Shell_Incendiary>Bullet_155mmHowitzerShell_Incendiary</Shell_Incendiary>
<Shell_Toxic MayRequire="Ludeon.RimWorld.Biotech">Bullet_81mmMortarShell_Tox</Shell_Toxic>
</ammoTypes>
</CombatExtended.AmmoSetDef>

</Defs>

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,6 @@
</value>
</Operation>

<Operation Class="PatchOperationRemove">
<xpath>Defs/ThingDef[defName="VFES_Turret_Artillery"]/building/buildingTags</xpath>
</Operation>

<Operation Class="PatchOperationReplace">
<xpath>Defs/ThingDef[defName="VFES_Turret_Artillery"]/building/turretBurstWarmupTime</xpath>
<value>
Expand Down
6 changes: 0 additions & 6 deletions Source/CombatExtended/CombatExtended/AmmoUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,6 @@ public static string GetProjectileReadout(this ThingDef projectileDef, Thing wea
/// </summary>
public static float GetExplosionArmorPenetration(this CompProperties_ExplosiveCE props) => props.damageAmountBase * ExplosiveArmorPenetrationMultiplier;

public static bool IsShell(ThingDef def)
{
var ammo = ThingDefOf.Turret_Mortar.building.turretGunDef.GetCompProperties<CompProperties_AmmoUser>();
return ammo?.ammoSet.ammoTypes.Any(l => l.ammo == def) ?? false;
}

public static bool IsAmmoSystemActive(AmmoDef def)
{
if (Controller.settings.EnableAmmoSystem)
Expand Down
150 changes: 103 additions & 47 deletions Source/CombatExtended/Harmony/Harmony_LordToil_Siege.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,69 +5,125 @@
using HarmonyLib;
using RimWorld;
using Verse;
using Verse.AI.Group;

namespace CombatExtended.HarmonyCE
{
/// <summary>
/// Transpile LordToil_Siege to support artillery pieces with different ammosets than the 81mm mortar.
/// </summary>
[HarmonyPatch(typeof(LordToil_Siege), "LordToilTick")]
internal static class Harmony_LordToil_Siege
{
internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions,
ILGenerator generator)
{
var codes = new List<CodeInstruction>(instructions);
var methodCustomCondition = typeof(Harmony_LordToil_Siege).GetMethod(nameof(CustomCondition), BindingFlags.Static | BindingFlags.Public);
var startIndex = -1;
var endIndex = -1;
MethodInfo targetMethod = AccessTools.Method(typeof(ThingDef), "get_IsShell"); // Start of the if condition
FieldInfo targetField = AccessTools.Field(typeof(DamageDef), "harmsHealth"); // End of the if condition
for (int i = 0; i < codes.Count; i++)
{
if (startIndex == -1)
{
// Find the start of the if statement
if (codes[i].opcode == OpCodes.Callvirt && codes[i].Calls(targetMethod))
{
startIndex = i - 1; // We want to edit -1 instruction from the call of IsShell
}
}
else if (codes[i].opcode == OpCodes.Ldfld && codes[i].LoadsField(targetField))
{
endIndex = i;
}
}
if (startIndex == -1 || endIndex == -1)
{
Log.Error("CombatExtended :: Harmony_LordToil_Siege couldn't find code block for patching");
return codes; // Don't modify as we couldn't find the original code block
}
else
{
codes[startIndex] = new CodeInstruction(OpCodes.Call, methodCustomCondition); // Call the new method for evaluation
codes.RemoveRange(startIndex + 1, endIndex - startIndex); // Remove the default code
return codes;
}
var methodCustomCondition = typeof(Harmony_LordToil_Siege).GetMethod(nameof(IsValidShellType),
BindingFlags.Static | BindingFlags.Public);
var dropAdditionalShells = AccessTools.Method(typeof(Harmony_LordToil_Siege), nameof(DropAdditionalShells));

var isShellMethod = AccessTools.PropertyGetter(typeof(ThingDef), nameof(ThingDef.IsShell));
var harmsHealthField = AccessTools.Field(typeof(DamageDef), nameof(DamageDef.harmsHealth));
var tryFindRandomShellDefMethod =
AccessTools.Method(typeof(TurretGunUtility), nameof(TurretGunUtility.TryFindRandomShellDef));

var codeMatcher = new CodeMatcher(instructions, generator);

var isShellPos = codeMatcher
.Start()
.MatchStartForward(CodeMatch.Calls(isShellMethod))
.ThrowIfInvalid("CombatExtended :: Harmony_LordToil_Siege couldn't find call to IsShell")
.Pos;

var harmsHealthPos = codeMatcher.MatchStartForward(CodeMatch.LoadsField(harmsHealthField))
.ThrowIfInvalid("CombatExtended :: Harmony_LordToil_Siege couldn't find harmsHealth")
.Pos;

// Consider all shell types usable by the siege when assessing how many are available to the siege
codeMatcher
.Advance(isShellPos - harmsHealthPos - 1)
.Insert(
CodeInstruction.LoadArgument(0),
new CodeInstruction(OpCodes.Call, methodCustomCondition)
)
.RemoveInstructionsInRange(isShellPos + 1, harmsHealthPos + 2);

var ifBlockStart = codeMatcher.Start()
.MatchStartForward(CodeMatch.Calls(tryFindRandomShellDefMethod))
.ThrowIfInvalid("CombatExtended :: Harmony_LordToil_Siege couldn't find call to TryFindRandomShellDef")
.MatchEndBackwards(CodeMatch.Branches())
.ThrowIfInvalid("CombatExtended :: Harmony_LordToil_Siege couldn't find start of enclosing if block")
.Pos;

var ifBlockEndLabel = (Label)codeMatcher.Operand;

// Drop shells for every type of artillery piece used by the siege
codeMatcher.MatchStartForward(new CodeMatch(instruction => instruction.labels.Contains(ifBlockEndLabel)))
.ThrowIfInvalid("CombatExtended :: Harmony_LordToil_Siege couldn't find end of enclosing if block")
.Insert(
CodeInstruction.LoadArgument(0),
new CodeInstruction(OpCodes.Call, dropAdditionalShells)
)
.RemoveInstructionsInRange(ifBlockStart + 1, codeMatcher.Pos - 1);

return codeMatcher.Instructions();
}

public static bool CustomCondition(Thing thing)
/// <summary>
/// Determine whether the given thing is a valid shell usable by this siege.
/// </summary>
/// <param name="thing">The thing to check.</param>
/// <param name="siege">The siege.</param>
/// <returns>true if at least one type of artillery piece taking part in the siege can use this shell, false otherwise.</returns>
public static bool IsValidShellType(Thing thing, LordToil_Siege siege)
{
// Ensure check is the same here as from CombatExtended.HarmonyCE.Harmony_TurretGunUtility
var ammoDef = thing.def as AmmoDef;
if (ammoDef == null)
if (thing.def is AmmoDef { spawnAsSiegeAmmo: true } ammoDef)
{
return false;
return UniqueArtilleryDefs(siege)
.SelectMany(def => def.building.turretGunDef.comps)
.OfType<CompProperties_AmmoUser>()
.SelectMany(props => props.ammoSet.ammoTypes)
.Any(ammoLink => ammoLink.ammo == ammoDef);
}

// Ignore all non-shell defs.
if (ammoDef == null || !AmmoUtility.IsShell(ammoDef))
{
return false;
}
return false;
}

/// <summary>
/// Supply additional shells for each type of artillery piece taking part in this siege.
/// </summary>
/// <param name="siege">The siege to resupply.</param>
public static void DropAdditionalShells(LordToil_Siege siege)
{
Lord lord = siege.lord;

// Check if shell is blacklisted
if (!ammoDef.spawnAsSiegeAmmo)
foreach (var uniqueMortarDef in UniqueArtilleryDefs(siege))
{
return false;
var shellDef = TurretGunUtility.TryFindRandomShellDef(
uniqueMortarDef,
allowEMP: false,
allowToxGas: false,
mustHarmHealth: true,
lord.faction.def.techLevel,
allowAntigrainWarhead: false,
faction: lord.faction
);

if (shellDef != null)
{
siege.DropSupplies(shellDef, LordToil_Siege.ShellReplenishCount);
}
}
return true;
}

/// <summary>
/// Get the unique artillery types taking part in this siege.
/// </summary>
/// <param name="siege">The siege to get artillery types for.</param>
/// <returns>Enumerable of unique artillery defs taking part in this siege.</returns>
private static IEnumerable<ThingDef> UniqueArtilleryDefs(LordToil_Siege siege) => siege.lord.ownedBuildings
.Select(t => t.def)
.Where(def => def.building.buildingTags.Contains("Artillery"))
.Distinct();
}
}
17 changes: 0 additions & 17 deletions Source/CombatExtended/Harmony/Harmony_ThingListGroupHelper.cs

This file was deleted.

Loading

0 comments on commit fe1d73c

Please sign in to comment.