Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Dubs Paint Shop | [![url][steam]](https://steamcommunity.com/sharedfiles/filedet
Eccentric Tech - Fusion Power | [![url][steam]](https://steamcommunity.com/sharedfiles/filedetails/?id=2742125879)
Enhanced Vat Learning | [![url][steam]](https://steamcommunity.com/sharedfiles/filedetails/?id=2908453851) [![url][repo]](https://github.com/SmArtKar/EnhancedVatLearning)
Extended Bioengineering for VFE Insectoids | [![url][steam]](https://steamcommunity.com/sharedfiles/filedetails/?id=2706534548)
Exosuit Framework | [![url][steam]](https://steamcommunity.com/sharedfiles/filedetails/?id=3352894993) [![url][repo]](https://github.com/AobaKuma/MechsuitFramework)
Fertile Fields | [![url][steam]](https://steamcommunity.com/sharedfiles/filedetails/?id=2012735237)
Follow Me | [![url][steam]](https://steamcommunity.com/sharedfiles/filedetails/?id=715759739) [![url][repo]](https://github.com/fluffy-mods/FollowMe)
Gastronomy | [![url][steam]](https://steamcommunity.com/sharedfiles/filedetails/?id=2279786905) [![url][repo]](https://github.com/OrionFive/Gastronomy)
Expand Down
457 changes: 423 additions & 34 deletions Source/Mods/ExosuitFramework.cs

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions Source/Mods/MoveWhileReloading.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Linq;
using HarmonyLib;
using Multiplayer.API;
using RimWorld;
using Verse;
using Verse.AI;

namespace Multiplayer.Compat;

/// <summary>CE - Move while reloading (Continue) by himawari</summary>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this was about Exosuit Framework.

/// <see href="https://steamcommunity.com/sharedfiles/filedetails/?id=3551361813"/>
[MpCompatFor("himawari.moveWhileReloading")]
public class MoveWhileReloadingCompat
{
private static JobDef reloadWeaponJobDef;

public MoveWhileReloadingCompat(ModContentPack mod)
{
LongEventHandler.ExecuteWhenFinished(LatePatch);
}

private static void LatePatch()
{
try
{
reloadWeaponJobDef = GetCeReloadJobDef();
EnsureRunAndGunBurstShotPatch();
PatchPawnGotoForMultiplayer();
Log.Message("MPCompat :: Initialized compatibility for himawari.moveWhileReloading");
}
catch (System.Exception ex)
{
Log.Warning($"MPCompat :: Failed to finish Move While Reloading compat setup: {ex}");
}
}

/// <summary>
/// CEMoveReload only checks roolo.RunAndGun at static init; kotobike/memegoddess forks never get TryCastNextBurstShot patched.
/// </summary>
private static void EnsureRunAndGunBurstShotPatch()
{
if (!IsAnyRunAndGunActive())
return;

var verbMethod = AccessTools.Method(typeof(Verb), nameof(Verb.TryCastNextBurstShot));
var patchesType = AccessTools.TypeByName("CEMoveReload.HarmonyPatches");
var prefix = patchesType != null
? AccessTools.Method(patchesType, "Prefix_TryCastNextBurstShot")
: null;
if (verbMethod == null || prefix == null)
return;

var patchInfo = Harmony.GetPatchInfo(verbMethod);
if (patchInfo?.Prefixes?.Any(p => p.PatchMethod == prefix) == true)
return;

MpCompat.harmony.Patch(verbMethod, prefix: new HarmonyMethod(prefix));
}

private static void PatchPawnGotoForMultiplayer()
{
var patchesType = AccessTools.TypeByName("CEMoveReload.HarmonyPatches");
var ceGotoPrefix = patchesType != null
? AccessTools.Method(patchesType, "Prefix_PawnGotoAction")
: null;
if (ceGotoPrefix == null)
return;

MpCompat.harmony.Patch(ceGotoPrefix,
prefix: new HarmonyMethod(typeof(MoveWhileReloadingCompat), nameof(SkipCeGotoWhenNotReloading)));
}

/// <summary>
/// Outside reload, CEMoveReload replaces FloatMenuMakerMap.PawnGotoAction with unsynced TryTakeOrderedJob calls.
/// Skip its prefix in MP so vanilla/MP job sync handles movement; keep reload-specific behavior intact.
/// </summary>
private static bool SkipCeGotoWhenNotReloading(Pawn pawn)
{
if (!MP.IsInMultiplayer)
return true;

reloadWeaponJobDef ??= GetCeReloadJobDef();
if (reloadWeaponJobDef != null && pawn.CurJobDef == reloadWeaponJobDef)
return true;

return false;
}

private static JobDef GetCeReloadJobDef()
{
var ceJobDefType = AccessTools.TypeByName("CombatExtended.CE_JobDefOf");
if (ceJobDefType != null)
{
var field = AccessTools.Field(ceJobDefType, "ReloadWeapon");
if (field != null)
return field.GetValue(null) as JobDef;
}

return DefDatabase<JobDef>.GetNamedSilentFail("ReloadWeapon");
}

private static bool IsAnyRunAndGunActive()
{
foreach (var mod in LoadedModManager.RunningMods)
{
var id = mod.PackageId.NoModIdSuffix().ToLower();
if (id is "roolo.runandgun" or "roolo.runandgun.kotobike" or "memegoddess.runandgun")
return true;
}

return false;
}
}
220 changes: 217 additions & 3 deletions Source/Mods/RunandGun.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,236 @@
using System;
using Verse;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Multiplayer.API;
using RimWorld;
using UnityEngine;
using Verse;

namespace Multiplayer.Compat
{
/// <summary>RunAndGun by roolo</summary>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RunAndGun?

/// <see href="https://github.com/rheirman/RunAndGun"/>
/// <see href="https://github.com/MemeGoddess/RunAndGun"/>
/// <see href="https://steamcommunity.com/sharedfiles/filedetails/?id=1204108550"/>
/// <see href="https://steamcommunity.com/sharedfiles/filedetails/?id=3523879860"/>
[MpCompatFor("roolo.RunAndGun")]
[MpCompatFor("roolo.RunAndGun.kotobike")]
[MpCompatFor("memegoddess.RunAndGun")]
class RunandGun
{
private const string GizmoPatch = "RunAndGun.Harmony.Pawn_DraftController_GetGizmos_Patch";
private const string RunAndGunIconPath = "UI/Buttons/enable_RG";

private static FieldInfo compIsEnabledField;
private static FieldInfo weaponForbidderField;
private static Texture2D runAndGunIcon;

public RunandGun(ModContentPack mod)
{
MpCompat.RegisterLambdaDelegate("RunAndGun.Harmony.Pawn_DraftController_GetGizmos_Patch", "Postfix", 2);
PatchingUtilities.PatchUnityRand("RunAndGun.Harmony.MentalStateHandler_TryStartMentalState:shouldRunAndGun", false);
RegisterToggleSync();
PatchPawnGetGizmos();
PatchMentalStateRand();
}

private static void PatchMentalStateRand()
{
var patchType = AccessTools.TypeByName("RunAndGun.Harmony.MentalStateHandler_TryStartMentalState");
if (patchType == null)
return;

MethodInfo target = null;
try
{
target = MpMethodUtil.GetLocalFunc(patchType, "Postfix", localFunc: "shouldRunAndGun");
}
catch (Exception)
{
// Roslyn local-function name may differ between builds
}

target ??= AccessTools.GetDeclaredMethods(patchType)
.FirstOrDefault(m => m.Name.Contains("shouldRunAndGun", StringComparison.Ordinal));

if (target != null)
PatchingUtilities.PatchSystemRand(target, false);
}

/// <summary>Fallback gizmo only — no MP.WatchBegin here (runs every GUI frame and breaks combat sync).</summary>
private static void PatchPawnGetGizmos()
{
var getGizmos = AccessTools.Method(typeof(Pawn), nameof(Pawn.GetGizmos));
MpCompat.harmony.Patch(getGizmos,
postfix: new HarmonyMethod(typeof(RunandGun), nameof(PawnGetGizmosPostfix)));
}

/// <summary>
/// Sync delegate must include the captured CompRunAndGun ("data") or the host cannot apply OFF.
/// kotobike/memegoddess: toggle is &lt;Postfix&gt;b__1; roolo 1.4 uses lambda ordinal 2.
/// </summary>
private static void RegisterToggleSync()
{
var patchType = AccessTools.TypeByName(GizmoPatch);
if (patchType == null)
return;

var compType = AccessTools.TypeByName("RunAndGun.CompRunAndGun");
if (compType != null)
MP.RegisterSyncField(compType, "isEnabled");

MP.RegisterSyncMethod(typeof(RunandGun), nameof(SyncSetRunAndGunEnabled));

string[] closureFields = ["data"];
var registered = false;

try
{
MP.RegisterSyncDelegate(patchType, "<>c__DisplayClass0_0", "<Postfix>b__1", closureFields);
registered = true;
}
catch (Exception)
{
// display class name differs between builds
}

foreach (var ord in new[] { 1, 2 })
{
try
{
MpCompat.RegisterLambdaDelegate(GizmoPatch, "Postfix", closureFields, ord);
registered = true;
}
catch (Exception)
{
// try next ordinal
}
}

if (!registered)
Log.Warning("MPCompat :: RunAndGun toggle lambda not found (tried direct delegate and ordinals 1, 2)");
}

public static void SyncSetRunAndGunEnabled(Pawn pawn, bool enabled)
{
if (pawn == null)
return;

foreach (var comp in pawn.AllComps)
{
if (comp.GetType().Name != "CompRunAndGun")
continue;

compIsEnabledField ??= AccessTools.Field(comp.GetType(), "isEnabled");
compIsEnabledField?.SetValue(comp, enabled);
return;
}
}

private static void PawnGetGizmosPostfix(Pawn __instance, ref IEnumerable<Gizmo> __result)
{
if (!MP.IsInMultiplayer || __instance == null || !__instance.Drafted)
return;

try
{
if (__instance.Faction != Faction.OfPlayer || !PawnHasRangedWeapon(__instance))
return;

var comp = GetRunAndGunComp(__instance);
if (comp == null || IsWeaponForbidden(__instance))
return;

var list = __result?.ToList() ?? new List<Gizmo>();
if (HasRunAndGunGizmo(list))
{
__result = list;
return;
}

runAndGunIcon ??= ContentFinder<Texture2D>.Get(RunAndGunIconPath, true);
var isEnabled = GetRunAndGunEnabled(comp);
list.Add(new Command_Toggle
{
defaultLabel = "RG_Action_Enable_Label".Translate(),
defaultDesc = (isEnabled ? "RG_Action_Disable_Description" : "RG_Action_Enable_Description").Translate(),
icon = runAndGunIcon,
isActive = () => GetRunAndGunEnabled(comp),
toggleAction = () => SyncSetRunAndGunEnabled(__instance, !GetRunAndGunEnabled(comp)),
});
__result = list;
}
catch (Exception ex)
{
Log.Warning($"MPCompat :: RunAndGun pawn GetGizmos compat skipped: {ex.Message}");
}
}

private static ThingComp GetRunAndGunComp(Pawn pawn)
{
foreach (var comp in pawn.AllComps)
{
if (comp.GetType().Name == "CompRunAndGun")
return comp;
}

return null;
}

private static bool GetRunAndGunEnabled(ThingComp comp)
{
compIsEnabledField ??= AccessTools.Field(comp.GetType(), "isEnabled");
return compIsEnabledField != null && (bool)compIsEnabledField.GetValue(comp);
}

private static bool HasRunAndGunGizmo(List<Gizmo> gizmos)
{
runAndGunIcon ??= ContentFinder<Texture2D>.Get(RunAndGunIconPath, true);
if (runAndGunIcon == null)
return false;

var iconName = runAndGunIcon.name;
return gizmos.OfType<Command_Toggle>().Any(g => g.icon != null && g.icon.name == iconName);
}

private static bool PawnHasRangedWeapon(Pawn pawn)
{
var primary = pawn.equipment?.Primary;
return primary?.def?.IsRangedWeapon == true;
}

private static bool IsWeaponForbidden(Pawn pawn)
{
try
{
if (pawn.equipment?.Primary == null)
return false;

var baseType = AccessTools.TypeByName("RunAndGun.Base");
if (baseType == null)
return false;

weaponForbidderField ??= AccessTools.Field(baseType, "weaponForbidder");
var forbidder = weaponForbidderField?.GetValue(null);
if (forbidder == null)
return false;

var innerListField = AccessTools.Field(forbidder.GetType(), "InnerList");
var innerList = innerListField?.GetValue(forbidder) as System.Collections.IDictionary;
if (innerList == null)
return false;

if (!innerList.Contains(pawn.equipment.Primary.def.defName))
return false;

var record = innerList[pawn.equipment.Primary.def.defName];
var isSelectedField = AccessTools.Field(record.GetType(), "isSelected");
return isSelectedField != null && (bool)isSelectedField.GetValue(record);
}
catch
{
return false;
}
}
}
}
Loading