Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Source/Client/Desyncs/ClientSyncOpinion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ join map2 in other.mapStates on map1.mapId equals map2.mapId
if (!commandRandomStates.SequenceEqual(other.commandRandomStates))
return "Random state from commands doesn't match";

if (!simulating && !other.simulating && desyncStackTraceHashes.Any() && other.desyncStackTraceHashes.Any() && !desyncStackTraceHashes.SequenceEqual(other.desyncStackTraceHashes))
if (Native.IsMonoRuntime && !simulating && !other.simulating && desyncStackTraceHashes.Any() && other.desyncStackTraceHashes.Any() && !desyncStackTraceHashes.SequenceEqual(other.desyncStackTraceHashes))
return "Trace hashes don't match";

return null;
Expand Down
25 changes: 24 additions & 1 deletion Source/Client/Desyncs/DeferredStackTracing.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Reflection;
using System.Runtime.InteropServices;
using HarmonyLib;
Expand Down Expand Up @@ -31,10 +33,31 @@ static IEnumerable<MethodBase> TargetMethods()

public static void Postfix()
{
if (Native.LmfPtr == 0) return;
if (Native.IsMonoRuntime && Native.LmfPtr == 0) return;
if (!ShouldAddStackTraceForDesyncLog()) return;

var logItem = StackTraceLogItemRaw.GetFromPool();
if (!Native.IsMonoRuntime)
{
// Managed fallback: never interpret CoreCLR addresses as Mono frames.
var frames = new StackTrace(1, false).GetFrames();
var text = new StringBuilder();
int managedHash = 17;
int managedDepth = System.Math.Min(frames?.Length ?? 0, StackTraceLogItemRaw.MaxDepth);
for (int i = 0; i < managedDepth; i++)
{
var method = frames[i].GetMethod();
var name = $"{method?.DeclaringType?.FullName}.{method}";
text.AppendLine(name);
// Explicit stable hashing; no process-randomized string hashes.
foreach (char c in name)
managedHash = unchecked(managedHash * 31 + c);
}
logItem.managedTrace = text.ToString();
Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, managedDepth, managedHash);
acc++;
return;
}
var trace = logItem.raw;
int hash = 0;
// Skip this (DeferredStackTracing.Postfix) frame. Keeping it in the trace doesn't provide any value.
Expand Down
6 changes: 5 additions & 1 deletion Source/Client/Desyncs/JittedMethods.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand Down Expand Up @@ -38,6 +38,7 @@ public static class JittedMethods

