From 2ae2cc6b7edd3d570a732cf2c8dfd51b8cc566f7 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Sun, 5 Oct 2025 06:06:10 +0200 Subject: [PATCH 1/3] Use a Faction's storyteller instead of the AsyncTimeComp's Fixes delayed storyteller events not showing up. Events were added to the storyteller (IncidentQueue) of AsyncTimeComp, which was never ticked because of StorytellerTickPatch which made the method run per-faction, using the faction's storyteller. This caused a bunch of delayed events to just not work, e.g. requesting traders from the comms console, psychic rituals (summon animals, summon shamblers, void provocation), naturally spawning harbinger trees, and possibly others. --- Source/Client/AsyncTime/AsyncTimeComp.cs | 52 +++++++----------------- Source/Client/Patches/MapSetup.cs | 9 ++-- Source/Client/Syncing/Game/SyncFields.cs | 18 ++------ 3 files changed, 21 insertions(+), 58 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 866609c3..a927a8f3 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -1,16 +1,15 @@ -using HarmonyLib; -using Multiplayer.Common; -using RimWorld; -using RimWorld.Planet; using System; using System.Collections.Generic; -using Verse; +using HarmonyLib; using Multiplayer.Client.Comp; using Multiplayer.Client.Factions; using Multiplayer.Client.Patches; using Multiplayer.Client.Saving; using Multiplayer.Client.Util; -using System.Linq; +using Multiplayer.Common; +using RimWorld; +using RimWorld.Planet; +using Verse; namespace Multiplayer.Client { @@ -86,8 +85,6 @@ public int GameStartAbsTick public bool forcedNormalSpeed; public int eventCount; - public Storyteller storyteller; - public StoryWatcher storyWatcher; public TimeSlower slower = new(); public TickList tickListNormal = new(TickerType.Normal); @@ -131,8 +128,8 @@ public void Tick() TickMapSessions(); - storyteller.StorytellerTick(); - storyWatcher.StoryWatcherTick(); + Find.Storyteller.StorytellerTick(); + Find.StoryWatcher.StoryWatcherTick(); QuestManagerTickAsyncTime(); @@ -173,28 +170,17 @@ public void UpdateManagers() } private TimeSnapshot? prevTime; - private Storyteller prevStoryteller; - private StoryWatcher prevStoryWatcher; public void PreContext() { - if (Multiplayer.GameComp.multifaction) - { - map.PushFaction( - map.ParentFaction is { IsPlayer: true } - ? map.ParentFaction - : Multiplayer.WorldComp.spectatorFaction, - force: true); - } + map.PushFaction( + map.ParentFaction is { IsPlayer: true } + ? map.ParentFaction + : Multiplayer.WorldComp.spectatorFaction, + force: true); prevTime = TimeSnapshot.GetAndSetFromMap(map); - prevStoryteller = Current.Game.storyteller; - prevStoryWatcher = Current.Game.storyWatcher; - - Current.Game.storyteller = storyteller; - Current.Game.storyWatcher = storyWatcher; - Rand.PushState(); Rand.StateCompressed = randState; @@ -204,16 +190,12 @@ public void PreContext() public void PostContext() { - Current.Game.storyteller = prevStoryteller; - Current.Game.storyWatcher = prevStoryWatcher; - prevTime?.Set(); randState = Rand.StateCompressed; Rand.PopState(); - if (Multiplayer.GameComp.multifaction) - map.PopFaction(); + map.PopFaction(); } public void ExposeData() @@ -223,12 +205,6 @@ public void ExposeData() Scribe_Values.Look(ref gameStartAbsTickMap, "gameStartAbsTickMap"); - Scribe_Deep.Look(ref storyteller, "storyteller"); - - Scribe_Deep.Look(ref storyWatcher, "storyWatcher"); - if (Scribe.mode == LoadSaveMode.LoadingVars && storyWatcher == null) - storyWatcher = new StoryWatcher(); - Scribe_Custom.LookULong(ref randState, "randState", 1); } @@ -264,7 +240,7 @@ public void ExecuteCmd(ScheduledCommand cmd) TickPatch.currentExecutingCmdType = cmdType; PreContext(); - map.PushFaction(cmd.GetFaction()); + map.PushFaction(cmd.GetFaction(), force: true); context.map = map; diff --git a/Source/Client/Patches/MapSetup.cs b/Source/Client/Patches/MapSetup.cs index a3a1d066..f762a3a6 100644 --- a/Source/Client/Patches/MapSetup.cs +++ b/Source/Client/Patches/MapSetup.cs @@ -26,7 +26,7 @@ public static void SetupMap(Map map, bool usingMapTimeFromSingleplayer = false) { Log.Message("MP: Setting up map " + map.uniqueID); - // Initialize and store Multiplayer + // Initialize and store Multiplayer var mapComp = new MultiplayerMapComp(map); Multiplayer.game.mapComps.Add(mapComp); @@ -40,7 +40,7 @@ public static void SetupMap(Map map, bool usingMapTimeFromSingleplayer = false) // Add all other (non Faction.OfPlayer) factions to the map foreach (var faction in Find.FactionManager.AllFactions.Where(f => f.IsPlayer)) if (faction != Faction.OfPlayer) - InitNewFactionData(map, faction); + InitNewFactionData(map, faction); } private static AsyncTimeComp CreateAsyncTimeCompForMap(Map map, bool usingMapTimeFromSingleplayer) @@ -79,10 +79,7 @@ private static AsyncTimeComp CreateAsyncTimeCompForMap(Map map, bool usingMapTim asyncTimeCompForMap = new AsyncTimeComp(map, gameStartAbsTick); asyncTimeCompForMap.mapTicks = startingMapTicks; - asyncTimeCompForMap.SetDesiredTimeSpeed(startingTimeSpeed); - - asyncTimeCompForMap.storyteller = new Storyteller(Find.Storyteller.def, Find.Storyteller.difficultyDef, Find.Storyteller.difficulty); - asyncTimeCompForMap.storyWatcher = new StoryWatcher(); + asyncTimeCompForMap.DesiredTimeSpeed = startingTimeSpeed; SetupNextMapFromTickZero = false; diff --git a/Source/Client/Syncing/Game/SyncFields.cs b/Source/Client/Syncing/Game/SyncFields.cs index 30b258db..8a3f58ba 100644 --- a/Source/Client/Syncing/Game/SyncFields.cs +++ b/Source/Client/Syncing/Game/SyncFields.cs @@ -1,12 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; using HarmonyLib; using Multiplayer.API; using Multiplayer.Client.Persistent; using RimWorld; using RimWorld.Planet; -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; using UnityEngine; using Verse; using static Verse.Widgets; @@ -252,24 +252,14 @@ static void ChangeStoryteller() static void StorytellerDef_Post(object target, object value) { Find.Storyteller.Notify_DefChanged(); - - foreach (var comp in Multiplayer.game.asyncTimeComps) - { - comp.storyteller.def = Find.Storyteller.def; - comp.storyteller.Notify_DefChanged(); - } } static void StorytellerDifficultyDef_Post(object target, object value) { - foreach (var comp in Multiplayer.game.asyncTimeComps) - comp.storyteller.difficultyDef = Find.Storyteller.difficultyDef; } static void StorytellerDifficulty_Post(object target, object value) { - foreach (var comp in Multiplayer.game.asyncTimeComps) - comp.storyteller.difficulty = Find.Storyteller.difficulty; } [MpPrefix(typeof(HealthCardUtility), nameof(HealthCardUtility.DrawOverviewTab))] From 08af4e735860149761cf93704a772b872a121e11 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:47:55 +0200 Subject: [PATCH 2/3] Convert SetDesiredTimeSpeed to property setter --- Source/Client/AsyncTime/AsyncTimeComp.cs | 9 ++++---- Source/Client/AsyncTime/AsyncTimePatches.cs | 8 +++---- Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 23 +++++++++---------- Source/Client/AsyncTime/ITickable.cs | 6 ++--- Source/Client/MultiplayerStatic.cs | 2 +- Source/Client/Networking/HostUtil.cs | 10 ++++---- 6 files changed, 27 insertions(+), 31 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index a927a8f3..52ecb477 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -50,11 +50,10 @@ public float TickRateMultiplier(TimeSpeed speed) } } - public TimeSpeed DesiredTimeSpeed => timeSpeedInt; - - public void SetDesiredTimeSpeed(TimeSpeed speed) + public TimeSpeed DesiredTimeSpeed { - timeSpeedInt = speed; + get => timeSpeedInt; + set => timeSpeedInt = value; } public bool Paused => this.ActualRateMultiplier(DesiredTimeSpeed) == 0f; @@ -269,7 +268,7 @@ public void ExecuteCmd(ScheduledCommand cmd) if (cmdType == CommandType.MapTimeSpeed && Multiplayer.GameComp.asyncTime) { TimeSpeed speed = (TimeSpeed)data.ReadByte(); - SetDesiredTimeSpeed(speed); + DesiredTimeSpeed = speed; MpLog.Debug("Set map time speed " + speed); } diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index 543a8f9e..10a8bb05 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -1,10 +1,10 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using HarmonyLib; using Multiplayer.Client.Factions; using RimWorld; using RimWorld.Planet; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using Verse; namespace Multiplayer.Client.AsyncTime @@ -205,7 +205,7 @@ private static void PauseOnLetter(TickManager manager) if (Multiplayer.GameComp.asyncTime) { var tickable = (ITickable)Multiplayer.MapContext.AsyncTime() ?? Multiplayer.AsyncWorldTime; - tickable.SetDesiredTimeSpeed(TimeSpeed.Paused); + tickable.DesiredTimeSpeed = TimeSpeed.Paused; Multiplayer.GameComp.ResetAllTimeVotes(tickable.TickableId); } else diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index 4be6aaa2..27843264 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -44,15 +44,14 @@ public float TickRateMultiplier(TimeSpeed speed) } // Run at the speed of the fastest map or at chosen speed if there are no maps - public TimeSpeed DesiredTimeSpeed => !Find.Maps.Any() ? - timeSpeedInt : - Find.Maps.Select(m => m.AsyncTime()) - .Where(a => a.ActualRateMultiplier(a.DesiredTimeSpeed) != 0f) - .Max(a => a?.DesiredTimeSpeed) ?? TimeSpeed.Paused; - - public void SetDesiredTimeSpeed(TimeSpeed speed) + public TimeSpeed DesiredTimeSpeed { - timeSpeedInt = speed; + get => !Find.Maps.Any() + ? timeSpeedInt + : Find.Maps.Select(m => m.AsyncTime()) + .Where(a => a.ActualRateMultiplier(a.DesiredTimeSpeed) != 0f) + .Max(a => a?.DesiredTimeSpeed) ?? TimeSpeed.Paused; + set => timeSpeedInt = value; } public Queue Cmds => cmds; @@ -274,8 +273,8 @@ private static void CreateJoinPointAndSendIfHost() public void SetTimeEverywhere(TimeSpeed speed) { foreach (var map in Find.Maps) - map.AsyncTime().SetDesiredTimeSpeed(speed); - SetDesiredTimeSpeed(speed); + map.AsyncTime().DesiredTimeSpeed = speed; + DesiredTimeSpeed = speed; } public static float lastSpeedChange; @@ -283,7 +282,7 @@ public void SetTimeEverywhere(TimeSpeed speed) private void HandleTimeSpeed(ScheduledCommand cmd, ByteReader data) { TimeSpeed speed = (TimeSpeed)data.ReadByte(); - SetDesiredTimeSpeed(speed); + DesiredTimeSpeed = speed; if (!Multiplayer.GameComp.asyncTime) { @@ -311,7 +310,7 @@ private void HandleTimeVote(ScheduledCommand cmd, ByteReader data) if (!Multiplayer.GameComp.asyncTime || vote == TimeVote.ResetGlobal) SetTimeEverywhere(Multiplayer.GameComp.GetLowestTimeVote(TickableId)); else if (TickPatch.TickableById(tickableId) is { } tickable) - tickable.SetDesiredTimeSpeed(Multiplayer.GameComp.GetLowestTimeVote(tickableId)); + tickable.DesiredTimeSpeed = Multiplayer.GameComp.GetLowestTimeVote(tickableId); } public void FinalizeInit() diff --git a/Source/Client/AsyncTime/ITickable.cs b/Source/Client/AsyncTime/ITickable.cs index 3b9dd6f4..31a19ce9 100644 --- a/Source/Client/AsyncTime/ITickable.cs +++ b/Source/Client/AsyncTime/ITickable.cs @@ -1,5 +1,5 @@ -using Multiplayer.Common; using System.Collections.Generic; +using Multiplayer.Common; using Verse; namespace Multiplayer.Client @@ -12,9 +12,7 @@ public interface ITickable float TimeToTickThrough { get; set; } - TimeSpeed DesiredTimeSpeed { get; } - - void SetDesiredTimeSpeed(TimeSpeed speed); + TimeSpeed DesiredTimeSpeed { get; set; } float TickRateMultiplier(TimeSpeed speed); diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 07b1211f..275bf1c3 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -218,7 +218,7 @@ void LoadNextReplay() Replay.LoadReplay(Replay.SavedReplayFile(current[1]), true, () => { - TickPatch.AllTickables.Do(t => t.SetDesiredTimeSpeed(TimeSpeed.Normal)); + TickPatch.AllTickables.Do(t => t.DesiredTimeSpeed = TimeSpeed.Normal); void TickBatch() { diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 10f305c7..bf3ad760 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -1,6 +1,3 @@ -using Multiplayer.Client.Networking; -using Multiplayer.Common; -using RimWorld; using System; using System.ComponentModel; using System.Diagnostics; @@ -9,7 +6,10 @@ using System.Threading.Tasks; using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; +using Multiplayer.Client.Networking; using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; using UnityEngine; using Verse; @@ -94,9 +94,9 @@ private static void PrepareGame() private static void SetGameState(ServerSettings settings) { - Multiplayer.AsyncWorldTime.SetDesiredTimeSpeed(TimeSpeed.Paused); + Multiplayer.AsyncWorldTime.DesiredTimeSpeed = TimeSpeed.Paused; foreach (var map in Find.Maps) - map.AsyncTime().SetDesiredTimeSpeed(TimeSpeed.Paused); + map.AsyncTime().DesiredTimeSpeed = TimeSpeed.Paused; Find.TickManager.CurTimeSpeed = TimeSpeed.Paused; From 371779b8fe7037096da3f4dfa9a3bcc1f694b837 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Wed, 17 Dec 2025 04:02:11 +0100 Subject: [PATCH 3/3] Fix areas not working correctly --- Source/Client/AsyncTime/AsyncTimeComp.cs | 6 +++--- Source/Client/Factions/FactionExtensions.cs | 7 ++----- Source/Client/Patches/MapSetup.cs | 18 +++++++----------- Source/Client/Patches/TickPatch.cs | 21 ++++++++------------- 4 files changed, 20 insertions(+), 32 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 52ecb477..3046b032 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -173,9 +173,9 @@ public void UpdateManagers() public void PreContext() { map.PushFaction( - map.ParentFaction is { IsPlayer: true } - ? map.ParentFaction - : Multiplayer.WorldComp.spectatorFaction, + !Multiplayer.GameComp.multifaction || map.ParentFaction is { IsPlayer: true } + ? map.ParentFaction + : Multiplayer.WorldComp.spectatorFaction, force: true); prevTime = TimeSnapshot.GetAndSetFromMap(map); diff --git a/Source/Client/Factions/FactionExtensions.cs b/Source/Client/Factions/FactionExtensions.cs index e2d7332a..a520c8cf 100644 --- a/Source/Client/Factions/FactionExtensions.cs +++ b/Source/Client/Factions/FactionExtensions.cs @@ -1,6 +1,6 @@ +using System.Linq; using RimWorld; using RimWorld.Planet; -using System.Linq; using Verse; namespace Multiplayer.Client.Factions; @@ -25,10 +25,7 @@ public static void PushFaction(this Map map, int factionId) map.PushFaction(faction); } - public static Faction PopFaction() - { - return PopFaction(null); - } + public static Faction PopFaction() => PopFaction(null); public static Faction PopFaction(this Map map) { diff --git a/Source/Client/Patches/MapSetup.cs b/Source/Client/Patches/MapSetup.cs index f762a3a6..f83e0f24 100644 --- a/Source/Client/Patches/MapSetup.cs +++ b/Source/Client/Patches/MapSetup.cs @@ -9,17 +9,12 @@ namespace Multiplayer.Client; [HarmonyPatch(typeof(MapGenerator), nameof(MapGenerator.GenerateMap))] public static class MapSetup { - public static bool SetupNextMapFromTickZero = false; + public static bool SetupNextMapFromTickZero; static void Prefix(ref Action extraInitBeforeContentGen) { if (Multiplayer.Client == null) return; - extraInitBeforeContentGen += SetupMap; - } - - public static void SetupMap(Map map) - { - SetupMap(map, false); + extraInitBeforeContentGen += map => SetupMap(map); } public static void SetupMap(Map map, bool usingMapTimeFromSingleplayer = false) @@ -48,7 +43,6 @@ private static AsyncTimeComp CreateAsyncTimeCompForMap(Map map, bool usingMapTim int startingMapTicks; int gameStartAbsTick; TimeSpeed startingTimeSpeed; - AsyncTimeComp asyncTimeCompForMap; bool startingMapTimeFromBeginning = Multiplayer.GameComp.multifaction && @@ -77,9 +71,11 @@ private static AsyncTimeComp CreateAsyncTimeCompForMap(Map map, bool usingMapTim if (!Multiplayer.GameComp.asyncTime) startingTimeSpeed = Find.TickManager.CurTimeSpeed; - asyncTimeCompForMap = new AsyncTimeComp(map, gameStartAbsTick); - asyncTimeCompForMap.mapTicks = startingMapTicks; - asyncTimeCompForMap.DesiredTimeSpeed = startingTimeSpeed; + var asyncTimeCompForMap = new AsyncTimeComp(map, gameStartAbsTick) + { + mapTicks = startingMapTicks, + DesiredTimeSpeed = startingTimeSpeed + }; SetupNextMapFromTickZero = false; diff --git a/Source/Client/Patches/TickPatch.cs b/Source/Client/Patches/TickPatch.cs index 9ce615c8..bf6a1608 100644 --- a/Source/Client/Patches/TickPatch.cs +++ b/Source/Client/Patches/TickPatch.cs @@ -175,7 +175,11 @@ private static bool RunCmds() ScheduledCommand cmd = tickable.Cmds.Dequeue(); // Minimal code impact fix for #733. Having all the commands be added to a single queue gets rid of // the out-of-order execution problem. With a proper fix, this can be reverted to tickable.ExecuteCmd - TickableById(cmd.mapId).ExecuteCmd(cmd); + var target = TickableById(cmd.mapId); + if (target == null) + { + Log.Error($"!!! Tickable of {cmd.mapId} not found! {cmd}"); + } else target.ExecuteCmd(cmd); if (LongEventHandler.eventQueue.Count > 0) return true; // Yield to e.g. join-point creation } @@ -290,10 +294,7 @@ public static float ActualRateMultiplier(this ITickable tickable, TimeSpeed spee return rate; } - public static void ClearSimulating() - { - simulating = null; - } + public static void ClearSimulating() => simulating = null; public static void Reset() { @@ -309,15 +310,9 @@ public static void Reset() TimeControlPatch.prePauseTimeSpeed = null; } - public static void SetTimer(int value) - { - Timer = value; - } + public static void SetTimer(int value) => Timer = value; - public static ITickable TickableById(int tickableId) - { - return AllTickables.FirstOrDefault(t => t.TickableId == tickableId); - } + public static ITickable TickableById(int tickableId) => AllTickables.FirstOrDefault(t => t.TickableId == tickableId); } public class SimulatingData