From 3ec9c4c746d8f76166e9e6e59522775bffb2ba14 Mon Sep 17 00:00:00 2001 From: Zetrith Date: Mon, 13 Nov 2023 02:17:32 +0100 Subject: [PATCH] Letters and location pings are now per faction Global storytellers are now per faction Use Harmony finalizers where it makes sense Fix assigning ideo roles in multifaction Fix attacking non-player bases in multifaction Fix bill pawn restrictions sometimes getting removed Fix host's zones and areas sometimes disappearing Fix potential desync sources caused by things running in the interface: creation of situational thoughts, pawn capacity level caching and stat caching Remove ResearchSpeed and situation thought proxy --- About/About.xml | 2 +- Source/Client/AsyncTime/AsyncTimeComp.cs | 12 +- Source/Client/AsyncTime/AsyncTimePatches.cs | 6 +- Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 2 +- .../Client/AsyncTime/MultiplayerAsyncQuest.cs | 2 +- Source/Client/AsyncTime/SetMapTime.cs | 4 +- Source/Client/AsyncTime/StorytellerPatches.cs | 6 +- Source/Client/Comp/Map/MultiplayerMapComp.cs | 22 +- Source/Client/Comp/World/FactionWorldData.cs | 28 +- .../Client/Comp/World/MultiplayerWorldComp.cs | 17 +- Source/Client/ConstantTicker.cs | 54 --- Source/Client/Debug/DebugActions.cs | 2 +- Source/Client/Debug/DebugPatches.cs | 9 + .../Debug/{DebugTools.cs => DebugSync.cs} | 16 +- .../Client/Factions/AutoRoofFactionPatch.cs | 5 +- .../Client/Factions/FactionCreationPatches.cs | 65 ++++ Source/Client/Factions/FactionCreator.cs | 104 +++--- Source/Client/Factions/FactionExtensions.cs | 2 - Source/Client/Factions/FactionRepeater.cs | 107 +++++- Source/Client/Factions/FactionSidebar.cs | 79 +++-- Source/Client/Factions/Forbiddables.cs | 2 +- Source/Client/Factions/MultifactionPatches.cs | 333 ++++++++++++++++-- .../Factions/Page_ChooseIdeo_Multifaction.cs | 11 + Source/Client/Multiplayer.cs | 4 - Source/Client/Multiplayer.csproj | 7 +- Source/Client/MultiplayerStatic.cs | 16 +- Source/Client/Networking/HostUtil.cs | 6 +- Source/Client/Patches/Determinism.cs | 141 ++++++++ Source/Client/Patches/MultiplayerPawnComp.cs | 2 - Source/Client/Patches/SituationalThoughts.cs | 105 ------ Source/Client/Patches/ThingMethodPatches.cs | 18 +- Source/Client/Persistent/Rituals.cs | 15 +- .../Client/Syncing/Dict/SyncDictRimWorld.cs | 3 +- Source/Client/Syncing/Game/SyncResearch.cs | 72 ---- Source/Client/Syncing/Handler/SyncField.cs | 1 - Source/Client/Util/MethodOf.cs | 2 +- Source/Client/Util/MpScope.cs | 25 ++ Source/Common/DeferredStackTracingImpl.cs | 3 +- Source/Common/Version.cs | 4 +- Source/MultiplayerLoader/Prepatches.cs | 7 +- Source/TestsOnMono/Program.cs | 2 - 41 files changed, 861 insertions(+), 462 deletions(-) rename Source/Client/Debug/{DebugTools.cs => DebugSync.cs} (95%) create mode 100644 Source/Client/Factions/FactionCreationPatches.cs delete mode 100644 Source/Client/Patches/SituationalThoughts.cs delete mode 100644 Source/Client/Syncing/Game/SyncResearch.cs create mode 100644 Source/Client/Util/MpScope.cs diff --git a/About/About.xml b/About/About.xml index d2f35253..f492fb65 100644 --- a/About/About.xml +++ b/About/About.xml @@ -8,7 +8,7 @@ RimWorld Multiplayer Team https://github.com/rwmt/Multiplayer <color=red><b>Important: </b> This mod should be placed right below Core and expansions in the mod list to work properly! -Requires >= Rimworld v1.4.3555</color>\n +Requires Rimworld >= v1.4.3901</color>\n Multiplayer mod for RimWorld. FAQ - https://hackmd.io/@rimworldmultiplayer/docs/ diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index a60c5ab1..ec07309d 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -171,7 +171,12 @@ public void UpdateManagers() public void PreContext() { - map.PushFaction(map.ParentFaction); // bullets? + if (Multiplayer.GameComp.multifaction) + { + map.PushFaction(map.ParentFaction is { IsPlayer: true } + ? map.ParentFaction + : Multiplayer.WorldComp.spectatorFaction); + } prevTime = TimeSnapshot.GetAndSetFromMap(map); @@ -198,7 +203,8 @@ public void PostContext() randState = Rand.StateCompressed; Rand.PopState(); - map.PopFaction(); + if (Multiplayer.GameComp.multifaction) + map.PopFaction(); } public void ExposeData() @@ -266,7 +272,7 @@ public void ExecuteCmd(ScheduledCommand cmd) if (cmdType == CommandType.DebugTools) { - MpDebugTools.HandleCmd(data); + DebugSync.HandleCmd(data); } if (cmdType == CommandType.MapTimeSpeed && Multiplayer.GameComp.asyncTime) diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index 526e7c5d..b8c3b635 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -32,7 +32,7 @@ static class MapUpdateMarker public static bool updating; static void Prefix() => updating = true; - static void Postfix() => updating = false; + static void Finalizer() => updating = false; } [HarmonyPatch] @@ -64,7 +64,7 @@ static void Prefix(DateNotifier __instance, ref int? __state) Find.TickManager.DebugSetTicksGame(map.AsyncTime().mapTicks); } - static void Postfix(int? __state) + static void Finalizer(int? __state) { if (!__state.HasValue) return; Find.TickManager.DebugSetTicksGame(__state.Value); @@ -120,7 +120,7 @@ static class PreDrawCalcMarker public static Pawn calculating; static void Prefix(PawnTweener __instance) => calculating = __instance.pawn; - static void Postfix() => calculating = null; + static void Finalizer() => calculating = null; } [HarmonyPatch(typeof(TickManager), nameof(TickManager.TickRateMultiplier), MethodType.Getter)] diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index 81e29b87..7a3d2a89 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -183,7 +183,7 @@ public void ExecuteCmd(ScheduledCommand cmd) if (cmdType == CommandType.DebugTools) { - MpDebugTools.HandleCmd(data); + DebugSync.HandleCmd(data); } if (cmdType == CommandType.GlobalTimeSpeed) diff --git a/Source/Client/AsyncTime/MultiplayerAsyncQuest.cs b/Source/Client/AsyncTime/MultiplayerAsyncQuest.cs index 8cbbe922..883980f3 100644 --- a/Source/Client/AsyncTime/MultiplayerAsyncQuest.cs +++ b/Source/Client/AsyncTime/MultiplayerAsyncQuest.cs @@ -266,7 +266,7 @@ private static AsyncTimeComp TryGetQuestMap(Quest quest) //Really terrible way to determine if any quest parts have a map which also has an async time foreach (var part in quest.parts.Where(x => x != null && questPartsToCheck.Contains(x.GetType()))) { - if (part.GetType().GetField("mapParent")?.GetValue(part) is MapParent mapParent) + if (part.GetType().GetField("mapParent")?.GetValue(part) is MapParent { Map: not null } mapParent) { var mapAsyncTimeComp = mapParent.Map.IsPlayerHome ? mapParent.Map.AsyncTime() : null; if (mapAsyncTimeComp != null) return mapAsyncTimeComp; diff --git a/Source/Client/AsyncTime/SetMapTime.cs b/Source/Client/AsyncTime/SetMapTime.cs index 9884d9db..28df1278 100644 --- a/Source/Client/AsyncTime/SetMapTime.cs +++ b/Source/Client/AsyncTime/SetMapTime.cs @@ -27,14 +27,14 @@ static IEnumerable TargetMethods() } [HarmonyPriority(MpPriority.MpFirst)] - static void Prefix(ref TimeSnapshot? __state) + internal static void Prefix(ref TimeSnapshot? __state) { if (Multiplayer.Client == null || WorldRendererUtility.WorldRenderedNow || Find.CurrentMap == null) return; __state = TimeSnapshot.GetAndSetFromMap(Find.CurrentMap); } [HarmonyPriority(MpPriority.MpLast)] - static void Postfix(TimeSnapshot? __state) => __state?.Set(); + internal static void Postfix(TimeSnapshot? __state) => __state?.Set(); } [HarmonyPatch] diff --git a/Source/Client/AsyncTime/StorytellerPatches.cs b/Source/Client/AsyncTime/StorytellerPatches.cs index f7940fc5..70cd4de8 100644 --- a/Source/Client/AsyncTime/StorytellerPatches.cs +++ b/Source/Client/AsyncTime/StorytellerPatches.cs @@ -26,7 +26,7 @@ static bool Prefix() return Multiplayer.Client == null || Multiplayer.Ticking; } - static void Postfix() => updating = false; + static void Finalizer() => updating = false; } [HarmonyPatch(typeof(Storyteller))] @@ -92,7 +92,7 @@ static void Prefix(IIncidentTarget target, ref Map __state) } } - static void Postfix(Map __state) + static void Finalizer(Map __state) { if (__state != null) { @@ -115,7 +115,7 @@ static void Prefix(IncidentParms parms, ref Map __state) } } - static void Postfix(Map __state) + static void Finalizer(Map __state) { if (__state != null) { diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index 1ebdbd80..65ccf9bc 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using HarmonyLib; using Multiplayer.Client.Factions; @@ -9,7 +8,6 @@ using Multiplayer.Common; using RimWorld; using RimWorld.Planet; -using UnityEngine; using Verse; namespace Multiplayer.Client @@ -96,13 +94,6 @@ public void SetFaction(Faction faction) map.listerMergeables = data.listerMergeables; } - [Conditional("DEBUG")] - public void CheckInvariant() - { - if (factionData.TryGetValue(Faction.OfPlayer.loadID, out var data) && map.areaManager != data.areaManager) - Log.Error($"(Debug) Invariant broken for {Faction.OfPlayer}: {FactionContext.stack.ToStringSafeEnumerable()} {factionData.FirstOrDefault(d => d.Value.areaManager == map.areaManager)} {StackTraceUtility.ExtractStackTrace()}"); - } - public CustomFactionMapData GetCurrentCustomFactionData() { return customFactionData[Faction.OfPlayer.loadID]; @@ -140,12 +131,12 @@ private void ExposeFactionData() { if (Scribe.mode == LoadSaveMode.Saving) { - int currentFactionId = Faction.OfPlayer.loadID; + int currentFactionId =GetFactionId(map.zoneManager); Scribe_Custom.LookValue(currentFactionId, "currentFactionId"); - var data = new Dictionary(factionData); - data.Remove(currentFactionId); - Scribe_Custom.LookValueDeep(ref data, "factionMapData", map); + var savedFactionData = new Dictionary(factionData); + savedFactionData.Remove(currentFactionId); + Scribe_Custom.LookValueDeep(ref savedFactionData, "factionMapData", map); } else { @@ -189,6 +180,11 @@ public void ReadSemiPersistent(ByteReader reader) ritualSession = session; } } + + public int GetFactionId(ZoneManager zoneManager) + { + return factionData.First(kv => kv.Value.zoneManager == zoneManager).Key; + } } [HarmonyPatch(typeof(MapDrawer), nameof(MapDrawer.DrawMapMesh))] diff --git a/Source/Client/Comp/World/FactionWorldData.cs b/Source/Client/Comp/World/FactionWorldData.cs index 5d2b9214..d04fdd0f 100644 --- a/Source/Client/Comp/World/FactionWorldData.cs +++ b/Source/Client/Comp/World/FactionWorldData.cs @@ -6,7 +6,6 @@ namespace Multiplayer.Client; public class FactionWorldData : IExposable { public int factionId; - public bool online; public ResearchManager researchManager; public OutfitDatabase outfitDatabase; @@ -14,14 +13,15 @@ public class FactionWorldData : IExposable public FoodRestrictionDatabase foodRestrictionDatabase; public PlaySettings playSettings; - public ResearchSpeed researchSpeed; + public History history; + public Storyteller storyteller; + public StoryWatcher storyWatcher; public FactionWorldData() { } public void ExposeData() { Scribe_Values.Look(ref factionId, "factionId"); - Scribe_Values.Look(ref online, "online"); Scribe_Deep.Look(ref researchManager, "researchManager"); Scribe_Deep.Look(ref drugPolicyDatabase, "drugPolicyDatabase"); @@ -29,7 +29,17 @@ public void ExposeData() Scribe_Deep.Look(ref foodRestrictionDatabase, "foodRestrictionDatabase"); Scribe_Deep.Look(ref playSettings, "playSettings"); - Scribe_Deep.Look(ref researchSpeed, "researchSpeed"); + Scribe_Deep.Look(ref history, "history"); + Scribe_Deep.Look(ref storyteller, "storyteller"); + Scribe_Deep.Look(ref storyWatcher, "storyWatcher"); + + if (Scribe.mode == LoadSaveMode.LoadingVars) + { + history ??= new History(); + storyteller ??= new Storyteller(Find.Storyteller.def, Find.Storyteller.difficultyDef, + Find.Storyteller.difficulty); + storyWatcher ??= new StoryWatcher(); + } } public void ReassignIds() @@ -55,7 +65,10 @@ public static FactionWorldData New(int factionId) outfitDatabase = new OutfitDatabase(), foodRestrictionDatabase = new FoodRestrictionDatabase(), playSettings = new PlaySettings(), - researchSpeed = new ResearchSpeed(), + + history = new History(), + storyteller = new Storyteller(Find.Storyteller.def, Find.Storyteller.difficultyDef, Find.Storyteller.difficulty), + storyWatcher = new StoryWatcher() }; } @@ -64,7 +77,6 @@ public static FactionWorldData FromCurrent(int factionId) return new FactionWorldData() { factionId = factionId == int.MinValue ? Faction.OfPlayer.loadID : factionId, - online = true, researchManager = Find.ResearchManager, drugPolicyDatabase = Current.Game.drugPolicyDatabase, @@ -72,7 +84,9 @@ public static FactionWorldData FromCurrent(int factionId) foodRestrictionDatabase = Current.Game.foodRestrictionDatabase, playSettings = Current.Game.playSettings, - researchSpeed = new ResearchSpeed(), + history = Find.History, + storyteller = Find.Storyteller, + storyWatcher = Find.StoryWatcher }; } } diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index c2209daa..cedf7e02 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -84,13 +84,13 @@ private void ExposeFactionData() { if (Scribe.mode == LoadSaveMode.Saving) { - int currentFactionId = Faction.OfPlayer.loadID; + int currentFactionId = GetFactionId(Find.ResearchManager); Scribe_Custom.LookValue(currentFactionId, "currentFactionId"); - var factionData = new Dictionary(this.factionData); - factionData.Remove(currentFactionId); + var savedFactionData = new Dictionary(factionData); + savedFactionData.Remove(currentFactionId); - Scribe_Collections.Look(ref factionData, "factionData", LookMode.Value, LookMode.Deep); + Scribe_Collections.Look(ref savedFactionData, "factionData", LookMode.Value, LookMode.Deep); } else { @@ -144,7 +144,9 @@ public void SetFaction(Faction faction) game.foodRestrictionDatabase = data.foodRestrictionDatabase; game.playSettings = data.playSettings; - SyncResearch.researchSpeed = data.researchSpeed; + game.history = data.history; + game.storyteller = data.storyteller; + game.storyWatcher = data.storyWatcher; } public void DirtyColonyTradeForMap(Map map) @@ -179,6 +181,11 @@ public bool AnyTradeSessionsOnMap(Map map) return false; } + public int GetFactionId(ResearchManager researchManager) + { + return factionData.First(kv => kv.Value.researchManager == researchManager).Key; + } + public override string ToString() { return $"{nameof(MultiplayerWorldComp)}_{world}"; diff --git a/Source/Client/ConstantTicker.cs b/Source/Client/ConstantTicker.cs index 4dd589b2..197253b5 100644 --- a/Source/Client/ConstantTicker.cs +++ b/Source/Client/ConstantTicker.cs @@ -1,6 +1,4 @@ -using System; using HarmonyLib; -using Multiplayer.Client.Factions; using Multiplayer.Common; using RimWorld; using Verse; @@ -17,14 +15,6 @@ public static void Tick() try { - //TickResearch(); - - // Not really deterministic but here for possible future server-side game state verification - //Extensions.PushFaction(null, Multiplayer.RealPlayerFaction); - //TickSync(); - //SyncResearch.ConstantTick(); - //Extensions.PopFaction(); - TickShipCountdown(); TickSyncCoordinator(); TickAutosave(); @@ -91,50 +81,6 @@ private static void TickShipCountdown() ShipCountdown.CountdownEnded(); } } - - private static Func bufferPredicate = SyncFieldUtil.BufferedChangesPruner(() => TickPatch.Timer); - - private static void TickSync() - { - foreach (SyncField f in Sync.bufferedFields) - { - if (!f.inGameLoop) continue; - SyncFieldUtil.bufferedChanges[f].RemoveAll(bufferPredicate); - } - } - - private static Pawn dummyPawn = new() - { - relations = new Pawn_RelationsTracker(dummyPawn), - }; - - public static void TickResearch() - { - MultiplayerWorldComp comp = Multiplayer.WorldComp; - foreach (FactionWorldData factionData in comp.factionData.Values) - { - if (factionData.researchManager.currentProj == null) - continue; - - FactionExtensions.PushFaction(null, factionData.factionId); - - foreach (var kv in factionData.researchSpeed.data) - { - Pawn pawn = PawnsFinder.AllMaps_Spawned.FirstOrDefault(p => p.thingIDNumber == kv.Key); - if (pawn == null) - { - dummyPawn.factionInt = Faction.OfPlayer; - pawn = dummyPawn; - } - - Find.ResearchManager.ResearchPerformed(kv.Value, pawn); - - dummyPawn.factionInt = null; - } - - FactionExtensions.PopFaction(); - } - } } [HarmonyPatch(typeof(ShipCountdown), nameof(ShipCountdown.CancelCountdown))] diff --git a/Source/Client/Debug/DebugActions.cs b/Source/Client/Debug/DebugActions.cs index b99b055d..902c9494 100644 --- a/Source/Client/Debug/DebugActions.cs +++ b/Source/Client/Debug/DebugActions.cs @@ -117,7 +117,7 @@ private static void SetFaction() [DebugAction(MultiplayerCategory, actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)] public static void SpawnShuttleAcceptColonists() { - var shuttle = ThingMaker.MakeThing(ThingDefOf.Shuttle, null); + var shuttle = ThingMaker.MakeThing(ThingDefOf.Shuttle); shuttle.TryGetComp().acceptColonists = true; GenPlace.TryPlaceThing(shuttle, UI.MouseCell(), Find.CurrentMap, ThingPlaceMode.Near); } diff --git a/Source/Client/Debug/DebugPatches.cs b/Source/Client/Debug/DebugPatches.cs index c577b5d8..0ad4e5f8 100644 --- a/Source/Client/Debug/DebugPatches.cs +++ b/Source/Client/Debug/DebugPatches.cs @@ -249,5 +249,14 @@ static class DontUpdateResolution static bool Prefix() => false; } + [HarmonyPatch(typeof(Log), nameof(Log.Notify_MessageReceivedThreadedInternal))] + static class DisableLogMessageLimit + { + static bool Prefix() + { + return false; + } + } + } #endif diff --git a/Source/Client/Debug/DebugTools.cs b/Source/Client/Debug/DebugSync.cs similarity index 95% rename from Source/Client/Debug/DebugTools.cs rename to Source/Client/Debug/DebugSync.cs index 700dc566..30fda23d 100644 --- a/Source/Client/Debug/DebugTools.cs +++ b/Source/Client/Debug/DebugSync.cs @@ -10,7 +10,7 @@ namespace Multiplayer.Client { - static class MpDebugTools + static class DebugSync { private static int currentPlayer; public static int currentHash; @@ -230,10 +230,10 @@ class MpDebugAction public void Action() { - if (Multiplayer.Client == null || Multiplayer.ExecutingCmds || !MpDebugTools.ShouldHandle()) + if (Multiplayer.Client == null || Multiplayer.ExecutingCmds || !DebugSync.ShouldHandle()) original(); else - MpDebugTools.SendCmd( + DebugSync.SendCmd( DebugSource.Tree, 0, node.NodePath(), @@ -251,7 +251,7 @@ static bool Prefix(Window window) if (Multiplayer.Client == null) return true; if (!Multiplayer.ExecutingCmds) return true; if (!Multiplayer.GameComp.debugMode) return true; - if (!MpDebugTools.ShouldHandle()) return true; + if (!DebugSync.ShouldHandle()) return true; bool keepOpen = TickPatch.currentExecutingCmdIssuedBySelf; var map = Multiplayer.MapContext; @@ -270,12 +270,12 @@ static bool Prefix(Window window) lister.options.Add(new DebugMenuOption( opt.label, opt.mode, - () => MpDebugTools.SendCmd(DebugSource.Lister, hash, null, map) + () => DebugSync.SendCmd(DebugSource.Lister, hash, null, map) )); } } - MpDebugTools.CurrentPlayerState.currentData = origOptions; + DebugSync.CurrentPlayerState.currentData = origOptions; return keepOpen; } @@ -291,12 +291,12 @@ static bool Prefix(Window window) { var copy = new FloatMenuOption(option.labelInt, option.action); int hash = copy.Hash(); - copy.action = () => MpDebugTools.SendCmd(DebugSource.FloatMenu, hash, null, map); + copy.action = () => DebugSync.SendCmd(DebugSource.FloatMenu, hash, null, map); menu.options.Add(copy); } } - MpDebugTools.CurrentPlayerState.currentData = origOptions; + DebugSync.CurrentPlayerState.currentData = origOptions; return keepOpen; } diff --git a/Source/Client/Factions/AutoRoofFactionPatch.cs b/Source/Client/Factions/AutoRoofFactionPatch.cs index 2ac5783c..454587db 100644 --- a/Source/Client/Factions/AutoRoofFactionPatch.cs +++ b/Source/Client/Factions/AutoRoofFactionPatch.cs @@ -33,9 +33,8 @@ static bool Prefix(AutoBuildRoofAreaSetter __instance, Room room, ref Map __stat return true; } - static void Postfix(ref Map __state) + static void Finalizer(ref Map __state) { - if (__state != null) - __state.PopFaction(); + __state?.PopFaction(); } } diff --git a/Source/Client/Factions/FactionCreationPatches.cs b/Source/Client/Factions/FactionCreationPatches.cs new file mode 100644 index 00000000..2665a7ca --- /dev/null +++ b/Source/Client/Factions/FactionCreationPatches.cs @@ -0,0 +1,65 @@ +using HarmonyLib; +using RimWorld; +using Verse; + +namespace Multiplayer.Client.Factions; + +[HarmonyPatch(typeof(Page_ConfigureStartingPawns), nameof(Page_ConfigureStartingPawns.DoWindowContents))] +static class ConfigureStartingPawns_DoWindowContents_Patch +{ + static void Prefix(ref ProgramState __state) + { + __state = Current.ProgramState; + Current.programStateInt = ProgramState.Entry; + } + + static void Finalizer(ProgramState __state) + { + Current.programStateInt = __state; + } +} + +[HarmonyPatch(typeof(Page_ConfigureStartingPawns), nameof(Page_ConfigureStartingPawns.RandomizeCurPawn))] +static class ConfigureStartingPawns_RandomizeCurPawn_Patch +{ + static void Prefix(ref ProgramState __state) + { + __state = Current.ProgramState; + Current.programStateInt = ProgramState.Entry; + } + + static void Finalizer(ProgramState __state) + { + Current.programStateInt = __state; + } +} + +[HarmonyPatch(typeof(LifeStageWorker_HumanlikeAdult), nameof(LifeStageWorker_HumanlikeAdult.Notify_LifeStageStarted))] +static class LifeStageWorker_Patch +{ + static bool Prefix() + { + // Corresponds to "Current.ProgramState == ProgramState.Playing" check in Notify_LifeStageStarted + return !ScribeUtil.loading; + } +} + +[HarmonyPatch(typeof(StartingPawnUtility), nameof(StartingPawnUtility.GetGenerationRequest))] +static class StartingPawnUtility_GetGenerationRequest_Patch +{ + static void Postfix(ref PawnGenerationRequest __result) + { + if (Multiplayer.Client != null) + __result.CanGeneratePawnRelations = false; + } +} + +[HarmonyPatch(typeof(StartingPawnUtility), nameof(StartingPawnUtility.DefaultStartingPawnRequest), MethodType.Getter)] +static class StartingPawnUtility_DefaultStartingPawnRequest_Patch +{ + static void Postfix(ref PawnGenerationRequest __result) + { + if (Multiplayer.Client != null) + __result.CanGeneratePawnRelations = false; + } +} diff --git a/Source/Client/Factions/FactionCreator.cs b/Source/Client/Factions/FactionCreator.cs index 7b759ed3..74e82b66 100644 --- a/Source/Client/Factions/FactionCreator.cs +++ b/Source/Client/Factions/FactionCreator.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using Multiplayer.API; +using Multiplayer.Client.Util; using Multiplayer.Common; using RimWorld; using RimWorld.Planet; @@ -11,6 +13,7 @@ namespace Multiplayer.Client.Factions; public static class FactionCreator { private static Dictionary> pawnStore = new(); + public static bool generatingMap; public static void ClearData() { @@ -18,26 +21,24 @@ public static void ClearData() } [SyncMethod(exposeParameters = new[] { 1 })] - public static void SendPawn(int sessionId, Pawn p) + public static void SendPawn(int playerId, Pawn p) { - pawnStore.GetOrAddNew(sessionId).Add(p); + pawnStore.GetOrAddNew(playerId).Add(p); } [SyncMethod] - public static void CreateFaction(int sessionId, string factionName, int tile, string scenario, FactionRelationKind relation, ChooseIdeoInfo chooseIdeoInfo) + public static void CreateFaction(int playerId, string factionName, int tile, [CanBeNull] ScenarioDef scenarioDef, ChooseIdeoInfo chooseIdeoInfo) { - PrepareState(sessionId); - var self = TickPatch.currentExecutingCmdIssuedBySelf; - LongEventHandler.QueueLongEvent(delegate + LongEventHandler.QueueLongEvent(() => { - int newFactionID = Find.UniqueIDsManager.GetNextFactionID(); + PrepareState(playerId); - var newFaction = NewFaction( - newFactionID, + var scenario = scenarioDef?.scenario ?? Current.Game.Scenario; + var newFaction = NewFactionWithIdeo( factionName, - FactionDefOf.PlayerColony, + scenario.playerFaction.factionDef, chooseIdeoInfo ); @@ -45,19 +46,19 @@ public static void CreateFaction(int sessionId, string factionName, int tile, st foreach (var f in Find.FactionManager.AllFactions.Where(f => f.IsPlayer)) if (f != newFaction) - { - newFaction.SetRelation(new FactionRelation(f, relation)); - } + newFaction.SetRelation(new FactionRelation(f, FactionRelationKind.Neutral)); - FactionContext.Push(newFaction); + Map newMap; - foreach (var pawn in StartingPawnUtility.StartingAndOptionalPawns) - pawn.ideo.SetIdeo(newFaction.ideos.PrimaryIdeo); + using (MpScope.PushFaction(newFaction)) + { + foreach (var pawn in StartingPawnUtility.StartingAndOptionalPawns) + pawn.ideo.SetIdeo(newFaction.ideos.PrimaryIdeo); - var newMap = GenerateNewMap(tile, scenario); - FactionContext.Pop(); + newMap = GenerateNewMap(tile, scenario); + } - // Add new faction to all maps but the new + // Add new faction to all maps but the new one (it's already handled during generation) foreach (Map map in Find.Maps) if (map != newMap) MapSetup.InitNewFactionData(map, newFaction); @@ -66,15 +67,8 @@ public static void CreateFaction(int sessionId, string factionName, int tile, st foreach (var f in Find.FactionManager.AllFactions.Where(f => f.IsPlayer)) map.attackTargetsCache.Notify_FactionHostilityChanged(f, newFaction); - FactionContext.Push(newFaction); - try - { + using (MpScope.PushFaction(newFaction)) InitNewGame(); - } - finally - { - FactionContext.Pop(); - } if (self) { @@ -92,7 +86,7 @@ public static void CreateFaction(int sessionId, string factionName, int tile, st }, "GeneratingMap", doAsynchronously: true, GameAndMapInitExceptionHandlers.ErrorWhileGeneratingMap); } - private static Map GenerateNewMap(int tile, string scenario) + private static Map GenerateNewMap(int tile, Scenario scenario) { // This has to be null, otherwise, during map generation, Faction.OfPlayer returns it which breaks FactionContext Find.GameInitData.playerFaction = null; @@ -104,7 +98,8 @@ private static Map GenerateNewMap(int tile, string scenario) Find.WorldObjects.Add(mapParent); var prevScenario = Find.Scenario; - Current.Game.scenarioInt = DefDatabase.AllDefs.First(s => s.defName == scenario).scenario; + Current.Game.Scenario = scenario; + generatingMap = true; try { @@ -116,7 +111,8 @@ private static Map GenerateNewMap(int tile, string scenario) } finally { - Current.Game.scenarioInt = prevScenario; + generatingMap = false; + Current.Game.Scenario = prevScenario; } } @@ -126,18 +122,13 @@ private static void InitNewGame() ResearchUtility.ApplyPlayerStartingResearch(); } - public static void SetGameInitData() + public static void PrepareState(int sessionId) { Current.Game.InitData = new GameInitData { startingPawnCount = 3, gameToLoad = "dummy" // Prevent special calculation path in GenTicks.TicksAbs }; - } - - public static void PrepareState(int sessionId) - { - SetGameInitData(); if (pawnStore.TryGetValue(sessionId, out var pawns)) { @@ -150,35 +141,30 @@ public static void PrepareState(int sessionId) } } - private static Faction NewFaction(int id, string name, FactionDef def, ChooseIdeoInfo chooseIdeoInfo) + private static Faction NewFactionWithIdeo(string name, FactionDef def, ChooseIdeoInfo chooseIdeoInfo) { - Faction faction = Find.FactionManager.AllFactions.FirstOrDefault(f => f.loadID == id); + var faction = new Faction { loadID = Find.UniqueIDsManager.GetNextFactionID(), def = def }; + faction.ideos = new FactionIdeosTracker(faction); - if (faction == null) + if (!ModsConfig.IdeologyActive || Find.IdeoManager.classicMode) { - faction = new Faction { loadID = id, def = def }; - faction.ideos = new FactionIdeosTracker(faction); - - if (!ModsConfig.IdeologyActive || Find.IdeoManager.classicMode) - { - faction.ideos.SetPrimary(Faction.OfPlayer.ideos.PrimaryIdeo); - } - else - { - var newIdeo = GenerateIdeo(chooseIdeoInfo); - faction.ideos.SetPrimary(newIdeo); - Find.IdeoManager.Add(newIdeo); - } + faction.ideos.SetPrimary(Faction.OfPlayer.ideos.PrimaryIdeo); + } + else + { + var newIdeo = GenerateIdeo(chooseIdeoInfo); + faction.ideos.SetPrimary(newIdeo); + Find.IdeoManager.Add(newIdeo); + } - foreach (Faction other in Find.FactionManager.AllFactionsListForReading) - faction.TryMakeInitialRelationsWith(other); + foreach (Faction other in Find.FactionManager.AllFactionsListForReading) + faction.TryMakeInitialRelationsWith(other); - Find.FactionManager.Add(faction); + Find.FactionManager.Add(faction); - var newWorldFactionData = FactionWorldData.New(faction.loadID); - Multiplayer.WorldComp.factionData[faction.loadID] = newWorldFactionData; - newWorldFactionData.ReassignIds(); - } + var newWorldFactionData = FactionWorldData.New(faction.loadID); + Multiplayer.WorldComp.factionData[faction.loadID] = newWorldFactionData; + newWorldFactionData.ReassignIds(); faction.Name = name; faction.def = def; diff --git a/Source/Client/Factions/FactionExtensions.cs b/Source/Client/Factions/FactionExtensions.cs index a971cd47..53ed80b0 100644 --- a/Source/Client/Factions/FactionExtensions.cs +++ b/Source/Client/Factions/FactionExtensions.cs @@ -10,8 +10,6 @@ public static class FactionExtensions // Applies faction's map components if map not null public static void PushFaction(this Map map, Faction f) { - map?.MpComp()?.CheckInvariant(); - var faction = FactionContext.Push(f); if (faction == null) return; diff --git a/Source/Client/Factions/FactionRepeater.cs b/Source/Client/Factions/FactionRepeater.cs index 01540e8b..d7db8d4d 100644 --- a/Source/Client/Factions/FactionRepeater.cs +++ b/Source/Client/Factions/FactionRepeater.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using HarmonyLib; using Multiplayer.Client.Factions; using RimWorld; @@ -8,15 +9,22 @@ namespace Multiplayer.Client { static class FactionRepeater { - public static bool Template(Action method, Map map, ref bool ignore) + public static bool Template(Dictionary factionIdToData, Action dataProcessor, Map map, ref bool ignore) { if (Multiplayer.Client == null || ignore) return true; ignore = true; - foreach (var data in map.MpComp().factionData.Values) + foreach (var (id, data) in factionIdToData) { - map.PushFaction(data.factionId); - method(data); + map.PushFaction(id); + try + { + dataProcessor(data); + } + catch (Exception e) + { + Log.Error($"Exception in FactionRepeater for faction {id} {Faction.OfPlayer}: {e}"); + } map.PopFaction(); } ignore = false; @@ -31,7 +39,12 @@ static class ListerFilthRebuildPatch static bool ignore; static bool Prefix(ListerFilthInHomeArea __instance) => - FactionRepeater.Template(d => d.listerFilthInHomeArea.RebuildAll(), __instance.map, ref ignore); + FactionRepeater.Template( + __instance.map.MpComp()?.factionData, // The template doesn't run if not in MP + d => d.listerFilthInHomeArea.RebuildAll(), + __instance.map, + ref ignore + ); } [HarmonyPatch(typeof(ListerFilthInHomeArea), nameof(ListerFilthInHomeArea.Notify_FilthSpawned))] @@ -40,7 +53,12 @@ static class ListerFilthSpawnedPatch static bool ignore; static bool Prefix(ListerFilthInHomeArea __instance, Filth f) => - FactionRepeater.Template(d => d.listerFilthInHomeArea.Notify_FilthSpawned(f), __instance.map, ref ignore); + FactionRepeater.Template( + __instance.map.MpComp()?.factionData, + d => d.listerFilthInHomeArea.Notify_FilthSpawned(f), + __instance.map, + ref ignore + ); } // todo look at slot group in ListerHaulables @@ -51,7 +69,12 @@ static class ListerHaulablesSpawnedPatch static bool ignore; static bool Prefix(ListerHaulables __instance, Thing t) => - FactionRepeater.Template(d => d.listerHaulables.Notify_Spawned(t), __instance.map, ref ignore); + FactionRepeater.Template( + __instance.map.MpComp()?.factionData, + d => d.listerHaulables.Notify_Spawned(t), + __instance.map, + ref ignore + ); } [HarmonyPatch(typeof(ListerHaulables), nameof(ListerHaulables.Notify_DeSpawned))] @@ -60,7 +83,12 @@ static class ListerHaulablesDespawnedPatch static bool ignore; static bool Prefix(ListerHaulables __instance, Thing t) => - FactionRepeater.Template(d => d.listerHaulables.Notify_DeSpawned(t), __instance.map, ref ignore); + FactionRepeater.Template( + __instance.map.MpComp()?.factionData, + d => d.listerHaulables.Notify_DeSpawned(t), + __instance.map, + ref ignore + ); } [HarmonyPatch(typeof(ListerMergeables), nameof(ListerMergeables.Notify_Spawned))] @@ -69,7 +97,12 @@ static class ListerMergeablesSpawnedPatch static bool ignore; static bool Prefix(ListerMergeables __instance, Thing t) => - FactionRepeater.Template(d => d.listerMergeables.Notify_Spawned(t), __instance.map, ref ignore); + FactionRepeater.Template( + __instance.map.MpComp()?.factionData, + d => d.listerMergeables.Notify_Spawned(t), + __instance.map, + ref ignore + ); } [HarmonyPatch(typeof(ListerMergeables), nameof(ListerMergeables.Notify_DeSpawned))] @@ -78,7 +111,12 @@ static class ListerMergeablesDespawnedPatch static bool ignore; static bool Prefix(ListerMergeables __instance, Thing t) => - FactionRepeater.Template(d => d.listerMergeables.Notify_DeSpawned(t), __instance.map, ref ignore); + FactionRepeater.Template( + __instance.map.MpComp()?.factionData, + d => d.listerMergeables.Notify_DeSpawned(t), + __instance.map, + ref ignore + ); } [HarmonyPatch(typeof(ListerMergeables), nameof(ListerMergeables.Notify_ThingStackChanged))] @@ -87,7 +125,54 @@ static class ListerMergeablesStackChangedPatch static bool ignore; static bool Prefix(ListerMergeables __instance, Thing t) => - FactionRepeater.Template(d => d.listerMergeables.Notify_ThingStackChanged(t), __instance.map, ref ignore); + FactionRepeater.Template( + __instance.map.MpComp()?.factionData, + d => d.listerMergeables.Notify_ThingStackChanged(t), + __instance.map, + ref ignore + ); + } + + [HarmonyPatch(typeof(History), nameof(History.HistoryTick))] + static class HistoryTickPatch + { + static bool ignore; + + static bool Prefix() => + FactionRepeater.Template( + Multiplayer.game?.worldComp.factionData, // The template doesn't run if not in MP + d => d.history.HistoryTick(), + null, + ref ignore + ); + } + + [HarmonyPatch(typeof(Storyteller), nameof(Storyteller.StorytellerTick))] + static class StorytellerTickPatch + { + static bool ignore; + + static bool Prefix() => + FactionRepeater.Template( + Multiplayer.game?.worldComp.factionData, + d => d.storyteller.StorytellerTick(), + null, + ref ignore + ); + } + + [HarmonyPatch(typeof(StoryWatcher), nameof(StoryWatcher.StoryWatcherTick))] + static class StoryWatcherTickPatch + { + static bool ignore; + + static bool Prefix() => + FactionRepeater.Template( + Multiplayer.game?.worldComp.factionData, + d => d.storyWatcher.StoryWatcherTick(), + null, + ref ignore + ); } } diff --git a/Source/Client/Factions/FactionSidebar.cs b/Source/Client/Factions/FactionSidebar.cs index 60b68ba3..9511ea32 100644 --- a/Source/Client/Factions/FactionSidebar.cs +++ b/Source/Client/Factions/FactionSidebar.cs @@ -12,11 +12,10 @@ namespace Multiplayer.Client; -public class FactionSidebar +public static class FactionSidebar { - private static string scenario = "Crashlanded"; - private static string factionName; - private static FactionRelationKind hostility = FactionRelationKind.Neutral; + private static ScenarioDef chosenScenario = ScenarioDefOf.Crashlanded; // null means current game's scenario + private static string newFactionName; private static Vector2 scroll; public static void DrawFactionSidebar(Rect rect) @@ -51,14 +50,25 @@ public static void DrawFactionSidebar(Rect rect) private static void DrawFactionCreator() { - factionName = Widgets.TextField(Layouter.Rect(150, 24), factionName); + Layouter.BeginHorizontal(); + // Label($"Scenario: {chosenScenario?.label ?? Find.Scenario.name}"); + // + // if (Mouse.IsOver(Layouter.LastRect())) + // Widgets.DrawAltRect(Layouter.LastRect()); + // + // if (Widgets.ButtonInvisible(Layouter.LastRect())) + // OpenScenarioChooser(); + + Layouter.EndHorizontal(); + + newFactionName = Widgets.TextField(Layouter.Rect(150, 24), newFactionName); if (Button("Settle new faction", 130)) { var tileError = new StringBuilder(); // todo check faction name not exists - if (factionName.NullOrEmpty()) + if (newFactionName.NullOrEmpty()) Messages.Message("The faction name can't be empty.", MessageTypeDefOf.RejectInput, historical: false); else if (Find.WorldInterface.SelectedTile < 0) Messages.Message("MustSelectStartingSite".TranslateWithBackup("MustSelectLandingSite"), MessageTypeDefOf.RejectInput, historical: false); @@ -94,15 +104,39 @@ private static void DrawFactionCreator() } } + private static void OpenScenarioChooser() + { + Find.WindowStack.Add(new FloatMenu( + DefDatabase.AllDefs. + Where(def => def.scenario.GetHashCode() != Find.Scenario.GetHashCode()). + Except(ScenarioDefOf.Tutorial). + Prepend(null). + Select(s => + { + return new FloatMenuOption(s?.label ?? Find.Scenario.name, () => + { + chosenScenario = s; + }); + }). + ToList())); + } + private static void PreparePawns() { + var scenario = chosenScenario?.scenario ?? Current.Game.Scenario; var prevState = Current.programStateInt; + Current.programStateInt = ProgramState.Entry; // Set ProgramState.Entry so that InInterface is false - try + Current.Game.InitData = new GameInitData { - FactionCreator.SetGameInitData(); + startingPawnCount = 3, + startingPawnKind = scenario.playerFaction.factionDef.basicMemberKind, + gameToLoad = "dummy" // Prevent special calculation path in GenTicks.TicksAbs + }; + try + { // Create starting pawns new ScenPart_ConfigPage_ConfigureStartingPawns { pawnCount = Current.Game.InitData.startingPawnCount } .GenerateStartingPawns(); @@ -115,7 +149,7 @@ private static void PreparePawns() private static void DoCreateFaction(ChooseIdeoInfo chooseIdeoInfo) { - int sessionId = Multiplayer.session.playerId; + int playerId = Multiplayer.session.playerId; var prevState = Current.programStateInt; Current.programStateInt = ProgramState.Playing; // This is to force a sync @@ -123,16 +157,15 @@ private static void DoCreateFaction(ChooseIdeoInfo chooseIdeoInfo) { foreach (var p in Current.Game.InitData.startingAndOptionalPawns) FactionCreator.SendPawn( - sessionId, + playerId, p ); FactionCreator.CreateFaction( - sessionId, - factionName, + playerId, + newFactionName, Find.WorldInterface.SelectedTile, - scenario, - hostility, + chosenScenario, chooseIdeoInfo ); } @@ -154,27 +187,15 @@ private static void DrawFactionChooser() if (i % 2 == 0) Widgets.DrawAltRect(Layouter.GroupRect()); - if (Mouse.IsOver(Layouter.GroupRect())) - { - // todo this doesn't exactly work - foreach (var settlement in Find.WorldObjects.Settlements) - if (settlement.Faction == playerFaction) - Graphics.DrawMesh(MeshPool.plane20, - Find.WorldGrid.GetTileCenter(settlement.Tile), - Quaternion.identity, - GenDraw.ArrowMatWhite, - 0); - - Widgets.DrawRectFast(Layouter.GroupRect(), new Color(0.2f, 0.2f, 0.2f)); - } - using (MpStyle.Set(TextAnchor.MiddleCenter)) Label(playerFaction.Name, true); Layouter.FlexibleWidth(); if (Button("Join", 70)) { - Current.Game.CurrentMap = Find.Maps.First(m => m.ParentFaction == playerFaction); + var factionHome = Find.Maps.FirstOrDefault(m => m.ParentFaction == playerFaction); + if (factionHome != null) + Current.Game.CurrentMap = factionHome; // todo setting faction of self Multiplayer.Client.Send( diff --git a/Source/Client/Factions/Forbiddables.cs b/Source/Client/Factions/Forbiddables.cs index 424bab26..25c598d2 100644 --- a/Source/Client/Factions/Forbiddables.cs +++ b/Source/Client/Factions/Forbiddables.cs @@ -55,7 +55,7 @@ static void Prefix(CompForbiddable __instance, ref bool __state) } [HarmonyPriority(MpPriority.MpLast)] - static void Postfix(CompForbiddable __instance, bool __state) + static void Finalizer(CompForbiddable __instance, bool __state) { __instance.forbiddenInt = __state; FactionContext.Pop(); diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 72c31e24..14cde5b6 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -1,14 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Multiplayer.API; -using Multiplayer.Client.Util; +using Multiplayer.Client.Factions; using RimWorld; using RimWorld.Planet; using UnityEngine; using Verse; +using Verse.AI; namespace Multiplayer.Client.Patches; @@ -38,14 +40,20 @@ static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) yield return new Command_Action { - defaultLabel = "Change faction relation", + defaultLabel = $"Change faction relation {Faction.OfPlayer.HostileTo(otherFaction)}", icon = MultiplayerStatic.ChangeRelationIcon, action = () => { List list = new List { - new(FactionRelationKind.Hostile.ToString(), () => { SetFactionRelation(otherFaction, FactionRelationKind.Hostile); }), - new(FactionRelationKind.Neutral.ToString(), () => { SetFactionRelation(otherFaction, FactionRelationKind.Neutral); }) + new(FactionRelationKind.Hostile.ToString(), () => + { + SetRelation(otherFaction, FactionRelationKind.Hostile); + }), + new(FactionRelationKind.Neutral.ToString(), () => + { + SetRelation(otherFaction, FactionRelationKind.Neutral); + }) }; Find.WindowStack.Add(new FloatMenu(list)); @@ -55,9 +63,26 @@ static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) } [SyncMethod] - static void SetFactionRelation(Faction other, FactionRelationKind kind) + static void SetRelation(Faction other, FactionRelationKind kind) { Faction.OfPlayer.SetRelation(new FactionRelation(other, kind)); + + foreach (Map map in Find.Maps) + map.attackTargetsCache.Notify_FactionHostilityChanged(Faction.OfPlayer, other); + } +} + +[HarmonyPatch(typeof(SettlementUtility), nameof(SettlementUtility.AttackNow))] +static class AttackNowPatch +{ + static void Prefix(Caravan caravan) + { + FactionContext.Push(caravan.Faction); + } + + static void Finalizer() + { + FactionContext.Pop(); } } @@ -225,62 +250,304 @@ static void Finalizer() } } -[HarmonyPatch(typeof(Page_ConfigureStartingPawns), nameof(Page_ConfigureStartingPawns.DoWindowContents))] -static class ConfigureStartingPawns_DoWindowContents_Patch +[HarmonyPatch(typeof(FactionIdeosTracker), nameof(FactionIdeosTracker.RecalculateIdeosBasedOnPlayerPawns))] +static class RecalculateFactionIdeosContext +{ + static void Prefix(FactionIdeosTracker __instance) + { + FactionContext.Push(__instance.faction); + } + + static void Finalizer() + { + FactionContext.Pop(); + } +} + +[HarmonyPatch(typeof(Ideo), nameof(Ideo.RecacheColonistBelieverCount))] +static class RecacheColonistBelieverCountPatch +{ + private static MethodInfo allColonists = AccessTools.PropertyGetter(typeof(PawnsFinder), nameof(PawnsFinder.AllMapsCaravansAndTravelingTransportPods_Alive_FreeColonists_NoCryptosleep)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand == allColonists) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(ColonistsAllFactions)); + yield return inst; + } + } + + private static List colonistsAllFactions = new(); + + private static List ColonistsAllFactions() + { + colonistsAllFactions.Clear(); + + foreach (var p in PawnsFinder.AllMapsCaravansAndTravelingTransportPods_Alive) + { + if (IsColonistAnyFaction(p) && p.HostFaction == null && !p.InCryptosleep) + colonistsAllFactions.Add(p); + } + + return colonistsAllFactions; + } + + public static bool IsColonistAnyFaction(Pawn p) + { + if (p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike) + return !p.IsSlave || p.guest.SlaveIsSecure; + return false; + } + + public static bool IsColonyMechAnyFaction(Pawn p) + { + if (ModsConfig.BiotechActive && p.RaceProps.IsMechanoid && p.Faction == Faction.OfPlayer && p.MentalStateDef == null) + return p.HostFaction == null || p.IsSlave; + return false; + } +} + +[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.AnyPawnBlockingMapRemoval), MethodType.Getter)] +static class AnyPawnBlockingMapRemovalPatch +{ + private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); + private static MethodInfo isColonyMech = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonyMech)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand == isColonist) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonistAnyFaction)); + + if (inst.operand == isColonyMech) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonyMechAnyFaction)); + + yield return inst; + } + } + + static void Postfix(MapPawns __instance, ref bool __result) + { + for (int i = 0; i < __instance.pawnsSpawned.Count; i++) + { + var p = __instance.pawnsSpawned[i]; + if (p.Faction is { IsPlayer: true } || p.HostFaction is { IsPlayer: true }) + { + Job curJob = p.CurJob; + if (curJob is { exitMapOnArrival: true }) + { + __result = true; + break; + } + + if (p.health.hediffSet.InLabor()) + { + __result = true; + break; + } + } + } + } +} + +[HarmonyPatch(typeof(Precept_Role), nameof(Precept_Role.ValidatePawn))] +static class ValidatePawnPatch +{ + private static MethodInfo isFreeNonSlaveColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeNonSlaveColonist)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand == isFreeNonSlaveColonist) + inst.operand = AccessTools.Method(typeof(ValidatePawnPatch), nameof(IsFreeNonSlaveColonistAnyFaction)); + + yield return inst; + } + } + + public static bool IsFreeNonSlaveColonistAnyFaction(Pawn p) + { + return p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike && p.HostFaction == null && !p.IsSlave; + } +} + +[HarmonyPatch(typeof(Faction), nameof(Faction.HasGoodwill), MethodType.Getter)] +static class PlayerFactionsHaveGoodwill +{ + static void Postfix(Faction __instance, ref bool __result) + { + if (__instance.IsPlayer) + __result = true; + } +} + +[HarmonyPatch(typeof(GenHostility), nameof(GenHostility.IsActiveThreatToPlayer))] +static class IsActiveThreatToAnyPlayer +{ + static void Postfix(IAttackTarget target, ref bool __result) + { + foreach (var f in Find.FactionManager.AllFactions) + if (f.IsPlayer) + __result |= GenHostility.IsActiveThreatTo(target, f); + } +} + +[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string))] +static class LetterStackReceiveOnlyMyFaction +{ + // todo the letter might get culled from the archive if it isn't in the stack and Sync depends on the archive + static void Postfix(LetterStack __instance, Letter let) + { + if (Multiplayer.RealPlayerFaction != Faction.OfPlayer) + __instance.letters.Remove(let); + } +} + +[HarmonyPatch] +static class DontClearDialogBeginRitualCache +{ + private static MethodInfo listClear = AccessTools.Method(typeof(List), "Clear"); + + static IEnumerable TargetMethods() + { + yield return typeof(Dialog_BeginRitual).GetConstructors(BindingFlags.Public | BindingFlags.Instance).First(); + } + + static IEnumerable Transpiler(IEnumerable insts, ILGenerator gen) + { + var list = insts.ToList(); + var brLabel = gen.DefineLabel(); + + foreach (var inst in list) + { + if (inst.operand == listClear) + { + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(DontClearDialogBeginRitualCache), nameof(ShouldCancelCacheClear))); + yield return new CodeInstruction(OpCodes.Brfalse, brLabel); + yield return new CodeInstruction(OpCodes.Pop); + yield return new CodeInstruction(OpCodes.Ret); + yield return new CodeInstruction(OpCodes.Nop) { labels = { brLabel } }; + } + + yield return inst; + } + } + + static bool ShouldCancelCacheClear() + { + return Multiplayer.Ticking || Multiplayer.ExecutingCmds; + } +} + +[HarmonyPatch(typeof(Bill), nameof(Bill.ValidateSettings))] +static class BillValidateSettingsPatch +{ + static void Prefix(Bill __instance) + { + if (Multiplayer.Client == null) return; + FactionContext.Push(__instance.pawnRestriction?.Faction); // todo HostFaction, SlaveFaction? + } + + static void Finalizer() + { + if (Multiplayer.Client == null) return; + FactionContext.Pop(); + } +} + +[HarmonyPatch(typeof(Bill_Production), nameof(Bill_Production.ValidateSettings))] +static class BillProductionValidateSettingsPatch { - static void Prefix(ref ProgramState __state) + static void Prefix(Bill_Production __instance, ref Map __state) { - __state = Current.ProgramState; - Current.programStateInt = ProgramState.Entry; + if (Multiplayer.Client == null) return; + + var zoneManager = __instance.storeZone?.zoneManager ?? __instance.includeFromZone?.zoneManager; + if (__instance.Map != null && zoneManager != null) + { + __instance.Map.PushFaction(zoneManager.map.MpComp().GetFactionId(zoneManager)); + __state = __instance.Map; + } } - static void Finalizer(ProgramState __state) + static void Finalizer(Map __state) { - Current.programStateInt = __state; + __state?.PopFaction(); } } -[HarmonyPatch(typeof(Page_ConfigureStartingPawns), nameof(Page_ConfigureStartingPawns.RandomizeCurPawn))] -static class ConfigureStartingPawns_RandomizeCurPawn_Patch +[HarmonyPatch(typeof(Apparel), nameof(Apparel.WornGraphicPath), MethodType.Getter)] +static class ApparelWornGraphicPathGetterPatch { - static void Prefix(ref ProgramState __state) + private static FieldInfo thingIDNumberField = AccessTools.Field(typeof(Apparel), nameof(Apparel.thingIDNumber)); + + static IEnumerable Transpiler(IEnumerable insts) { - __state = Current.ProgramState; - Current.programStateInt = ProgramState.Entry; + foreach (var inst in insts) + { + yield return inst; + + // This instruction is part of wornGraphicPaths[thingIDNumber % wornGraphicPaths.Count] + // The function makes sure the id is positive + if (inst.operand == thingIDNumberField) + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(ApparelWornGraphicPathGetterPatch), nameof(MakeIdPositive))); + } } - static void Finalizer(ProgramState __state) + private static int MakeIdPositive(int id) { - Current.programStateInt = __state; + return id < 0 ? -id : id; } } -[HarmonyPatch(typeof(LifeStageWorker_HumanlikeAdult), nameof(LifeStageWorker_HumanlikeAdult.Notify_LifeStageStarted))] -static class LifeStageWorker_Patch +[HarmonyPatch(typeof(ScenPart_ScatterThings), nameof(ScenPart_ScatterThings.GenerateIntoMap))] +static class ScenPartScatterThingsPatch { static bool Prefix() { - // Corresponds to "Current.ProgramState == ProgramState.Playing" check in Notify_LifeStageStarted - return !ScribeUtil.loading; + return Multiplayer.Client == null || FactionCreator.generatingMap; } } -[HarmonyPatch(typeof(StartingPawnUtility), nameof(StartingPawnUtility.GetGenerationRequest))] -static class StartingPawnUtility_GetGenerationRequest_Patch +[HarmonyPatch(typeof(ScenPart_PlayerPawnsArriveMethod), nameof(ScenPart_PlayerPawnsArriveMethod.GenerateIntoMap))] +static class ScenPartPlayerPawnsArriveMethodPatch { - static void Postfix(ref PawnGenerationRequest __result) + static bool Prefix() { - if (Multiplayer.Client != null) - __result.CanGeneratePawnRelations = false; + return Multiplayer.Client == null || FactionCreator.generatingMap; } } -[HarmonyPatch(typeof(StartingPawnUtility), nameof(StartingPawnUtility.DefaultStartingPawnRequest), MethodType.Getter)] -static class StartingPawnUtility_DefaultStartingPawnRequest_Patch +[HarmonyPatch(typeof(CharacterCardUtility), nameof(CharacterCardUtility.DoTopStack))] +static class CharacterCardUtilityDontDrawIdeoPlate { - static void Postfix(ref PawnGenerationRequest __result) + private static FieldInfo classicModeField = AccessTools.Field(typeof(IdeoManager), nameof(IdeoManager.classicMode)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + yield return inst; + + // Don't draw the ideo plate while choosing starting pawns in multifaction + if (inst.operand == classicModeField) + { + yield return new CodeInstruction(OpCodes.Ldarg_2); + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(CharacterCardUtilityDontDrawIdeoPlate), nameof(DontDrawIdeoPlate))); + yield return new CodeInstruction(OpCodes.Or); + } + } + } + + private static bool DontDrawIdeoPlate(bool generating) { - if (Multiplayer.Client != null) - __result.CanGeneratePawnRelations = false; + return Multiplayer.Client != null && generating; } } diff --git a/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs b/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs index aebfea52..0a312135 100644 --- a/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs +++ b/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs @@ -46,4 +46,15 @@ public override void DoWindowContents(Rect inRect) DoBottomButtons(inRect); } + + public override bool CanDoNext() + { + if (pageChooseIdeo.selectedIdeo == null) + { + Messages.Message("Please select a preset.", MessageTypeDefOf.RejectInput, historical: false); + return false; + } + + return base.CanDoNext(); + } } diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index 2798dbea..8a563d50 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -123,10 +123,6 @@ public static void InitMultiplayer() // Double Execute ensures it'll run last. LongEventHandler.ExecuteWhenFinished(EarlyInit.LatePatches); }); - -#if DEBUG - Application.logMessageReceivedThreaded -= Log.Notify_MessageReceivedThreadedInternal; -#endif } [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index 74e979f7..334ce2ba 100644 --- a/Source/Client/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -29,7 +29,7 @@ - + @@ -48,11 +48,6 @@ - - - - - diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index ebb02eb7..5f54dc6d 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -327,8 +327,8 @@ void LogError(string str) // Remove side effects from methods which are non-deterministic during ticking (e.g. camera dependent motes and sound effects) { - var randPatchPrefix = new HarmonyMethod(typeof(RandPatches), "Prefix"); - var randPatchPostfix = new HarmonyMethod(typeof(RandPatches), "Postfix"); + var randPatchPrefix = new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Prefix)); + var randPatchPostfix = new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Postfix)); var subSustainerStart = MpMethodUtil.GetLambda(typeof(SubSustainer), parentMethodType: MethodType.Constructor, parentArgs: new[] { typeof(Sustainer), typeof(SubSoundDef) }); var sampleCtor = typeof(Sample).GetConstructor(new[] { typeof(SubSoundDef) }); @@ -367,8 +367,8 @@ void LogError(string str) // Set ThingContext and FactionContext (for pawns and buildings) in common Thing methods { - var thingMethodPrefix = new HarmonyMethod(typeof(ThingMethodPatches).GetMethod("Prefix")); - var thingMethodPostfix = new HarmonyMethod(typeof(ThingMethodPatches).GetMethod("Postfix")); + var thingMethodPrefix = new HarmonyMethod(typeof(ThingMethodPatches).GetMethod(nameof(ThingMethodPatches.Prefix))); + var thingMethodFinalizer = new HarmonyMethod(typeof(ThingMethodPatches).GetMethod(nameof(ThingMethodPatches.Finalizer))); var thingMethodPrefixSpawnSetup = new HarmonyMethod(typeof(ThingMethodPatches).GetMethod(nameof(ThingMethodPatches.Prefix_SpawnSetup))); var thingMethods = new[] @@ -385,7 +385,7 @@ void LogError(string str) // SpawnSetup is patched separately because it sets the map var spawnSetupMethod = t.GetMethod("SpawnSetup", BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); if (spawnSetupMethod != null) - harmony.PatchMeasure(spawnSetupMethod, thingMethodPrefixSpawnSetup, thingMethodPostfix); + harmony.PatchMeasure(spawnSetupMethod, thingMethodPrefixSpawnSetup, finalizer: thingMethodFinalizer); foreach ((string m, Type[] args) in thingMethods) { @@ -394,7 +394,7 @@ void LogError(string str) { try { - harmony.PatchMeasure(method, thingMethodPrefix, thingMethodPostfix); + harmony.PatchMeasure(method, thingMethodPrefix, finalizer: thingMethodFinalizer); } catch (Exception e) { LogError($"FAIL: {method.DeclaringType.FullName}:{method.Name} with {e}"); } @@ -417,8 +417,8 @@ void LogError(string str) // Set the map time for GUI methods depending on it { - var setMapTimePrefix = new HarmonyMethod(AccessTools.Method(typeof(SetMapTimeForUI), "Prefix")); - var setMapTimePostfix = new HarmonyMethod(AccessTools.Method(typeof(SetMapTimeForUI), "Postfix")); + var setMapTimePrefix = new HarmonyMethod(AccessTools.Method(typeof(SetMapTimeForUI), nameof(SetMapTimeForUI.Prefix))); + var setMapTimePostfix = new HarmonyMethod(AccessTools.Method(typeof(SetMapTimeForUI), nameof(SetMapTimeForUI.Postfix))); var windowMethods = new[] { "DoWindowContents", "WindowUpdate" }; foreach (string m in windowMethods) diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 917688af..d008482e 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -124,9 +124,6 @@ private static void SetupGameFromSingleplayer() { var worldComp = new MultiplayerWorldComp(Find.World); - Faction.OfPlayer.Name = $"{Multiplayer.username}'s faction"; - //comp.factionData[Faction.OfPlayer.loadID] = FactionWorldData.FromCurrent(); - Multiplayer.game = new MultiplayerGame { gameComp = new MultiplayerGameComp(), @@ -134,10 +131,13 @@ private static void SetupGameFromSingleplayer() worldComp = worldComp }; + Faction.OfPlayer.Name = $"{Multiplayer.username}'s faction"; + var spectator = AddNewFaction("Spectator", FactionDefOf.PlayerColony); spectator.hidden = true; spectator.SetRelation(new FactionRelation(Faction.OfPlayer, FactionRelationKind.Neutral)); + worldComp.factionData[Faction.OfPlayer.loadID] = FactionWorldData.FromCurrent(Faction.OfPlayer.loadID); worldComp.factionData[spectator.loadID] = FactionWorldData.New(spectator.loadID); worldComp.spectatorFaction = spectator; diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index d4aab3bf..6e728494 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -1,3 +1,4 @@ +using System; using HarmonyLib; using RimWorld; using RimWorld.Planet; @@ -10,6 +11,7 @@ using RimWorld.QuestGen; using UnityEngine; using Verse; +using Random = UnityEngine.Random; namespace Multiplayer.Client.Patches { @@ -339,4 +341,143 @@ static IEnumerable Transpiler(IEnumerable inst } } + [HarmonyPatch(typeof(SituationalThoughtHandler), nameof(SituationalThoughtHandler.CheckRecalculateSocialThoughts))] + static class DontRecalculateSocialThoughtsInInterface + { + static bool Prefix(SituationalThoughtHandler __instance, Pawn otherPawn) + { + if (!Multiplayer.InInterface) return true; + + // This initializer needs to always run (the method itself begins with it) + if (!__instance.cachedSocialThoughts.TryGetValue(otherPawn, out var value)) + { + value = new SituationalThoughtHandler.CachedSocialThoughts(); + __instance.cachedSocialThoughts.Add(otherPawn, value); + } + + return false; + } + } + + [HarmonyPatch(typeof(SituationalThoughtHandler), nameof(SituationalThoughtHandler.CheckRecalculateMoodThoughts))] + static class DontRecalculateMoodThoughtsInInterface + { + static bool Prefix() + { + return !Multiplayer.InInterface; + } + } + + [HarmonyPatch(typeof(PawnCapacitiesHandler), nameof(PawnCapacitiesHandler.GetLevel))] + static class PawnCapacitiesHandlerGetLevelPatch + { + private static readonly PawnCapacitiesHandler.CacheStatus CachedInInterface = (PawnCapacitiesHandler.CacheStatus)3; + + private static FieldInfo statusField = AccessTools.Field(typeof(PawnCapacitiesHandler.CacheElement), + nameof(PawnCapacitiesHandler.CacheElement.status)); + + static IEnumerable Transpiler(IEnumerable insts) + { + var matcher = new CodeMatcher(insts); + + // Modify cache update checking + matcher.MatchEndForward( + new CodeMatch(OpCodes.Ldfld, statusField), + new CodeMatch(OpCodes.Brtrue_S) + ).Insert( + new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(PawnCapacitiesHandlerGetLevelPatch), nameof(ShouldUpdateCache))), + new CodeInstruction(OpCodes.Ldc_I4_0), + new CodeInstruction(OpCodes.Ceq) + ); + + // Modify status setter + matcher.MatchEndForward( + new CodeMatch(OpCodes.Ldc_I4_2), + new CodeMatch(OpCodes.Stfld, statusField) + ).Insert( + new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(PawnCapacitiesHandlerGetLevelPatch), nameof(NewCacheStatus))) + ); + + return matcher.codes; + } + + private static bool ShouldUpdateCache(PawnCapacitiesHandler.CacheStatus status) + { + return status == PawnCapacitiesHandler.CacheStatus.Uncached || !Multiplayer.InInterface && status == CachedInInterface; + } + + private static PawnCapacitiesHandler.CacheStatus NewCacheStatus(PawnCapacitiesHandler.CacheStatus _) + { + return Multiplayer.InInterface ? CachedInInterface : PawnCapacitiesHandler.CacheStatus.Cached; + } + } + + [HarmonyPatch(typeof(StatWorker), nameof(StatWorker.GetValue), typeof(Thing), typeof(bool), typeof(int))] + static class StatWorkerGetValuePatch + { + private static readonly PawnCapacitiesHandler.CacheStatus CachedInInterface = (PawnCapacitiesHandler.CacheStatus)3; + + private static FieldInfo statusField = AccessTools.Field(typeof(PawnCapacitiesHandler.CacheElement), + nameof(PawnCapacitiesHandler.CacheElement.status)); + + static IEnumerable Transpiler(IEnumerable insts) + { + var matcher = new CodeMatcher(insts); + + // Modify cache update checking + matcher.MatchEndForward( + new CodeMatch(OpCodes.Callvirt, typeof(Dictionary).GetMethod("TryGetValue")) + ).Advance(1).Insert( + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldarg_1), + new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(StatWorkerGetValuePatch), nameof(HasValueInCache))) + ); + + matcher.MatchEndForward( + new CodeMatch(OpCodes.Ldfld, typeof(StatCacheEntry).GetField(nameof(StatCacheEntry.gameTick))) + ).Advance(1).Insert( + new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(Math), nameof(Math.Abs), new[] { typeof(int) })) + ); + + // Modify status setter + matcher.MatchEndForward( + new CodeMatch(OpCodes.Newobj) + ).Advance(1).Insert( + new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(StatWorkerGetValuePatch), nameof(NewCacheStatusCtor))) + ); + + // Modify status setter + matcher.MatchEndForward( + new CodeMatch(OpCodes.Stfld, typeof(StatCacheEntry).GetField(nameof(StatCacheEntry.gameTick))) + ).Insert( + new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(StatWorkerGetValuePatch), nameof(NewCacheStatus))) + ); + + return matcher.codes; + } + + private static bool HasValueInCache(bool hasValueInCache, StatWorker worker, Thing t) + { + var simulating = !Multiplayer.InInterface; + return hasValueInCache && !(simulating && worker.temporaryStatCache[t].gameTick < 0); + } + + private static StatCacheEntry NewCacheStatusCtor(StatCacheEntry entry) + { + entry.gameTick = NewCacheStatus(entry.gameTick); + return entry; + } + + private static int NewCacheStatus(int gameTick) + { + return Multiplayer.InInterface ? -gameTick : gameTick; + } + } + } diff --git a/Source/Client/Patches/MultiplayerPawnComp.cs b/Source/Client/Patches/MultiplayerPawnComp.cs index 895bdce1..e07eb222 100644 --- a/Source/Client/Patches/MultiplayerPawnComp.cs +++ b/Source/Client/Patches/MultiplayerPawnComp.cs @@ -1,12 +1,10 @@ using HarmonyLib; -using RimWorld; using Verse; namespace Multiplayer.Client { public class MultiplayerPawnComp : ThingComp { - public SituationalThoughtHandler thoughtsForInterface; public int lastMap = -1; public int worldPawnRemoveTick = -1; } diff --git a/Source/Client/Patches/SituationalThoughts.cs b/Source/Client/Patches/SituationalThoughts.cs deleted file mode 100644 index 4605d170..00000000 --- a/Source/Client/Patches/SituationalThoughts.cs +++ /dev/null @@ -1,105 +0,0 @@ -using HarmonyLib; -using RimWorld; -using System.Collections.Generic; -using Verse; - -namespace Multiplayer.Client -{ - [HarmonyPatch(typeof(Pawn_NeedsTracker), nameof(Pawn_NeedsTracker.AddOrRemoveNeedsAsAppropriate))] - static class AddOrRemoveNeedMoodPatch - { - static void Postfix(Pawn_NeedsTracker __instance) - { - MultiplayerPawnComp comp = __instance.pawn.GetComp(); - if (__instance.mood == null) - { - comp.thoughtsForInterface = null; - } - else - { - SituationalThoughtHandler thoughts = new SituationalThoughtHandler(__instance.pawn); - comp.thoughtsForInterface = thoughts; - } - } - } - - [HarmonyPatch(typeof(SituationalThoughtHandler), nameof(SituationalThoughtHandler.Notify_SituationalThoughtsDirty))] - static class NotifySituationalThoughtsPatch - { - private static bool ignore; - - static void Prefix(SituationalThoughtHandler __instance) - { - if (ignore) return; - - ignore = true; - - SituationalThoughtHandler thoughts = __instance.pawn.GetComp().thoughtsForInterface; - thoughts?.Notify_SituationalThoughtsDirty(); - - ignore = false; - } - } - - [HarmonyPatch(typeof(SituationalThoughtHandler), nameof(SituationalThoughtHandler.RemoveExpiredThoughtsFromCache))] - static class RemoveExpiredThoughtsFromCachePatch - { - private static bool ignore; - - static void Prefix(SituationalThoughtHandler __instance) - { - if (ignore) return; - - ignore = true; - - SituationalThoughtHandler thoughts = __instance.pawn.GetComp().thoughtsForInterface; - thoughts?.RemoveExpiredThoughtsFromCache(); - - ignore = false; - } - } - - [HarmonyPatch(typeof(SituationalThoughtHandler), nameof(SituationalThoughtHandler.AppendMoodThoughts))] - [HarmonyPriority(Priority.First)] - static class AppendMoodThoughtsPatch - { - private static bool Cancel => Multiplayer.Client != null && !Multiplayer.Ticking && !Multiplayer.ExecutingCmds; - private static bool ignore; - - static bool Prefix(SituationalThoughtHandler __instance, List outThoughts) - { - if (!Cancel || ignore) return true; - - ignore = true; - - SituationalThoughtHandler thoughts = __instance.pawn.GetComp().thoughtsForInterface; - thoughts?.AppendMoodThoughts(outThoughts); - - ignore = false; - - return false; - } - } - - [HarmonyPatch(typeof(SituationalThoughtHandler), nameof(SituationalThoughtHandler.AppendSocialThoughts))] - [HarmonyPriority(Priority.First)] - static class AppendSocialThoughtsPatch - { - private static bool Cancel => Multiplayer.Client != null && !Multiplayer.Ticking && !Multiplayer.ExecutingCmds; - private static bool ignore; - - static bool Prefix(SituationalThoughtHandler __instance, Pawn otherPawn, List outThoughts) - { - if (!Cancel || ignore) return true; - - ignore = true; - - SituationalThoughtHandler thoughts = __instance.pawn.GetComp().thoughtsForInterface; - thoughts?.AppendSocialThoughts(otherPawn, outThoughts); - - ignore = false; - - return false; - } - } -} diff --git a/Source/Client/Patches/ThingMethodPatches.cs b/Source/Client/Patches/ThingMethodPatches.cs index 44c94575..8ce42dbc 100644 --- a/Source/Client/Patches/ThingMethodPatches.cs +++ b/Source/Client/Patches/ThingMethodPatches.cs @@ -18,7 +18,7 @@ static void Prefix(Pawn pawn, ref Container? __state) __state = pawn.Map; } - static void Postfix(Pawn pawn, Container? __state) + static void Finalizer(Pawn pawn, Container? __state) { if (__state is { Inner: var map }) map.PopFaction(); @@ -36,7 +36,7 @@ static void Prefix(Pawn ___pawn, ref Container? __state) __state = ___pawn.Map; } - static void Postfix(Container? __state) + static void Finalizer(Container? __state) { if (__state is { Inner: var map }) map.PopFaction(); @@ -64,15 +64,14 @@ static void Prefix(Pawn_JobTracker __instance, Job newJob, ref Container? _ __state = pawn.Map; } - static void Postfix(Container? __state) + static void Finalizer(Container? __state) { if (__state is { Inner: var map }) map.PopFaction(); } } - [HarmonyPatch(typeof(Pawn_JobTracker))] - [HarmonyPatch(nameof(Pawn_JobTracker.EndCurrentJob))] + [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.EndCurrentJob))] public static class JobTrackerEndCurrent { static void Prefix(Pawn_JobTracker __instance, JobCondition condition, ref Container? __state) @@ -86,15 +85,14 @@ static void Prefix(Pawn_JobTracker __instance, JobCondition condition, ref Conta __state = pawn.Map; } - static void Postfix(Container? __state) + static void Finalizer(Container? __state) { if (__state is { Inner: var map }) map.PopFaction(); } } - [HarmonyPatch(typeof(Pawn_JobTracker))] - [HarmonyPatch(nameof(Pawn_JobTracker.CheckForJobOverride))] + [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.CheckForJobOverride))] public static class JobTrackerOverride { static void Prefix(Pawn_JobTracker __instance, ref Container? __state) @@ -109,7 +107,7 @@ static void Prefix(Pawn_JobTracker __instance, ref Container? __state) __state = pawn.Map; } - static void Postfix(Container? __state) + static void Finalizer(Container? __state) { if (__state is { Inner: var map }) { @@ -146,7 +144,7 @@ public static void Prefix_SpawnSetup(Thing __instance, Map __0, ref Container? __state) + public static void Finalizer(Thing __instance, Container? __state) { if (__state is not { Inner: var map }) return; diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index b1798def..0969f743 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -4,6 +4,7 @@ using RimWorld; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Multiplayer.Client.Util; using UnityEngine; @@ -104,9 +105,21 @@ public class BeginRitualProxy : Dialog_BeginRitual, ISwitchToMap public RitualSession Session => map.MpComp().ritualSession; - public BeginRitualProxy(string header, string ritualLabel, Precept_Ritual ritual, TargetInfo target, Map map, ActionCallback action, Pawn organizer, RitualObligation obligation, Func filter = null, string confirmText = null, List requiredPawns = null, Dictionary forcedForRole = null, string ritualName = null, RitualOutcomeEffectDef outcome = null, List extraInfoText = null, Pawn selectedPawn = null) : base(header, ritualLabel, ritual, target, map, action, organizer, obligation, filter, confirmText, requiredPawns, forcedForRole, ritualName, outcome, extraInfoText, selectedPawn) + public BeginRitualProxy(string header, string ritualLabel, Precept_Ritual ritual, TargetInfo target, Map map, ActionCallback action, Pawn organizer, RitualObligation obligation, Func filter = null, string confirmText = null, List requiredPawns = null, Dictionary forcedForRole = null, string ritualName = null, RitualOutcomeEffectDef outcome = null, List extraInfoText = null, Pawn selectedPawn = null) : + base(header, ritualLabel, ritual, target, map, action, organizer, obligation, filter, confirmText, requiredPawns, forcedForRole, ritualName, outcome, extraInfoText, selectedPawn) { soundClose = SoundDefOf.TabClose; + + // This gets cancelled in the base constructor if called from ticking/cmd in DontClearDialogBeginRitualCache + 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) diff --git a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs index ac287b35..172990fc 100644 --- a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs +++ b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs @@ -1136,9 +1136,8 @@ public static class SyncDictRimWorld }, (ByteReader data) => { - // Note that this only returns live (not archived) letters var id = data.ReadInt32(); - return Find.LetterStack.LettersListForReading.Find(l => l.ID == id); + return (Letter)Find.Archive.ArchivablesListForReading.Find(a => a is Letter l && l.ID == id); }, true }, #endregion diff --git a/Source/Client/Syncing/Game/SyncResearch.cs b/Source/Client/Syncing/Game/SyncResearch.cs deleted file mode 100644 index 64999cf7..00000000 --- a/Source/Client/Syncing/Game/SyncResearch.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using Multiplayer.API; -using RimWorld; -using Verse; - -namespace Multiplayer.Client; - -// Currently unused -public static class SyncResearch -{ - private static Dictionary localResearch = new Dictionary(); - - //[MpPrefix(typeof(ResearchManager), nameof(ResearchManager.ResearchPerformed))] - static bool ResearchPerformed_Prefix(float amount, Pawn researcher) - { - if (Multiplayer.Client == null || !SyncMarkers.researchToil) - return true; - - // todo only faction leader - if (Faction.OfPlayer == Multiplayer.RealPlayerFaction) - { - float current = localResearch.GetValueSafe(researcher.thingIDNumber); - localResearch[researcher.thingIDNumber] = current + amount; - } - - return false; - } - - // Set by faction context - public static ResearchSpeed researchSpeed; - public static ISyncField SyncResearchSpeed = - Sync.Field(null, "Multiplayer.Client.SyncResearch/researchSpeed/[]").SetBufferChanges().InGameLoop(); - - public static void ConstantTick() - { - if (localResearch.Count == 0) return; - - SyncFieldUtil.FieldWatchPrefix(); - - foreach (int pawn in localResearch.Keys.ToList()) - { - SyncResearchSpeed.Watch(null, pawn); - researchSpeed[pawn] = localResearch[pawn]; - localResearch[pawn] = 0; - } - - SyncFieldUtil.FieldWatchPostfix(); - } -} - -public class ResearchSpeed : IExposable -{ - public Dictionary data = new Dictionary(); - - public float this[int pawnId] - { - get => data.TryGetValue(pawnId, out float speed) ? speed : 0f; - - set - { - if (value == 0) data.Remove(pawnId); - else data[pawnId] = value; - } - } - - public void ExposeData() - { - Scribe_Collections.Look(ref data, "data", LookMode.Value, LookMode.Value); - } -} diff --git a/Source/Client/Syncing/Handler/SyncField.cs b/Source/Client/Syncing/Handler/SyncField.cs index d5c2789b..3293bf3b 100644 --- a/Source/Client/Syncing/Handler/SyncField.cs +++ b/Source/Client/Syncing/Handler/SyncField.cs @@ -2,7 +2,6 @@ using Multiplayer.Common; using System; using Multiplayer.Client.Util; -using UnityEngine; using Verse; namespace Multiplayer.Client diff --git a/Source/Client/Util/MethodOf.cs b/Source/Client/Util/MethodOf.cs index 5b921e05..b69fb5e6 100644 --- a/Source/Client/Util/MethodOf.cs +++ b/Source/Client/Util/MethodOf.cs @@ -45,7 +45,7 @@ public static MethodInfo Inner(LambdaExpression expression) { if (expression.Body is not MethodCallExpression outermostExpression) { - if (expression.Body is UnaryExpression ue && ue.Operand is MethodCallExpression me && + if (expression.Body is UnaryExpression { Operand: MethodCallExpression me } ue && me.Object is ConstantExpression ce && ce.Value is MethodInfo mi) return mi; throw new ArgumentException("Invalid Expression. Expression should consist of a Method call only."); diff --git a/Source/Client/Util/MpScope.cs b/Source/Client/Util/MpScope.cs new file mode 100644 index 00000000..624fe179 --- /dev/null +++ b/Source/Client/Util/MpScope.cs @@ -0,0 +1,25 @@ +using System; +using RimWorld; + +namespace Multiplayer.Client.Util; + +public class MpScope : IDisposable +{ + private Action end; + + public void Dispose() + { + end(); + } + + public static MpScope Create(Action begin, Action end) + { + begin(); + return new MpScope { end = end }; + } + + public static MpScope PushFaction(Faction f) + { + return Create(() => FactionContext.Push(f), () => FactionContext.Pop()); + } +} diff --git a/Source/Common/DeferredStackTracingImpl.cs b/Source/Common/DeferredStackTracingImpl.cs index 53a86fce..cd56bb0b 100644 --- a/Source/Common/DeferredStackTracingImpl.cs +++ b/Source/Common/DeferredStackTracingImpl.cs @@ -96,10 +96,11 @@ public unsafe static int TraceImpl(long[] traceIn, ref int hash) trace[depth] = ret; + // info.nameHash == 0 marks methods to skip if (depth < HashInfluence && info.nameHash != 0) hash = HashCombineInt(hash, (int)info.nameHash); - if (++depth == MaxDepth) + if (info.nameHash != 0 && ++depth == MaxDepth) break; if (stackUsage == RBPBased) diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs index 7796b9a7..d2bf5f8f 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.9.2"; - public const int Protocol = 35; + public const string Version = "0.9.3"; + public const int Protocol = 36; public const string ApiAssemblyName = "0MultiplayerAPI"; diff --git a/Source/MultiplayerLoader/Prepatches.cs b/Source/MultiplayerLoader/Prepatches.cs index d9179f02..fe0dc1fb 100644 --- a/Source/MultiplayerLoader/Prepatches.cs +++ b/Source/MultiplayerLoader/Prepatches.cs @@ -14,8 +14,11 @@ static void DontSetResolution(ModuleDefinition module) { if (MpVersion.IsDebug) { - var utilType = module.ImportReference(typeof(ResolutionUtility)).Resolve(); - utilType.FindMethod("SetResolutionRaw").Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ret)); + var resolutionUtilityType = module.ImportReference(typeof(ResolutionUtility)).Resolve(); + resolutionUtilityType.FindMethod("SetResolutionRaw").Body.Instructions.Insert( + 0, + Instruction.Create(OpCodes.Ret) + ); } } } diff --git a/Source/TestsOnMono/Program.cs b/Source/TestsOnMono/Program.cs index c6b48e1a..402caf3a 100644 --- a/Source/TestsOnMono/Program.cs +++ b/Source/TestsOnMono/Program.cs @@ -1,7 +1,5 @@ using System; -using System.Reflection; using System.Runtime.CompilerServices; -using HarmonyLib; using Multiplayer.Client; using Multiplayer.Client.Desyncs;