public static void Init()
{
if (!Native.IsMonoRuntime) return;
profiler = Native.mono_profiler_create(IntPtr.Zero);
Native.mono_profiler_set_jit_done_callback(profiler, (_, method, _) =>
{
Expand Down Expand Up @@ -73,11 +74,14 @@ public static void Init()

public static void OnApplicationQuit()
{
if (!Native.IsMonoRuntime || profiler == IntPtr.Zero) return;
Native.mono_profiler_set_jit_done_callback(profiler, null);
}

public static string GetJittedMethodsString()
{
if (!Native.IsMonoRuntime)
return "Mono JIT history is unavailable on this runtime.";
// Two ToArrays to prevent a compilation in between from causing a "Collection was modified" exception
var jittedMethods = methodQueue.ToArray().Select((j, i) => (j, i)).ToArray();

Expand Down
4 changes: 3 additions & 1 deletion Source/Client/Desyncs/SaveableDesyncInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
Expand Down Expand Up @@ -128,7 +128,9 @@ private string GetDesyncDetails()
.AppendLine($"Processor Speed (MHz)|||{SystemInfo.processorFrequency}")
.AppendLine($"Thread Count|||{SystemInfo.processorCount}")
.AppendLine("\n###GPU Info###")
#if !RIMWORLD_1_7
.AppendLine($"GPU Family|||{SystemInfo.graphicsDeviceVendor}")
#endif
.AppendLine($"GPU Type|||{SystemInfo.graphicsDeviceType}")
.AppendLine($"GPU Name|||{SystemInfo.graphicsDeviceName}")
.AppendLine($"GPU VRAM|||{SystemInfo.graphicsMemorySize}")
Expand Down
3 changes: 3 additions & 0 deletions Source/Client/Desyncs/StackTraceLogItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class StackTraceLogItemRaw : StackTraceLogItem
public const int MaxDepth = 32;
public long[] raw = new long[MaxDepth];
public int depth;
public string managedTrace;

public int ticksGame;
public ulong rngState;
Expand All @@ -56,6 +57,7 @@ public override string StackTraceString
{
get
{
if (managedTrace != null) return managedTrace;
var builder = new StringBuilder();
for (int i = 0; i < depth; i++)
{
Expand All @@ -78,6 +80,7 @@ public static StackTraceLogItemRaw GetFromPool()

public override void Dispose()
{
managedTrace = null;
depth = 0;
ticksGame = 0;
rngState = 0;
Expand Down
2 changes: 1 addition & 1 deletion Source/Client/Factions/MultifactionPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting.Messaging;

using UnityEngine;
using Verse;
using Verse.AI;
Expand Down
1 change: 1 addition & 0 deletions Source/Client/Multiplayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ public static void InitMultiplayer(ModContentPack content)
[MethodImpl(MethodImplOptions.NoInlining)]
private static void DisableOmitFramePointer()
{
if (!Native.IsMonoRuntime) return;
Native.mini_parse_debug_option("disable_omit_fp");
}

Expand Down
5 changes: 3 additions & 2 deletions Source/Client/Multiplayer.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<TargetFramework Condition="'$(RimWorld17)' != 'true'">net48</TargetFramework>
<TargetFramework Condition="'$(RimWorld17)' == 'true'">net10.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>12</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
Expand Down
2 changes: 1 addition & 1 deletion Source/Client/Patches/GravshipTravelSessionPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ static MethodBase TargetMethod()
return MpMethodUtil.GetLambda(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation), lambdaOrdinal: 4);
}

static void Postfix(Dialog_MessageBox __instance)
static void Postfix()
{
if (Multiplayer.Client == null) return;
if (!Multiplayer.ExecutingCmds) return;
Expand Down
2 changes: 2 additions & 0 deletions Source/Client/Patches/VanillaTweaks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ static void Prefix()
{
if (done) return;
GUI.skin.font = Text.fontStyles[1].font;
#if !RIMWORLD_1_7
Text.fontStyles[1].font.fontNames = new[] { "arial", "arialbd", "ariali", "arialbi" };
#endif
done = true;
}
}
Expand Down
32 changes: 6 additions & 26 deletions Source/Client/Syncing/SyncTemplates.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -39,19 +39,7 @@ static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<
if (pInfo.IsOut) {
pType = pType.GetElementType();
yield return new CodeInstruction(OpCodes.Ldarg, argIndex);
yield return CreateDefaultCodes(gen, pType).Last();
if (AccessTools.IsClass(pType))
yield return new CodeInstruction(OpCodes.Stind_Ref);
if (AccessTools.IsValue(pType)) {
if (pType == typeof(float))
yield return new CodeInstruction(OpCodes.Stind_R4, (float) 0);
else if (pType == typeof(double))
yield return new CodeInstruction(OpCodes.Stind_R8, (double) 0);
else if (pType == typeof(long))
yield return new CodeInstruction(OpCodes.Stind_I8, (long) 0);
else
yield return new CodeInstruction(OpCodes.Stind_I4, 0);
}
yield return new CodeInstruction(OpCodes.Initobj, pType);
}
}

Expand Down Expand Up @@ -94,29 +82,21 @@ static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<

static IEnumerable<CodeInstruction> CreateDefaultCodes(ILGenerator generator, Type type)
{
if (type == typeof(void)) yield break;
if (type.IsByRef)
type = type.GetElementType();

if (AccessTools.IsClass(type)) {
if (!type.IsValueType) {
yield return new CodeInstruction(OpCodes.Ldnull);
yield break;
}
if (AccessTools.IsStruct(type)) {
if (type.IsValueType) {
var v = generator.DeclareLocal(type);
yield return new CodeInstruction(OpCodes.Ldloca, v);
yield return new CodeInstruction(OpCodes.Initobj, type);
yield return new CodeInstruction(OpCodes.Ldloc, v);
yield break;
}
if (AccessTools.IsValue(type)) {
if (type == typeof(float))
yield return new CodeInstruction(OpCodes.Ldc_R4, (float) 0);
else if (type == typeof(double))
yield return new CodeInstruction(OpCodes.Ldc_R8, (double) 0);
else if (type == typeof(long))
yield return new CodeInstruction(OpCodes.Ldc_I8, (long) 0);
else
yield return new CodeInstruction(OpCodes.Ldc_I4, 0);
}
}

}
Expand Down
5 changes: 3 additions & 2 deletions Source/Client/UI/MainMenuAnimation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
Expand All @@ -24,7 +24,8 @@ static class MainMenuAnimation

// The background might be drawn at the same time as the loading of a map on a different thread calls Verse.Rand
// Verse.Rand can't be used here because it isn't thread-safe
private static Random rand = new(SystemInfo.deviceUniqueIdentifier.GetHashCode());
// Cosmetic only: no dependency on Unity's hardware identifier.
private static Random rand = new();

private static float newPulseTimer;

Expand Down
2 changes: 2 additions & 0 deletions Source/Client/Util/MpUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,10 @@ public static void DoPasswordField(Rect rect, string controlName, ref string pas
{
te.scrollOffset = scrollOffset;
te.text = new string(PassChar, password.Length);
#if !RIMWORLD_1_7
AccessTools.Field(typeof(TextEditor), "m_RevealCursor").SetValue(te, true);
te.UpdateScrollOffsetIfNeeded(new Event());
#endif
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions Source/Client/Util/MpUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public static class MpUtil
{
public static unsafe void MarkNoInlining(MethodBase method)
{
// This offset is a MonoMethod layout, not a CoreCLR MethodDesc.
// Harmony handles detouring on CoreCLR; writing here corrupts memory.
if (!Native.IsMonoRuntime) return;
ushort* iflags = (ushort*)(method.MethodHandle.Value) + 1;
*iflags |= (ushort)MethodImplOptions.NoInlining;
}
Expand Down
5 changes: 3 additions & 2 deletions Source/Common/Common.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<TargetFramework Condition="'$(RimWorld17)' != 'true'">net48</TargetFramework>
<TargetFramework Condition="'$(RimWorld17)' == 'true'">net10.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
Expand Down
6 changes: 6 additions & 0 deletions Source/Common/Native.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ namespace Multiplayer.Client
{
public static class Native
{
// Waylith uses CoreCLR. Never load Mono libraries or dereference Mono
// thread structures on that runtime, even when running old mod assemblies.
public static bool IsMonoRuntime => Type.GetType("Mono.Runtime") != null;

public enum NativeOS
{
Windows, OSX, Linux, Dummy
Expand All @@ -24,6 +28,7 @@ public enum NativeOS

public static void EarlyInit(NativeOS os)
{
if (!IsMonoRuntime) return;
if (os == NativeOS.Linux)
TheLinuxWay();
if (os == NativeOS.OSX)
Expand All @@ -46,6 +51,7 @@ private static void EarlyInitInternal()

public static unsafe void InitLmfPtr(NativeOS os)
{
if (!IsMonoRuntime) return;
// Don't bother on 32 bit runtimes
if (IntPtr.Size == 4)
return;
Expand Down
26 changes: 26 additions & 0 deletions Source/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project>
<!-- Opt in explicitly; the upstream 1.6 build remains available. -->
<PropertyGroup Condition="'$(RimWorld17)' == 'true' and ('$(MSBuildProjectName)' == 'Multiplayer' or '$(MSBuildProjectName)' == 'MultiplayerLoader' or '$(MSBuildProjectName)' == 'Common')">
<TargetFramework>net10.0</TargetFramework>
<DefineConstants>$(DefineConstants);RIMWORLD_1_7</DefineConstants>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup Condition="'$(RimWorld17)' == 'true' and ('$(MSBuildProjectName)' == 'Multiplayer' or '$(MSBuildProjectName)' == 'MultiplayerLoader' or '$(MSBuildProjectName)' == 'Common')">
<Using Include="HarmonyLib.MethodInvoker" Alias="MethodInvoker" />
<PackageReference Remove="Krafs.Rimworld.Ref" />
<!-- MultiplayerAPI also depends on the old reference package transitively. -->
<PackageReference Include="Krafs.Rimworld.Ref" Version="1.6.4850" ExcludeAssets="all" PrivateAssets="all" />
<Reference Include="$(RimWorldDir)/Assembly-CSharp.dll;$(RimWorldDir)/UnityStubs.dll;$(RimWorldDir)/Waylith.UnityCompat.dll;$(RimWorldDir)/Waylith.dll;$(RimWorldDir)/com.rlabrecque.steamworks.net.dll">

@romangr romangr Oct 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we avoid having references to local installation of the game? It makes it impossible to build a CI for it

<Private>false</Private>
</Reference>
<Publicize Remove="UnityEngine.IMGUIModule" />
<Publicize Include="Waylith.UnityCompat;UnityStubs" />
</ItemGroup>
<Target Name="ValidateRimWorld17References" BeforeTargets="ResolveReferences" Condition="'$(RimWorld17)' == 'true'">
<Error Condition="!Exists('$(RimWorldDir)/Assembly-CSharp.dll')" Text="Set RimWorldDir to a RimWorld 1.7 installation containing Assembly-CSharp.dll." />
</Target>
<!-- The old projects clear framework paths for the Mono reference package. -->
<Target Name="RemoveFramework" BeforeTargets="PrepareForBuild" DependsOnTargets="GetReferenceAssemblyPaths" Condition="'$(RimWorld17)' != 'true'">
<PropertyGroup><TargetFrameworkDirectory /></PropertyGroup>
</Target>
</Project>
5 changes: 3 additions & 2 deletions Source/MultiplayerLoader/MultiplayerLoader.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<TargetFramework Condition="'$(RimWorld17)' != 'true'">net48</TargetFramework>
<TargetFramework Condition="'$(RimWorld17)' == 'true'">net10.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>12</LangVersion>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
Expand Down
4 changes: 3 additions & 1 deletion Source/MultiplayerLoader/Prepatches.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -29,7 +29,9 @@ static void DontSetResolution(ModuleDefinition module)
[FreePatch]
static void ApplySyncPrepatches(ModuleDefinition module)
{
#if !RIMWORLD_1_7
SyncRituals.ApplyPrepatches(module);
#endif
}

internal static void AddPrepatcherPrefix(ModuleDefinition module, int id, MethodInfo target, MethodInfo prefix)
Expand Down
20 changes: 19 additions & 1 deletion Source/MultiplayerLoader/SyncRituals.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand All @@ -13,6 +13,17 @@ namespace MultiplayerLoader;
public static class SyncRituals
{
private static List<ISyncMethodForPrepatcher> syncMethodsWithPrepatches = new();
#if RIMWORLD_1_7
private static readonly Dictionary<(Type, string), ISyncMethodForPrepatcher> runtimeSyncMethods = new();
private static readonly HashSet<MethodBase> runtimePatchedMethods = new();
private static readonly Harmony runtimeHarmony = new("rwmt.Multiplayer.WaylithRituals");

public static bool RuntimePrefix(object __instance, object[] __args, MethodBase __originalMethod)
{
return !runtimeSyncMethods.TryGetValue((__instance.GetType(), __originalMethod.Name), out var sync)
|| !sync.DoSync(__instance, __args);
}
#endif

public static bool PrepatcherPrefix(int index, object __instance, object[] __args)
{
Expand Down Expand Up @@ -50,6 +61,13 @@ void Register(Type baseType, string method, Type derivedType, Action<ISyncMethod
var newSyncMethod = (ISyncMethodForPrepatcher)syncMethod.Invoke(null, [derivedType, method, null]);
postProcess?.Invoke(newSyncMethod);
syncMethodsWithPrepatches.Add(newSyncMethod);
#if RIMWORLD_1_7
// Waylith supports runtime patching; the Mono preloader is not used.
runtimeSyncMethods.Add((derivedType, method), newSyncMethod);
var target = AccessTools.Method(derivedType, method);
if (runtimePatchedMethods.Add(target))
runtimeHarmony.Patch(target, prefix: new HarmonyMethod(typeof(SyncRituals), nameof(RuntimePrefix)));
#endif
}
}

Expand Down
Loading