using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using ExpandWorld.Music; using ExpandWorldData; using HarmonyLib; using Microsoft.CodeAnalysis; using Service; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("RDMods")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Client-side Expand World biome music and environment sync fix for multiplayer.")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("EWSync")] [assembly: AssemblyTitle("EWSync")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RDMods.EWSync { internal static class BiomeSyncService { internal static void Apply() { BiomeManager.LoadEnvironments(); EnvMan instance = EnvMan.instance; if ((Object)(object)instance != (Object)null) { ValheimAccess.ResetEnvironmentCycle(instance); } RefreshMusicIfNeeded(); WorldSyncSession.MarkApplied(); if (WorldSyncConfig.EnableDiagnostics.Value) { EWSyncPlugin.LogInfo("EWSync applied biome environments on client."); } } internal static VerifyResult Verify() { VerifyResult verifyResult = new VerifyResult(); if (WorldSyncConfig.ParsedRules.Count == 0) { verifyResult.AddFailure("No verification rules configured."); return verifyResult; } EnvMan instance = EnvMan.instance; if ((Object)(object)instance == (Object)null) { verifyResult.AddFailure("EnvMan.instance is null."); return verifyResult; } foreach (VerificationRule parsedRule in WorldSyncConfig.ParsedRules) { VerifyRule(parsedRule, instance, verifyResult); } return verifyResult; } private static void VerifyRule(VerificationRule rule, EnvMan envMan, VerifyResult result) { Biome biomeFlag = default(Biome); if (!BiomeManager.TryGetBiome(rule.BiomeName, ref biomeFlag)) { result.AddFailure(rule.BiomeName + ": unknown biome name."); result.RuleSummaries.Add(" " + rule.BiomeName + ": FAIL (unknown biome)"); return; } BiomeEnvSetup val = envMan.m_biomes?.FirstOrDefault((Func)((BiomeEnvSetup b) => b.m_biome == biomeFlag)); if (val == null) { result.AddFailure(rule.BiomeName + ": biome setup missing from EnvMan."); result.RuleSummaries.Add(" " + rule.BiomeName + ": FAIL (missing setup)"); return; } bool flag = true; List list = new List(); if (!MatchesOptional(val.m_musicDay, rule.MusicDay)) { flag = false; result.AddFailure(rule.BiomeName + ": musicDay expected '" + (rule.MusicDay ?? "-") + "', got '" + (val.m_musicDay ?? "(null)") + "'."); list.Add("musicDay FAIL (got " + (val.m_musicDay ?? "null") + ")"); } else { list.Add("musicDay OK"); } if (!MatchesOptional(val.m_musicNight, rule.MusicNight)) { flag = false; result.AddFailure(rule.BiomeName + ": musicNight expected '" + (rule.MusicNight ?? "-") + "', got '" + (val.m_musicNight ?? "(null)") + "'."); list.Add("musicNight FAIL (got " + (val.m_musicNight ?? "null") + ")"); } else { list.Add("musicNight OK"); } foreach (string envName in rule.RequiredEnvironments) { if (val.m_environments == null || !val.m_environments.Any((EnvEntry e) => e.m_environment == envName && e.m_weight > 0f)) { flag = false; result.AddFailure(rule.BiomeName + ": environment '" + envName + "' missing from biome weights."); list.Add("env " + envName + " FAIL"); } else { list.Add("env " + envName + " OK"); } } foreach (string requiredPool in rule.RequiredPools) { if (!IsPoolReady(requiredPool, out string detail)) { flag = false; result.AddFailure(rule.BiomeName + ": music pool '" + requiredPool + "' " + detail + "."); list.Add("pool " + requiredPool + " FAIL (" + detail + ")"); } else { list.Add("pool " + requiredPool + " OK"); } } result.RuleSummaries.Add(" " + rule.BiomeName + ": " + (flag ? "OK" : "FAIL") + " (" + string.Join(", ", list) + ")"); } private static bool MatchesOptional(string? actual, string? expected) { string obj = (string.IsNullOrWhiteSpace(actual) ? "" : actual); string text = expected ?? ""; return obj == text; } private static bool IsPoolReady(string poolName, out string detail) { MusicMan instance = MusicMan.instance; if ((Object)(object)instance == (Object)null) { detail = "MusicMan missing"; return false; } int stableHashCode = StringExtensionMethods.GetStableHashCode(poolName); Dictionary musicHashes = ValheimAccess.GetMusicHashes(instance); if (musicHashes == null) { detail = "music table missing"; return false; } if (!musicHashes.TryGetValue(stableHashCode, out var value) || value == null) { detail = "not registered"; return false; } if (value.m_clips == null || value.m_clips.Length == 0 || (Object)(object)value.m_clips[0] == (Object)null) { detail = "no clips loaded"; return false; } int num = value.m_clips.Count((AudioClip c) => (Object)(object)c != (Object)null); detail = $"{num} clip(s)"; return true; } private static void RefreshMusicIfNeeded() { MusicMan instance = MusicMan.instance; if (!((Object)(object)instance == (Object)null)) { string text = ValheimAccess.GetCurrentMusic(instance)?.m_name ?? ""; instance.Reset(); if (!string.IsNullOrEmpty(text)) { ValheimAccess.StartMusic(instance, text); } } } internal static string? GetCurrentMusicName() { return ValheimAccess.GetCurrentMusic(MusicMan.instance)?.m_name; } internal static string? GetCurrentMusicClipName() { NamedMusic val = (((Object)(object)MusicMan.instance == (Object)null) ? null : ValheimAccess.GetCurrentMusic(MusicMan.instance)); if (val?.m_clips == null || val.m_clips.Length == 0) { return null; } AudioClip obj = val.m_clips[0]; if (obj == null) { return null; } return ((Object)obj).name; } internal static string? GetCurrentEnvironmentName() { EnvMan instance = EnvMan.instance; if (instance == null) { return null; } return instance.GetCurrentEnvironment()?.m_name; } internal static bool IsEnvironmentRegistered(string environmentName) { EnvMan instance = EnvMan.instance; if (instance == null) { return false; } return instance.m_environments?.Any((EnvSetup e) => e.m_name == environmentName) == true; } } [BepInPlugin("rdmods.ewsync", "EWSync", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class EWSyncPlugin : BaseUnityPlugin { private Harmony? _harmony; internal static EWSyncPlugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } private void Awake() { Instance = this; Log = ((BaseUnityPlugin)this).Logger; WorldSyncConfig.Init(((BaseUnityPlugin)this).Config); if (!WorldSyncConfig.Enabled.Value) { LogInfo("EWSync is disabled in config."); return; } _harmony = Harmony.CreateAndPatchAll(typeof(EWSyncPlugin).Assembly, (string)null); LogInfo("EWSync v1.0.0 loaded."); } private void OnDestroy() { WorldSyncSession.Reset(); Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } internal static void LogInfo(string message) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)message); } } internal void ApplyAfterBiomeLoad() { if (WorldSyncConfig.Enabled.Value && NetworkRole.IsMultiplayerClient()) { WorldSyncSession.MarkBiomeDataReceived(); BiomeSyncService.Apply(); VerifyResult verifyResult = BiomeSyncService.Verify(); WorldSyncSession.MarkVerified(verifyResult); if (WorldSyncConfig.EnableDiagnostics.Value) { LogInfo(verifyResult.Passed ? "EWSync verify passed after biome apply." : ("EWSync verify failed after biome apply: " + verifyResult.FormatFailures())); } } } internal void ScheduleSpawnSafetyRetry() { if (WorldSyncConfig.Enabled.Value && WorldSyncConfig.SpawnSafetyRetry.Value && NetworkRole.IsMultiplayerClient() && !WorldSyncSession.IsVerified && !WorldSyncSession.SpawnRetryConsumed && !WorldSyncSession.SpawnRetryScheduled) { WorldSyncSession.SpawnRetryScheduled = true; ((MonoBehaviour)this).StartCoroutine(DelayedSpawnRetry()); } } private IEnumerator DelayedSpawnRetry() { float value = WorldSyncConfig.SpawnRetryDelaySeconds.Value; if (value > 0f) { yield return (object)new WaitForSeconds(value); } WorldSyncSession.MarkSpawnRetryConsumed(); if (WorldSyncConfig.Enabled.Value && NetworkRole.IsMultiplayerClient() && !WorldSyncSession.IsVerified) { BiomeSyncService.Apply(); VerifyResult verifyResult = BiomeSyncService.Verify(); WorldSyncSession.MarkVerified(verifyResult); if (WorldSyncConfig.EnableDiagnostics.Value) { LogInfo(verifyResult.Passed ? "EWSync verify passed after spawn safety retry." : ("EWSync verify failed after spawn safety retry: " + verifyResult.FormatFailures())); } } } } internal static class NetworkRole { internal static bool IsMultiplayerClient() { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 if ((Object)(object)ZNet.instance != (Object)null && (int)ZNet.GetConnectionStatus() == 2) { return !ZNet.instance.IsServer(); } return false; } internal static string DescribeMode() { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 if ((Object)(object)ZNet.instance == (Object)null || (int)ZNet.GetConnectionStatus() != 2) { return "Offline / Menu"; } if (!ZNet.instance.IsServer()) { return "Multiplayer Client"; } return "Host / Solo Server"; } internal static bool PatchesActive() { if (WorldSyncConfig.Enabled.Value) { return IsMultiplayerClient(); } return false; } } internal static class PluginInfo { internal const string Guid = "rdmods.ewsync"; internal const string Name = "EWSync"; internal const string Version = "1.0.0"; } internal static class ValheimAccess { private static readonly FieldInfo EnvPeriodField = AccessTools.Field(typeof(EnvMan), "m_environmentPeriod"); private static readonly FieldInfo EnvFirstField = AccessTools.Field(typeof(EnvMan), "m_firstEnv"); private static readonly FieldInfo MusicHashesField = AccessTools.Field(typeof(MusicMan), "m_musicHashes"); private static readonly FieldInfo CurrentMusicField = AccessTools.Field(typeof(MusicMan), "m_currentMusic"); private static readonly MethodInfo StartMusicMethod = AccessTools.Method(typeof(MusicMan), "StartMusic", new Type[1] { typeof(string) }, (Type[])null); internal static void ResetEnvironmentCycle(EnvMan envMan) { EnvPeriodField.SetValue(envMan, -1L); EnvFirstField.SetValue(envMan, true); } internal static Dictionary? GetMusicHashes(MusicMan musicMan) { return MusicHashesField.GetValue(musicMan) as Dictionary; } internal static NamedMusic? GetCurrentMusic(MusicMan musicMan) { object? value = CurrentMusicField.GetValue(musicMan); return (NamedMusic?)((value is NamedMusic) ? value : null); } internal static void StartMusic(MusicMan musicMan, string musicName) { StartMusicMethod.Invoke(musicMan, new object[1] { musicName }); } } internal sealed class VerificationRule { internal string BiomeName = ""; internal string? MusicDay; internal string? MusicNight; internal List RequiredEnvironments = new List(); internal List RequiredPools = new List(); internal static List Parse(string rulesText, Action logWarning) { List list = new List(); if (string.IsNullOrWhiteSpace(rulesText)) { return list; } string[] array = rulesText.Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } string[] array2 = text.Split(new char[1] { '|' }); if (array2.Length < 2) { logWarning("EWSync: Skipping invalid verify rule (missing fields): " + text); continue; } VerificationRule verificationRule = new VerificationRule { BiomeName = array2[0].Trim() }; for (int j = 1; j < array2.Length; j++) { string text2 = array2[j].Trim(); int num = text2.IndexOf('='); if (num <= 0) { logWarning("EWSync: Skipping invalid token in rule for " + verificationRule.BiomeName + ": " + text2); continue; } string text3 = text2.Substring(0, num).Trim().ToLowerInvariant(); string value = text2.Substring(num + 1).Trim(); switch (text3) { case "musicday": verificationRule.MusicDay = NormalizeOptional(value); break; case "musicnight": verificationRule.MusicNight = NormalizeOptional(value); break; case "env": verificationRule.RequiredEnvironments.AddRange(SplitCsv(value)); break; case "pool": verificationRule.RequiredPools.AddRange(SplitCsv(value)); break; default: logWarning("EWSync: Unknown verify key '" + text3 + "' in rule for " + verificationRule.BiomeName); break; } } if (verificationRule.BiomeName.Length > 0) { list.Add(verificationRule); } } return list; } private static string? NormalizeOptional(string value) { if (string.IsNullOrWhiteSpace(value) || value == "-") { return null; } return value; } private static IEnumerable SplitCsv(string value) { return from v in value.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries) select v.Trim() into v where v.Length > 0 select v; } } internal sealed class VerifyResult { internal bool Passed => Failures.Count == 0; internal List Failures { get; } = new List(); internal List RuleSummaries { get; } = new List(); internal void AddFailure(string message) { Failures.Add(message); } internal string FormatFailures() { if (Failures.Count != 0) { return string.Join("; ", Failures); } return "none"; } internal string FormatRuleSummaries() { if (RuleSummaries.Count != 0) { return string.Join("\n", RuleSummaries); } return "(no rules)"; } } internal static class WorldSyncConfig { private const string DefaultRules = "BlackForest|musicDay=blackforest_custom|musicNight=blackforest_night_custom|env=BlackForest_CinderMist|pool=blackforest_custom,blackforest_night_custom;Meadows|musicDay=meadows_day_custom|musicNight=-|pool=meadows_day_custom,mmoheim_morning;Swamp|musicDay=swamp_custom|musicNight=swamp_custom|pool=swamp_custom;Mountain|musicDay=mountain_custom|musicNight=mountain_custom|pool=mountain_custom;Plains|musicDay=plains_custom|musicNight=plains_night_custom|pool=plains_custom,plains_night_custom;Mistlands|musicDay=mistlands_custom|musicNight=mistlands_custom|pool=mistlands_custom;AshLands|musicDay=ashlands|musicNight=ashlands|pool=ashlands;Ocean|musicDay=ocean_sailing_day|musicNight=ocean_sailing_night|pool=ocean_sailing_day,ocean_sailing_night"; internal static ConfigEntry Enabled { get; private set; } = null; internal static ConfigEntry EnableDiagnostics { get; private set; } = null; internal static ConfigEntry SpawnSafetyRetry { get; private set; } = null; internal static ConfigEntry SpawnRetryDelaySeconds { get; private set; } = null; internal static ConfigEntry Rules { get; private set; } = null; internal static IReadOnlyList ParsedRules { get; private set; } = Array.Empty(); internal static void Init(ConfigFile config) { Enabled = config.Bind("General", "Enabled", true, "Master switch for all EWSync client fixes."); EnableDiagnostics = config.Bind("General", "EnableDiagnostics", false, "Log apply/verify summaries when biome config is applied on clients."); SpawnSafetyRetry = config.Bind("General", "SpawnSafetyRetry", true, "If verify fails after biome sync, retry once after local player spawns."); SpawnRetryDelaySeconds = config.Bind("General", "SpawnRetryDelaySeconds", 1.5f, "Seconds to wait after OnSpawned before the safety retry."); Rules = config.Bind("Verification", "Rules", "BlackForest|musicDay=blackforest_custom|musicNight=blackforest_night_custom|env=BlackForest_CinderMist|pool=blackforest_custom,blackforest_night_custom;Meadows|musicDay=meadows_day_custom|musicNight=-|pool=meadows_day_custom,mmoheim_morning;Swamp|musicDay=swamp_custom|musicNight=swamp_custom|pool=swamp_custom;Mountain|musicDay=mountain_custom|musicNight=mountain_custom|pool=mountain_custom;Plains|musicDay=plains_custom|musicNight=plains_night_custom|pool=plains_custom,plains_night_custom;Mistlands|musicDay=mistlands_custom|musicNight=mistlands_custom|pool=mistlands_custom;AshLands|musicDay=ashlands|musicNight=ashlands|pool=ashlands;Ocean|musicDay=ocean_sailing_day|musicNight=ocean_sailing_night|pool=ocean_sailing_day,ocean_sailing_night", "Semicolon-separated biome rules. Format per biome: BiomeName|musicDay=x|musicNight=y|env=a,b|pool=p1,p2"); ReloadRules(); Rules.SettingChanged += delegate { ReloadRules(); }; } private static void ReloadRules() { ParsedRules = VerificationRule.Parse(Rules.Value, delegate(string message) { EWSyncPlugin.LogInfo(message); }); } } internal static class WorldSyncSession { internal static bool BiomeDataReceived { get; private set; } internal static bool IsVerified { get; private set; } internal static bool SpawnRetryScheduled { get; set; } internal static bool SpawnRetryConsumed { get; private set; } internal static VerifyResult? LastVerifyResult { get; private set; } internal static DateTime LastApplyTime { get; private set; } internal static void MarkBiomeDataReceived() { BiomeDataReceived = true; } internal static void MarkVerified(VerifyResult result) { LastVerifyResult = result; if (result.Passed) { IsVerified = true; } } internal static void MarkSpawnRetryConsumed() { SpawnRetryConsumed = true; SpawnRetryScheduled = false; } internal static void MarkApplied() { LastApplyTime = DateTime.UtcNow; } internal static void Reset() { BiomeDataReceived = false; IsVerified = false; SpawnRetryScheduled = false; SpawnRetryConsumed = false; LastVerifyResult = null; LastApplyTime = default(DateTime); } } } namespace RDMods.EWSync.Patches { [HarmonyPatch(typeof(BiomeManager), "Load")] internal static class BiomeManager_Load_Patch { private static void Postfix() { EWSyncPlugin.Instance?.ApplyAfterBiomeLoad(); } } [HarmonyPatch(typeof(FejdStartup), "ShowCharacterSelection")] internal static class FejdStartup_ShowCharacterSelection_Patch { private static void Prefix() { WorldSyncSession.Reset(); } } [HarmonyPatch(typeof(Game), "Logout")] internal static class Game_Logout_Patch { private static void Prefix() { WorldSyncSession.Reset(); } } [HarmonyPatch(typeof(MusicManager), "FromSetting")] internal static class MusicManager_FromSetting_Patch { private static void Prefix(List data) { if (!WorldSyncConfig.Enabled.Value || !NetworkRole.IsMultiplayerClient()) { return; } string text = Yaml.ReadFiles(MusicManager.Pattern); if (string.IsNullOrEmpty(text)) { return; } Dictionary> dictionary = (from entry in Yaml.Read(text, "Music", (Action)Log.Warning).ToList() where entry.clips != null && entry.clips.Count > 0 select entry).ToDictionary((Data entry) => entry.name, (Data entry) => entry.clips); if (dictionary.Count == 0) { return; } int num = 0; foreach (Data datum in data) { if (dictionary.TryGetValue(datum.name, out var value) && value != null) { datum.clips = new List(value); num++; } } if (num > 0 && WorldSyncConfig.EnableDiagnostics.Value) { EWSyncPlugin.LogInfo($"EWSync merged local clip paths for {num} music pool(s) on client."); } } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class Player_OnSpawned_Patch { private static void Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { EWSyncPlugin.Instance?.ScheduleSpawnSafetyRetry(); } } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] internal static class ZNet_OnDestroy_Patch { private static void Postfix() { WorldSyncSession.Reset(); } } } namespace RDMods.EWSync.Commands { internal static class SyncCheckCommand { [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__Run; } internal static void Register() { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown object obj = <>O.<0>__Run; if (obj == null) { ConsoleEvent val = Run; <>O.<0>__Run = val; obj = (object)val; } new ConsoleCommand("ewsync_check", "EWSync: print biome sync status, current music, and current weather.", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } private static void Run(ConsoleEventArgs args) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("=== EWSync (RDMods) ==="); stringBuilder.AppendLine($"Plugin enabled: {WorldSyncConfig.Enabled.Value}"); stringBuilder.AppendLine("Mode: " + NetworkRole.DescribeMode()); stringBuilder.AppendLine($"Patches active: {NetworkRole.PatchesActive()}"); stringBuilder.AppendLine($"Session verified: {WorldSyncSession.IsVerified}"); stringBuilder.AppendLine($"Biome data received: {WorldSyncSession.BiomeDataReceived}"); stringBuilder.AppendLine($"Spawn retry used: {WorldSyncSession.SpawnRetryConsumed}"); stringBuilder.AppendLine($"Verify rules loaded: {WorldSyncConfig.ParsedRules.Count}"); VerifyResult lastVerifyResult = WorldSyncSession.LastVerifyResult; stringBuilder.AppendLine(); stringBuilder.AppendLine("--- Config rules ---"); stringBuilder.AppendLine(lastVerifyResult?.FormatRuleSummaries() ?? "(no verify run yet)"); stringBuilder.AppendLine(); stringBuilder.AppendLine("--- Live runtime ---"); stringBuilder.AppendLine("Current music: " + (BiomeSyncService.GetCurrentMusicName() ?? "(none)")); stringBuilder.AppendLine("Current music clip: " + (BiomeSyncService.GetCurrentMusicClipName() ?? "(none)")); stringBuilder.AppendLine("Current weather: " + (BiomeSyncService.GetCurrentEnvironmentName() ?? "(none)")); stringBuilder.AppendLine(); stringBuilder.AppendLine("--- Last verify ---"); if (lastVerifyResult == null) { stringBuilder.AppendLine("Result: (not run yet)"); } else { stringBuilder.AppendLine("Result: " + (lastVerifyResult.Passed ? "PASSED" : "FAILED")); stringBuilder.AppendLine("Failures: " + lastVerifyResult.FormatFailures()); } if (!NetworkRole.PatchesActive()) { stringBuilder.AppendLine(); stringBuilder.AppendLine("Note: EWSync client patches only run while connected as a non-host client."); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Edit BepInEx/config/rdmods.ewsync.cfg [Verification] Rules if expand_biomes.yaml changes."); string text = stringBuilder.ToString().TrimEnd(Array.Empty()); args.Context.AddString(text); EWSyncPlugin.LogInfo(text.Replace("\n", " | ")); } } [HarmonyPatch(typeof(Terminal), "InitTerminal")] internal static class Terminal_InitTerminal_Patch { private static void Postfix() { SyncCheckCommand.Register(); } } }