diff --git a/About/ModIcon.png b/About/ModIcon.png
new file mode 100644
index 00000000..a2d0ad41
Binary files /dev/null and b/About/ModIcon.png differ
diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj
index a38b0088..0c443666 100644
--- a/Source/Client/Multiplayer.csproj
+++ b/Source/Client/Multiplayer.csproj
@@ -32,7 +32,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs
index df943b0b..34ce2342 100644
--- a/Source/Client/MultiplayerStatic.cs
+++ b/Source/Client/MultiplayerStatic.cs
@@ -353,7 +353,8 @@ void TryPatch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod po
var canEverSpectate = typeof(RitualRoleAssignments).GetMethod(nameof(RitualRoleAssignments.CanEverSpectate));
var effectMethods = new MethodBase[] { subSustainerStart, sampleCtor, subSoundPlay, effecterTick, effecterTrigger, effecterCleanup, randomBoltMesh, drawTrackerCtor, randomHair };
- var moteMethods = typeof(MoteMaker).GetMethods(BindingFlags.Static | BindingFlags.Public);
+ var moteMethods = typeof(MoteMaker).GetMethods(BindingFlags.Static | BindingFlags.Public)
+ .Concat(typeof(CompCableConnection.Cable).GetMethod(nameof(CompCableConnection.Cable.Tick)));
var fleckMethods = typeof(FleckMaker).GetMethods(BindingFlags.Static | BindingFlags.Public)
.Where(m => m.ReturnType == typeof(void))
.Concat(typeof(FleckManager).GetMethods() // FleckStatic uses Rand in Setup method, FleckThrown uses RandomInRange in TimeInterval. May as well catch all in case mods do the same.
diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs
index 90c81df5..8acb9feb 100644
--- a/Source/Client/Patches/Determinism.cs
+++ b/Source/Client/Patches/Determinism.cs
@@ -15,16 +15,19 @@
namespace Multiplayer.Client.Patches
{
+ [EarlyPatch]
[HarmonyPatch(typeof(PawnTweener))]
[HarmonyPatch(nameof(PawnTweener.TweenedPos), MethodType.Getter)]
static class DrawPosPatch
{
- static bool Prefix() => Multiplayer.Client == null || Multiplayer.InInterface;
+ public static bool returnTruePosition = false;
+
+ static bool Prefix() => Multiplayer.Client == null || Multiplayer.InInterface || returnTruePosition;
// Give the root position during ticking
static void Postfix(PawnTweener __instance, ref Vector3 __result)
{
- if (Multiplayer.Client == null || Multiplayer.InInterface) return;
+ if (Multiplayer.Client == null || Multiplayer.InInterface || returnTruePosition) return;
__result = __instance.TweenedPosRoot();
}
}
@@ -524,6 +527,66 @@ static IEnumerable Transpiler(IEnumerable inst
}
}
+ [HarmonyPatch(typeof(UndercaveMapComponent), nameof(UndercaveMapComponent.MapComponentTick))]
+ static class DeterministicUndercaveRockCollapse
+ {
+ static IEnumerable Transpiler(IEnumerable instr)
+ {
+ var target = MethodOf.Lambda(Rand.MTBEventOccurs);
+
+ foreach (var ci in instr)
+ {
+ yield return ci;
+
+ // Add "& false" to any call to Rand.MTBEventOccurs.
+ // We'll handle those calls in our postfix.
+ if (ci.Calls(target))
+ {
+ yield return new CodeInstruction(OpCodes.Ldc_I4_0);
+ yield return new CodeInstruction(OpCodes.And);
+ }
+ }
+ }
+
+ static void Prefix() => Rand.PushState();
+
+ static void Postfix(UndercaveMapComponent __instance)
+ {
+ // Pop the RNG state from the prefix
+ Rand.PopState();
+
+ // Make sure the pit gate is collapsing
+ if (__instance.pitGate is not { IsCollapsing: true })
+ return;
+
+ // Check if the rocks should collapse
+ var mtb = UndercaveMapComponent.HoursToShakeMTBTicksCurve.Evaluate(__instance.pitGate.TicksUntilCollapse / 2500f);
+ if (!Rand.MTBEventOccurs(mtb, 1, 1))
+ return;
+
+ // Since the number of RNG calls will depend on numDustEffecters argument, we need to push/pop the RNG state.
+ // The RNG calls related to simulation will happen first, followed by the one determined by amount of
+ // effecters - it would not be MP safe, but since it happens last it will be fine once we pop the state.
+ Rand.PushState();
+
+ // If not looking at the map, trigger the collapse without shake/effecters (since it's not needed for current player).
+ // The call to play a sound is handled by RW itself, since it targets a specific map already.
+ if (Find.CurrentMap != __instance.map)
+ {
+ // Progress the RNG state, matching the RandomInRange call in other two cases
+ Rand.RangeInclusive(0, 100);
+ __instance.TriggerCollapseFX(0, 0);
+ }
+ // Else, follow vanilla shake/effecter rules
+ else if (__instance.pitGate.CollapseStage == 1)
+ __instance.TriggerCollapseFX(UndercaveMapComponent.StageOneShakeAmount, UndercaveMapComponent.StageOneNumCollapseEffects.RandomInRange);
+ else
+ __instance.TriggerCollapseFX(UndercaveMapComponent.StageTwoShakeAmount, UndercaveMapComponent.StageTwoNumCollapseEffects.RandomInRange);
+
+ Rand.PopState();
+ }
+ }
+
[HarmonyPatch(typeof(CompObelisk_Abductor), nameof(CompObelisk_Abductor.GenerateLabyrinth))]
static class SynchronousAbductorObelisk
{
diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs
index a0c35a87..37e4e25c 100644
--- a/Source/Client/Patches/Patches.cs
+++ b/Source/Client/Patches/Patches.cs
@@ -9,6 +9,7 @@
using System.Reflection.Emit;
using System.Text.RegularExpressions;
using System.Xml.Linq;
+using Multiplayer.Client.Patches;
using UnityEngine;
using Verse;
using Verse.AI;
@@ -581,4 +582,12 @@ static void FixStorage(IStoreSettingsParent __instance, StorageSettings ___allow
___allowedNutritionSettings.owner ??= __instance;
}
}
+
+ [HarmonyPatch(typeof(MoteAttachLink), nameof(MoteAttachLink.UpdateDrawPos))]
+ static class MoteAttachLinkUsesTruePosition
+ {
+ static void Prefix() => DrawPosPatch.returnTruePosition = true;
+
+ static void Finalizer() => DrawPosPatch.returnTruePosition = false;
+ }
}
diff --git a/Source/Client/Patches/Seeds.cs b/Source/Client/Patches/Seeds.cs
index d4301500..cef7d7d3 100644
--- a/Source/Client/Patches/Seeds.cs
+++ b/Source/Client/Patches/Seeds.cs
@@ -98,11 +98,12 @@ static void Prefix(ref Action action)
{
if (Multiplayer.Client != null && (Multiplayer.Ticking || Multiplayer.ExecutingCmds))
{
- action = PushState + action + Rand.PopState;
+ var seed = Rand.Int;
+ action = (() => PushState(seed)) + action + Rand.PopState;
}
}
- static void PushState() => Rand.PushState(4);
+ static void PushState(int seed) => Rand.PushState(seed);
}
// Seed the rotation random
diff --git a/Source/Client/Patches/UniqueIds.cs b/Source/Client/Patches/UniqueIds.cs
index c2591c1d..cc96246e 100644
--- a/Source/Client/Patches/UniqueIds.cs
+++ b/Source/Client/Patches/UniqueIds.cs
@@ -13,16 +13,19 @@ public static class UniqueIdsPatch
// Start at -2 because -1 is sometimes used as the uninitialized marker
private static int localIds = -2;
+ public static bool useLocalIdsOverride;
+
+ private static bool UseLocalIds =>
+ Multiplayer.Client != null && (useLocalIdsOverride || Multiplayer.InInterface || Current.ProgramState == ProgramState.Entry);
+
static bool Prefix()
{
- return Multiplayer.Client == null || (!Multiplayer.InInterface && Current.ProgramState != ProgramState.Entry);
+ return !UseLocalIds;
}
static void Postfix(ref int __result)
{
- if (Multiplayer.Client == null) return;
-
- if (Multiplayer.InInterface || Current.ProgramState == ProgramState.Entry)
+ if (UseLocalIds)
__result = localIds--;
}
}
diff --git a/Source/Client/Persistent/CaravanFormingPatches.cs b/Source/Client/Persistent/CaravanFormingPatches.cs
index 18922d96..9dab7444 100644
--- a/Source/Client/Persistent/CaravanFormingPatches.cs
+++ b/Source/Client/Persistent/CaravanFormingPatches.cs
@@ -2,6 +2,8 @@
using RimWorld;
using RimWorld.Planet;
using System;
+using Multiplayer.API;
+using Multiplayer.Client.Patches;
using UnityEngine;
using Verse;
using static Verse.Widgets;
@@ -147,7 +149,7 @@ static class CancelDialogFormCaravan
{
static bool Prefix(Window window)
{
- if (Multiplayer.MapContext != null && window.GetType() == typeof(Dialog_FormCaravan))
+ if (Multiplayer.Client != null && window.GetType() == typeof(Dialog_FormCaravan))
return false;
return true;
@@ -155,21 +157,76 @@ static bool Prefix(Window window)
}
[HarmonyPatch(typeof(Dialog_FormCaravan), MethodType.Constructor)]
- [HarmonyPatch(new[] { typeof(Map), typeof(bool), typeof(Action), typeof(bool), typeof(IntVec3) })]
+ [HarmonyPatch(new[] { typeof(Map), typeof(bool), typeof(Action), typeof(bool), typeof(IntVec3?) })]
static class DialogFormCaravanCtorPatch
{
- static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action onClosed, bool mapAboutToBeRemoved)
+ static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action onClosed, bool mapAboutToBeRemoved, IntVec3? designatedMeetingPoint)
{
+ if (Multiplayer.Client == null)
+ return;
+
if (__instance.GetType() != typeof(Dialog_FormCaravan))
return;
// Handles showing the dialog from TimedForcedExit.CompTick -> TimedForcedExit.ForceReform
+ // (note TimedForcedExit is obsolete)
if (Multiplayer.ExecutingCmds || Multiplayer.Ticking)
{
var comp = map.MpComp();
if (comp.sessionManager.GetFirstOfType() == null)
- comp.CreateCaravanFormingSession(reform, onClosed, mapAboutToBeRemoved);
+ comp.CreateCaravanFormingSession(reform, onClosed, mapAboutToBeRemoved, designatedMeetingPoint);
}
+ else // Handles opening from the interface: forming gizmos, reforming gizmos and caravan hitching spots
+ {
+ StartFormingCaravan(map, reform, designatedMeetingPoint);
+ }
+ }
+
+ [SyncMethod]
+ internal static void StartFormingCaravan(Map map, bool reform = false, IntVec3? designatedMeetingPoint = null, int? routePlannerWaypoint = null)
+ {
+ var comp = map.MpComp();
+ var session = comp.CreateCaravanFormingSession(reform, null, false, designatedMeetingPoint);
+
+ if (TickPatch.currentExecutingCmdIssuedBySelf)
+ {
+ var dialog = session.OpenWindow();
+ if (routePlannerWaypoint is { } tile)
+ {
+ try
+ {
+ UniqueIdsPatch.useLocalIdsOverride = true;
+
+ // Just to be safe
+ // RNG shouldn't be invoked but TryAddWaypoint is quite complex and calls pathfinding
+ Rand.PushState();
+
+ var worldRoutePlanner = Find.WorldRoutePlanner;
+ worldRoutePlanner.Start(dialog);
+ worldRoutePlanner.TryAddWaypoint(tile);
+ }
+ finally
+ {
+ Rand.PopState();
+ UniqueIdsPatch.useLocalIdsOverride = false;
+ }
+ }
+ }
+ }
+ }
+
+ [HarmonyPatch(typeof(FormCaravanGizmoUtility), nameof(FormCaravanGizmoUtility.DialogFromToSettlement))]
+ static class HandleFormCaravanShowRoutePlanner
+ {
+ static bool Prefix(Map origin, int tile)
+ {
+ if (Multiplayer.Client == null)
+ return true;
+
+ // Override behavior in multiplayer
+ DialogFormCaravanCtorPatch.StartFormingCaravan(origin, routePlannerWaypoint: tile);
+
+ return false;
}
}
diff --git a/Source/Client/Persistent/CaravanFormingProxy.cs b/Source/Client/Persistent/CaravanFormingProxy.cs
index c8886833..8536b51b 100644
--- a/Source/Client/Persistent/CaravanFormingProxy.cs
+++ b/Source/Client/Persistent/CaravanFormingProxy.cs
@@ -11,8 +11,11 @@ public class CaravanFormingProxy : Dialog_FormCaravan, ISwitchToMap
public CaravanFormingSession Session => map.MpComp().sessionManager.GetFirstOfType();
- public CaravanFormingProxy(Map map, bool reform = false, Action onClosed = null, bool mapAboutToBeRemoved = false, IntVec3? meetingSpot = null) : base(map, reform, onClosed, mapAboutToBeRemoved, meetingSpot)
+ public int originalSessionId;
+
+ public CaravanFormingProxy(int originalSessionId, Map map, bool reform = false, Action onClosed = null, bool mapAboutToBeRemoved = false, IntVec3? meetingSpot = null) : base(map, reform, onClosed, mapAboutToBeRemoved, meetingSpot)
{
+ this.originalSessionId = originalSessionId;
}
public override void DoWindowContents(Rect inRect)
diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs
index 7f2639af..fd1fc32a 100644
--- a/Source/Client/Persistent/CaravanFormingSession.cs
+++ b/Source/Client/Persistent/CaravanFormingSession.cs
@@ -44,7 +44,7 @@ public CaravanFormingSession(Map map, bool reform, Action onClosed, bool mapAbou
private void AddItems()
{
- var dialog = new CaravanFormingProxy(map, reform, null, mapAboutToBeRemoved, meetingSpot)
+ var dialog = new CaravanFormingProxy(sessionId, map, reform, null, mapAboutToBeRemoved, meetingSpot)
{
autoSelectTravelSupplies = autoSelectTravelSupplies
};
@@ -52,7 +52,7 @@ private void AddItems()
transferables = dialog.transferables;
}
- public void OpenWindow(bool sound = true)
+ public CaravanFormingProxy OpenWindow(bool sound = true)
{
var dialog = PrepareDummyDialog();
if (!sound)
@@ -72,13 +72,14 @@ public void OpenWindow(bool sound = true)
);
dialog.Notify_TransferablesChanged();
-
Find.WindowStack.Add(dialog);
+
+ return dialog;
}
private CaravanFormingProxy PrepareDummyDialog()
{
- var dialog = new CaravanFormingProxy(map, reform, null, mapAboutToBeRemoved, meetingSpot)
+ var dialog = new CaravanFormingProxy(sessionId, map, reform, null, mapAboutToBeRemoved, meetingSpot)
{
transferables = transferables,
startingTile = startingTile,
@@ -142,7 +143,9 @@ public void Cancel()
private void Remove()
{
map.MpComp().sessionManager.RemoveSession(this);
- Find.WorldRoutePlanner.Stop();
+
+ if (Find.WorldRoutePlanner.currentFormCaravanDialog is CaravanFormingProxy proxy && proxy.originalSessionId == sessionId)
+ Find.WorldRoutePlanner.Stop();
}
[SyncMethod]
@@ -186,7 +189,6 @@ public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry
{
return new FloatMenuOption("MpCaravanFormingSession".Translate(), () =>
{
- SwitchToMapOrWorld(entry.map);
OpenWindow();
});
}
diff --git a/Source/Client/Persistent/RitualBeginProxy.cs b/Source/Client/Persistent/RitualBeginProxy.cs
new file mode 100644
index 00000000..7f3b015f
--- /dev/null
+++ b/Source/Client/Persistent/RitualBeginProxy.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+using System.Linq;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace Multiplayer.Client.Persistent;
+
+public class RitualBeginProxy : Dialog_BeginRitual, ISwitchToMap
+{
+ public static RitualBeginProxy drawing;
+
+ public RitualSession Session => map.MpComp().sessionManager.GetFirstOfType();
+
+ // In the base type, the unused fields are used to create RitualRoleAssignments which already exist in an MP session
+ // They are here to help keep the base constructor in sync with this one
+ public RitualBeginProxy(
+ RitualRoleAssignments assignments,
+ string ritualLabel,
+ Precept_Ritual ritual,
+ TargetInfo target,
+ Map map,
+ ActionCallback action,
+ Pawn organizer,
+ RitualObligation obligation,
+ PawnFilter filter = null,
+ string okButtonText = null,
+ // ReSharper disable once UnusedParameter.Local
+ List requiredPawns = null,
+ // ReSharper disable once UnusedParameter.Local
+ Dictionary forcedForRole = null,
+ RitualOutcomeEffectDef outcome = null,
+ List extraInfoText = null,
+ // ReSharper disable once UnusedParameter.Local
+ Pawn selectedPawn = null) :
+ base(assignments, ritual, target, ritual?.outcomeEffect?.def ?? outcome)
+ {
+ this.obligation = obligation;
+ this.filter = filter;
+ this.organizer = organizer;
+ this.map = map;
+ this.action = action;
+ ritualExplanation = ritual?.ritualExplanation;
+ this.ritualLabel = ritualLabel;
+ this.okButtonText = okButtonText ?? "OK".Translate();
+ extraInfos = extraInfoText;
+
+ soundClose = SoundDefOf.TabClose;
+
+ // This gets cancelled in the base constructor if called from ticking/cmd in DontClearDialogBeginRitualCache
+ // Note that: cachedRoles is a static field, cachedRoles is only used for UI drawing
+ cachedRoles.Clear();
+ if (ritual is { ideo: not null })
+ {
+ cachedRoles.AddRange(ritual.ideo.RolesListForReading.Where(r => !r.def.leaderRole));
+ Precept_Role preceptRole = Faction.OfPlayer.ideos.PrimaryIdeo.RolesListForReading.FirstOrDefault(p => p.def.leaderRole);
+ if (preceptRole != null)
+ cachedRoles.Add(preceptRole);
+ cachedRoles.SortBy(x => x.def.displayOrderInImpact);
+ }
+ }
+
+ public override void DoWindowContents(Rect inRect)
+ {
+ drawing = this;
+
+ try
+ {
+ var session = Session;
+
+ if (session == null)
+ {
+ soundClose = SoundDefOf.Click;
+ Close();
+ }
+
+ // Make space for the "Switch to map" button
+ inRect.yMin += 20f;
+
+ base.DoWindowContents(inRect);
+ }
+ finally
+ {
+ drawing = null;
+ }
+ }
+}
diff --git a/Source/Client/Persistent/RitualData.cs b/Source/Client/Persistent/RitualData.cs
index 2827e942..9028a8b0 100644
--- a/Source/Client/Persistent/RitualData.cs
+++ b/Source/Client/Persistent/RitualData.cs
@@ -6,6 +6,11 @@
namespace Multiplayer.Client.Persistent
{
+ public class MpRitualAssignments : RitualRoleAssignments
+ {
+ public RitualSession session;
+ }
+
public class RitualData : ISynchronizable
{
public Precept_Ritual ritual;
diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/RitualPatches.cs
similarity index 56%
rename from Source/Client/Persistent/Rituals.cs
rename to Source/Client/Persistent/RitualPatches.cs
index c5ebcc91..e8cff093 100644
--- a/Source/Client/Persistent/Rituals.cs
+++ b/Source/Client/Persistent/RitualPatches.cs
@@ -11,179 +11,12 @@
namespace Multiplayer.Client.Persistent
{
- public class RitualSession : SemiPersistentSession
- {
- public Map map;
- public RitualData data;
-
- public override Map Map => map;
-
- public RitualSession(Map map) : base(map)
- {
- this.map = map;
- }
-
- public RitualSession(Map map, RitualData data) : this(map)
- {
- this.data = data;
- this.data.assignments.session = this;
- }
-
- [SyncMethod]
- public void Remove()
- {
- map.MpComp().sessionManager.RemoveSession(this);
- }
-
- [SyncMethod]
- public void Start()
- {
- if (data.action != null && data.action(data.assignments))
- Remove();
- }
-
- public void OpenWindow(bool sound = true)
- {
- var dialog = new BeginRitualProxy(
- data.assignments,
- data.ritualLabel,
- data.ritual,
- data.target,
- map,
- data.action,
- data.organizer,
- data.obligation,
- null,
- data.confirmText,
- null,
- null,
- data.outcome,
- data.extraInfos,
- null
- );
-
- if (!sound)
- dialog.soundAppear = null;
-
- Find.WindowStack.Add(dialog);
- }
-
- public override void Sync(SyncWorker sync)
- {
- if (sync.isWriting)
- {
- sync.Write(data);
- }
- else
- {
- data = sync.Read();
- data.assignments.session = this;
- }
- }
-
- public override bool IsCurrentlyPausing(Map map) => map == this.map;
-
- public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry)
- {
- return new FloatMenuOption("MpRitualSession".Translate(), () =>
- {
- SwitchToMapOrWorld(entry.map);
- OpenWindow();
- });
- }
- }
-
- public class MpRitualAssignments : RitualRoleAssignments
- {
- public RitualSession session;
- }
-
- public class BeginRitualProxy : Dialog_BeginRitual, ISwitchToMap
- {
- public static BeginRitualProxy drawing;
-
- public RitualSession Session => map.MpComp().sessionManager.GetFirstOfType();
-
- // In the base type, the unused fields are used to create RitualRoleAssignments which already exist in an MP session
- // They are here to help keep the base constructor in sync with this one
- public BeginRitualProxy(
- RitualRoleAssignments assignments,
- string ritualLabel,
- Precept_Ritual ritual,
- TargetInfo target,
- Map map,
- ActionCallback action,
- Pawn organizer,
- RitualObligation obligation,
- PawnFilter filter = null,
- string okButtonText = null,
- // ReSharper disable once UnusedParameter.Local
- List requiredPawns = null,
- // ReSharper disable once UnusedParameter.Local
- Dictionary forcedForRole = null,
- RitualOutcomeEffectDef outcome = null,
- List extraInfoText = null,
- // ReSharper disable once UnusedParameter.Local
- Pawn selectedPawn = null) :
- base(assignments, ritual, target, ritual?.outcomeEffect?.def ?? outcome)
- {
- this.obligation = obligation;
- this.filter = filter;
- this.organizer = organizer;
- this.map = map;
- this.action = action;
- ritualExplanation = ritual?.ritualExplanation;
- this.ritualLabel = ritualLabel;
- this.okButtonText = okButtonText ?? "OK".Translate();
- extraInfos = extraInfoText;
-
- soundClose = SoundDefOf.TabClose;
-
- // This gets cancelled in the base constructor if called from ticking/cmd in DontClearDialogBeginRitualCache
- // Note that: cachedRoles is a static field, cachedRoles is only used for UI drawing
- cachedRoles.Clear();
- if (ritual is { ideo: not null })
- {
- cachedRoles.AddRange(ritual.ideo.RolesListForReading.Where(r => !r.def.leaderRole));
- Precept_Role preceptRole = Faction.OfPlayer.ideos.PrimaryIdeo.RolesListForReading.FirstOrDefault(p => p.def.leaderRole);
- if (preceptRole != null)
- cachedRoles.Add(preceptRole);
- cachedRoles.SortBy(x => x.def.displayOrderInImpact);
- }
- }
-
- public override void DoWindowContents(Rect inRect)
- {
- drawing = this;
-
- try
- {
- var session = Session;
-
- if (session == null)
- {
- soundClose = SoundDefOf.Click;
- Close();
- }
-
- // Make space for the "Switch to map" button
- inRect.yMin += 20f;
-
- base.DoWindowContents(inRect);
- }
- finally
- {
- drawing = null;
- }
- }
- }
-
[HarmonyPatch(typeof(Widgets), nameof(Widgets.ButtonTextWorker))]
static class MakeCancelRitualButtonRed
{
static void Prefix(string label, ref bool __state)
{
- if (BeginRitualProxy.drawing == null) return;
+ if (RitualBeginProxy.drawing == null) return;
if (label != "CancelButton".Translate()) return;
GUI.color = new Color(1f, 0.3f, 0.35f);
@@ -197,7 +30,7 @@ static void Postfix(bool __state, ref DraggableResult __result)
GUI.color = Color.white;
if (__result.AnyPressed())
{
- BeginRitualProxy.drawing.Session?.Remove();
+ RitualBeginProxy.drawing.Session?.Remove();
__result = DraggableResult.Idle;
}
}
@@ -208,7 +41,7 @@ static class HandleStartRitual
{
static bool Prefix(Dialog_BeginRitual __instance)
{
- if (__instance is BeginRitualProxy proxy)
+ if (__instance is RitualBeginProxy proxy)
{
proxy.Session.Start();
return false;
@@ -224,7 +57,7 @@ static class CancelDialogBeginRitual
static bool Prefix(Window window)
{
if (Multiplayer.Client != null
- && window.GetType() == typeof(Dialog_BeginRitual) // Doesn't let BeginRitualProxy through
+ && window.GetType() == typeof(Dialog_BeginRitual) // Doesn't let RitualBeginProxy through
&& (Multiplayer.ExecutingCmds || Multiplayer.Ticking))
{
var tempDialog = (Dialog_BeginRitual)window;
diff --git a/Source/Client/Persistent/RitualSession.cs b/Source/Client/Persistent/RitualSession.cs
new file mode 100644
index 00000000..6010eed0
--- /dev/null
+++ b/Source/Client/Persistent/RitualSession.cs
@@ -0,0 +1,87 @@
+using Multiplayer.API;
+using RimWorld;
+using Verse;
+
+namespace Multiplayer.Client.Persistent;
+
+public class RitualSession : SemiPersistentSession
+{
+ public Map map;
+ public RitualData data;
+
+ public override Map Map => map;
+
+ public RitualSession(Map map) : base(map)
+ {
+ this.map = map;
+ }
+
+ public RitualSession(Map map, RitualData data) : this(map)
+ {
+ this.data = data;
+ this.data.assignments.session = this;
+ }
+
+ [SyncMethod]
+ public void Remove()
+ {
+ map.MpComp().sessionManager.RemoveSession(this);
+ }
+
+ [SyncMethod]
+ public void Start()
+ {
+ if (data.action != null && data.action(data.assignments))
+ Remove();
+ }
+
+ public void OpenWindow(bool sound = true)
+ {
+ var dialog = new RitualBeginProxy(
+ data.assignments,
+ data.ritualLabel,
+ data.ritual,
+ data.target,
+ map,
+ data.action,
+ data.organizer,
+ data.obligation,
+ null,
+ data.confirmText,
+ null,
+ null,
+ data.outcome,
+ data.extraInfos,
+ null
+ );
+
+ if (!sound)
+ dialog.soundAppear = null;
+
+ Find.WindowStack.Add(dialog);
+ }
+
+ public override void Sync(SyncWorker sync)
+ {
+ if (sync.isWriting)
+ {
+ sync.Write(data);
+ }
+ else
+ {
+ data = sync.Read();
+ data.assignments.session = this;
+ }
+ }
+
+ public override bool IsCurrentlyPausing(Map map) => map == this.map;
+
+ public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry)
+ {
+ return new FloatMenuOption("MpRitualSession".Translate(), () =>
+ {
+ SwitchToMapOrWorld(entry.map);
+ OpenWindow();
+ });
+ }
+}
diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs
index b145fc5b..caf07fa3 100644
--- a/Source/Client/Syncing/Game/SyncDelegates.cs
+++ b/Source/Client/Syncing/Game/SyncDelegates.cs
@@ -320,6 +320,13 @@ private static void InitChoiceLetters()
// Growth moment for a child
CloseDialogsForExpiredLetters.RegisterDefaultLetterChoice(AccessTools.Method(typeof(SyncDelegates), nameof(PickRandomTraitAndPassions)), typeof(ChoiceLetter_GrowthMoment));
SyncMethod.Register(typeof(ChoiceLetter_GrowthMoment), nameof(ChoiceLetter_GrowthMoment.MakeChoices)).ExposeParameter(1);
+
+ // Creep joiner
+ SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 0); // Accept joiner
+ SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 1); // Arrest joiner
+ CloseDialogsForExpiredLetters.RegisterDefaultLetterChoice(
+ SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 2)
+ .method); // Reject joiner
}
static void SyncBabyToChildLetter(ChoiceLetter_BabyToChild letter)
@@ -364,7 +371,7 @@ static void SetBabyName(ChoiceLetter_BabyBirth letter)
}
// If the baby ended up being stillborn, the timer to name them is 1 tick. This patch is here to allow players in MP to actually change their name.
- [MpPostfix(typeof(PregnancyUtility), nameof(PregnancyUtility.ApplyBirthOutcome))]
+ [MpPostfix(typeof(PregnancyUtility), nameof(PregnancyUtility.ApplyBirthOutcome_NewTemp))]
static void GiveTimeToNameStillborn(Thing __result)
{
if (Multiplayer.Client != null && __result is Pawn pawn && pawn.health.hediffSet.HasHediff(HediffDefOf.Stillborn))
@@ -468,54 +475,6 @@ static void GeneUIUtilityTarget(Thing target)
geneUIUtilityTarget = target;
}
- [MpPrefix(typeof(FormCaravanComp), nameof(FormCaravanComp.GetGizmos), lambdaOrdinal: 0)]
- static bool GizmoFormCaravan(MapParent ___mapParent)
- {
- if (Multiplayer.Client == null) return true;
- GizmoFormCaravan(___mapParent.Map, false);
- return false;
- }
-
- [MpPrefix(typeof(FormCaravanComp), nameof(FormCaravanComp.GetGizmos), lambdaOrdinal: 1)]
- static bool GizmoReformCaravan(MapParent ___mapParent)
- {
- if (Multiplayer.Client == null) return true;
- GizmoFormCaravan(___mapParent.Map, true);
- return false;
- }
-
- [MpPrefix(typeof(CompHitchingSpot), nameof(CompHitchingSpot.CompGetGizmosExtra), 0)]
- static bool GizmoFormCaravan(CompHitchingSpot __instance)
- {
- if (Multiplayer.Client == null) return true;
- GizmoFormCaravan(__instance.parent.Map, false, __instance.parent.Position);
- return false;
- }
-
- private static void GizmoFormCaravan(Map map, bool reform, IntVec3? meetingSpot = null)
- {
- var comp = map.MpComp();
-
- if (comp.sessionManager.GetFirstOfType() is { } session)
- session.OpenWindow();
- else
- CreateCaravanFormingSession(comp, reform, meetingSpot);
- }
-
- [SyncMethod]
- private static void CreateCaravanFormingSession(MultiplayerMapComp comp, bool reform, IntVec3? meetingSpot = null)
- {
- var session = comp.CreateCaravanFormingSession(reform, null, false, meetingSpot);
-
- if (TickPatch.currentExecutingCmdIssuedBySelf)
- {
- session.OpenWindow();
- AsyncTimeComp.keepTheMap = true;
- Current.Game.CurrentMap = comp.map;
- Find.World.renderer.wantedMode = WorldRenderMode.None;
- }
- }
-
[MpPostfix(typeof(CaravanVisitUtility), nameof(CaravanVisitUtility.TradeCommand))]
static void ReopenTradingWindowLocally(Caravan caravan, Command __result)
{
diff --git a/Source/Client/Syncing/Game/SyncMethods.cs b/Source/Client/Syncing/Game/SyncMethods.cs
index d2351e09..dc63361b 100644
--- a/Source/Client/Syncing/Game/SyncMethods.cs
+++ b/Source/Client/Syncing/Game/SyncMethods.cs
@@ -105,7 +105,7 @@ public static void Init()
SyncMethod.Register(typeof(Building_SunLamp), nameof(Building_SunLamp.MakeMatchingGrowZone));
SyncMethod.Register(typeof(Building_ShipComputerCore), nameof(Building_ShipComputerCore.TryLaunch));
SyncMethod.Register(typeof(CompPower), nameof(CompPower.TryManualReconnect));
- SyncMethod.Register(typeof(CompTempControl), nameof(CompTempControl.InterfaceChangeTargetTemperature));
+ SyncMethod.Register(typeof(CompTempControl), nameof(CompTempControl.InterfaceChangeTargetTemperature_NewTemp));
SyncMethod.Register(typeof(CompTransporter), nameof(CompTransporter.CancelLoad), Array.Empty());
SyncMethod.Register(typeof(MapPortal), nameof(MapPortal.CancelLoad));
SyncMethod.Register(typeof(StorageSettings), nameof(StorageSettings.CopyFrom)).ExposeParameter(0);
diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj
index 039415ad..7f39a9d6 100644
--- a/Source/Common/Common.csproj
+++ b/Source/Common/Common.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs
index db91a7f4..b43cf4a9 100644
--- a/Source/Common/Version.cs
+++ b/Source/Common/Version.cs
@@ -2,8 +2,8 @@ namespace Multiplayer.Common
{
public static class MpVersion
{
- public const string Version = "0.10.3";
- public const int Protocol = 45;
+ public const string Version = "0.10.4";
+ public const int Protocol = 46;
public const string ApiAssemblyName = "0MultiplayerAPI";
diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj
index 195aae26..5572bda9 100644
--- a/Source/MultiplayerLoader/MultiplayerLoader.csproj
+++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj
@@ -10,7 +10,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/MultiplayerLoader/SyncRituals.cs b/Source/MultiplayerLoader/SyncRituals.cs
index b927f4bd..759b4cb3 100644
--- a/Source/MultiplayerLoader/SyncRituals.cs
+++ b/Source/MultiplayerLoader/SyncRituals.cs
@@ -47,9 +47,9 @@ void Register(Type baseType, string method, Type derivedType, Action