Skip to content

Commit 2c703fe

Browse files
Make RNG in multiplayer more random, fix small RNG bug
`AsyncWorldTimeComp` uses the world's ConstantRandomSeed rather than the constant value of 2. Using the world's ConstantRandomSeed should ensure a unique seed for each save (even if using the same seed). `AsyncTimeComp` is no longer using the constant value of 1. It now uses the map's unique ID to ensure each map has a unique seed, combined with the world's ConstantRandomSeed to ensure a unique seed for each save file as well. Several of the seeded methods in `Seeds.cs` now also use world's ConstantRandomSeed to make their RNG feel more unique. Some of those could use `Find.TickManager.TicksGame`, either together with or instead of world's ConstantRandomSeed - which would ensure a different outcome based on the passage of time in-game. However, this depends on whether of not we want this. I've also updated the patch to `PawnRenderer.SetAllGraphicsDirty` to instead target the lambda the method is calling. Given that the lambda is called in a `LongEventHandler.ExecuteWhenFinished` block, the actual call to the lambda may end up being delayed and us seeding the method would do nothing. On top of that, given that pawn rendering is now using multithreading, if this method was called from a different thread - it could potentially cause some issues, since (I believe) RimWorld's RNG is not thread safe. I am unsure if this could happen, but it's better to be safe here. Sadly, I haven't applied those changes to every single possible method where we're seeding RNG. `SeedGameLoad` is not really possible to safely seed without further changes. Since that this method is called when there's nothing loaded into the game we could use as our seed, the host would have to generate the seed themselves and share it with all the players when they are joining. Given the extra work, I've opted for leaving this as-is for now. I've also left `RandPatches`, given that those weren't seeded in the first place.
1 parent f0221d0 commit 2c703fe

File tree

3 files changed

+27
-11
lines changed

3 files changed

+27
-11
lines changed

Source/Client/AsyncTime/AsyncTimeComp.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,17 @@ public void SetDesiredTimeSpeed(TimeSpeed speed)
8080
public TickList tickListLong = new(TickerType.Long);
8181

8282
// Shared random state for ticking and commands
83-
public ulong randState = 1;
83+
public ulong randState;
8484

8585
public Queue<ScheduledCommand> cmds = new();
8686

8787
public AsyncTimeComp(Map map)
8888
{
8989
this.map = map;
90+
91+
// Use the world's constant rand seed and map tile ID as our initial randState.
92+
// Only fill the seed part, leave the iterations out.
93+
randState = (uint)Gen.HashCombineInt(map.uniqueID, Find.World.ConstantRandSeed);
9094
}
9195

9296
public void Tick()

Source/Client/AsyncTime/AsyncWorldTimeComp.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,17 @@ public void SetDesiredTimeSpeed(TimeSpeed speed)
6262
public int TickableId => -1;
6363

6464
public World world;
65-
public ulong randState = 2;
65+
public ulong randState;
6666

6767
public int worldTicks;
6868

6969
public AsyncWorldTimeComp(World world)
7070
{
7171
this.world = world;
72+
73+
// Use the world's constant rand seed as our initial randState.
74+
// Only fill the seed part, leave the iterations out.
75+
randState = (uint)world.ConstantRandSeed;
7276
}
7377

7478
public void ExposeData()

Source/Client/Patches/Seeds.cs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Reflection;
77
using System.Reflection.Emit;
8+
using Multiplayer.Client.Util;
89
using Verse;
910
using Verse.Grammar;
1011

@@ -37,7 +38,7 @@ static void Prefix(Map __instance, ref bool __state)
3738
if (Multiplayer.Client == null) return;
3839
if (Scribe.mode != LoadSaveMode.LoadingVars && Scribe.mode != LoadSaveMode.ResolvingCrossRefs && Scribe.mode != LoadSaveMode.PostLoadInit) return;
3940

40-
int seed = __instance.uniqueID;
41+
int seed = Gen.HashCombineInt(__instance.uniqueID, Find.World.ConstantRandSeed);
4142
Rand.PushState(seed);
4243

4344
__state = true;
@@ -58,7 +59,7 @@ static void Prefix(Map __instance, ref bool __state)
5859
{
5960
if (Multiplayer.Client == null) return;
6061

61-
int seed = __instance.uniqueID;
62+
int seed = Gen.HashCombineInt(__instance.uniqueID, Find.World.ConstantRandSeed);
6263
Rand.PushState(seed);
6364

6465
__state = true;
@@ -71,14 +72,14 @@ static void Postfix(Map __instance, bool __state)
7172
}
7273
}
7374

74-
[HarmonyPatch(typeof(CaravanEnterMapUtility), nameof(CaravanEnterMapUtility.Enter), new[] { typeof(Caravan), typeof(Map), typeof(Func<Pawn, IntVec3>), typeof(CaravanDropInventoryMode), typeof(bool) })]
75+
[HarmonyPatch(typeof(CaravanEnterMapUtility), nameof(CaravanEnterMapUtility.Enter), typeof(Caravan), typeof(Map), typeof(Func<Pawn, IntVec3>), typeof(CaravanDropInventoryMode), typeof(bool))]
7576
static class SeedCaravanEnter
7677
{
7778
static void Prefix(Map map, ref bool __state)
7879
{
7980
if (Multiplayer.Client == null) return;
8081

81-
int seed = map.uniqueID;
82+
int seed = Gen.HashCombineInt(map.uniqueID, Find.World.ConstantRandSeed);
8283
Rand.PushState(seed);
8384

8485
__state = true;
@@ -105,7 +106,7 @@ static void Prefix(ref Action action)
105106
}
106107

107108
// Seed the rotation random
108-
[HarmonyPatch(typeof(GenSpawn), nameof(GenSpawn.Spawn), new[] { typeof(Thing), typeof(IntVec3), typeof(Map), typeof(Rot4), typeof(WipeMode), typeof(bool), typeof(bool) })]
109+
[HarmonyPatch(typeof(GenSpawn), nameof(GenSpawn.Spawn), typeof(Thing), typeof(IntVec3), typeof(Map), typeof(Rot4), typeof(WipeMode), typeof(bool), typeof(bool))]
109110
static class GenSpawnRotatePatch
110111
{
111112
static MethodInfo Rot4GetRandom = AccessTools.Property(typeof(Rot4), nameof(Rot4.Random)).GetGetMethod();
@@ -116,9 +117,16 @@ static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> inst
116117
{
117118
if (inst.operand == Rot4GetRandom)
118119
{
120+
// Load newThing.thingIdNumber to the stack
119121
yield return new CodeInstruction(OpCodes.Ldarg_0);
120122
yield return new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(Thing), nameof(Thing.thingIDNumber)));
121-
yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Rand), nameof(Rand.PushState), new[] { typeof(int) }));
123+
// Load Find.World.ConstantRandSeed to the stack
124+
yield return new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(Find), nameof(Find.World)));
125+
yield return new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(World), nameof(World.ConstantRandSeed)));
126+
// Pop our 2 values, call Gen.HashCombineInt with them, and push the outcome to the stack
127+
yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Gen), nameof(Gen.HashCombineInt), [typeof(int), typeof(int)]));
128+
// Pop the value off the stack and call Rand.PushState with it as the argument
129+
yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Rand), nameof(Rand.PushState), [typeof(int)]));
122130
}
123131

124132
yield return inst;
@@ -153,7 +161,7 @@ static IEnumerable<MethodBase> TargetMethods()
153161
{
154162
yield return AccessTools.Method(typeof(GrammarResolver), nameof(GrammarResolver.Resolve));
155163
yield return AccessTools.Method(typeof(PawnBioAndNameGenerator), nameof(PawnBioAndNameGenerator.GeneratePawnName));
156-
yield return AccessTools.Method(typeof(NameGenerator), nameof(NameGenerator.GenerateName), new[] { typeof(RulePackDef), typeof(Predicate<string>), typeof(bool), typeof(string), typeof(string), typeof(List<Rule>) });
164+
yield return AccessTools.Method(typeof(NameGenerator), nameof(NameGenerator.GenerateName), [typeof(RulePackDef), typeof(Predicate<string>), typeof(bool), typeof(string), typeof(string), typeof(List<Rule>)]);
157165
}
158166

159167
[HarmonyPriority(MpPriority.MpFirst)]
@@ -177,13 +185,13 @@ static class SeedPawnGraphics
177185
{
178186
static IEnumerable<MethodBase> TargetMethods()
179187
{
180-
yield return AccessTools.Method(typeof(PawnRenderer), nameof(PawnRenderer.SetAllGraphicsDirty));
188+
yield return MpMethodUtil.GetLambda(typeof(PawnRenderer), nameof(PawnRenderer.SetAllGraphicsDirty), 0);
181189
}
182190

183191
[HarmonyPriority(MpPriority.MpFirst)]
184192
static void Prefix(PawnRenderer __instance, ref bool __state)
185193
{
186-
Rand.PushState(__instance.pawn.thingIDNumber);
194+
Rand.PushState(Gen.HashCombineInt(__instance.pawn.thingIDNumber, Find.World.ConstantRandSeed));
187195
__state = true;
188196
}
189197

0 commit comments

Comments
 (0)