Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of FiresVAngarde v0.2.37
FiresVAngarde.dll
Decompiled 2 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using FiresCore.Async; using FiresCore.Bridge; using FiresCore.ClientLogRelay.Transport; using FiresCore.Help; using FiresCore.IO; using FiresCore.Logging; using FiresCore.Net; using FiresCore.Sync; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Splatform; using TMPro; using UnityEngine; using VerdantsAscent.Managers; using VerdantsAscent.Modules.AntiCheat; using VerdantsAscent.Modules.Characters; using VerdantsAscent.Modules.Characters.Transport; using VerdantsAscent.Utilities; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("FiresVAngarde")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("FiresVAngarde")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("6905d51a-5fb4-408f-896f-bfc41cebd1b7")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 VerdantsAscent { [BepInPlugin("com.Fire.FiresVAngarde", "FiresVAngarde", "0.2.37")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class FiresVAngarde : BaseUnityPlugin { public const string PluginGUID = "com.Fire.FiresVAngarde"; public const string PluginName = "FiresVAngarde"; public const string PluginVersion = "0.2.37"; public static readonly Harmony harmony = new Harmony("com.Fire.FiresVAngarde"); public ConfigSync configSync; private readonly Action<bool> _refreshConfigLockOnAdminChange = delegate { ConfigManager.Instance?.OnConfigLockChanged(); }; public static FiresVAngarde Instance { get; private set; } public static ManualLogSource Log { get; private set; } private void Awake() { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; try { FiresVAngardeBanner.PrintBig(); } catch (Exception ex) { Log.LogWarning((object)("Big banner failed: " + ex.Message)); } Log.LogInfo((object)"[FiresVAngarde] Awake() entered — version 0.2.37"); configSync = new ConfigSync("com.Fire.FiresVAngarde") { DisplayName = "FiresVAngarde", CurrentVersion = "0.2.37", MinimumRequiredVersion = "0.2.37" }; configSync.lockedConfigChanged += ConfigManager.Instance.OnConfigLockChanged; AdminSyncing.AdminStatusChanged += _refreshConfigLockOnAdminChange; ConfigManager.Instance.Initialize(((BaseUnityPlugin)this).Config); BindConfigsToSync(); VAngardeConfig.Initialize(((BaseUnityPlugin)this).Config); VAngardeConfig.BindToSync(configSync); CharactersConfig.Initialize(((BaseUnityPlugin)this).Config); CharactersConfig.BindToSync(configSync); try { harmony.PatchAll(Assembly.GetExecutingAssembly()); } catch (Exception arg) { Debug.LogError((object)string.Format("[{0}] harmony.PatchAll threw: {1}", "FiresVAngarde", arg)); } try { VangardeHelpContent.Register(); } catch (Exception ex2) { Debug.LogWarning((object)("[FiresVAngarde] Help registration failed: " + ex2.Message)); } Debug.Log((object)"[FiresVAngarde] loaded — anti-cheat + character subsystems active (v0.2.37)."); ((MonoBehaviour)this).StartCoroutine(EmitCompactBannerWhenZNetReady()); } private IEnumerator EmitCompactBannerWhenZNetReady() { while ((Object)(object)ZNetScene.instance == (Object)null || ZNetScene.instance.m_prefabs == null || ZNetScene.instance.m_prefabs.Count == 0) { yield return null; } yield return null; try { FiresVAngardeBanner.Print(); } catch (Exception ex) { Log.LogInfo((object)("FiresVAngarde v0.2.37 loaded. (banner failed: " + ex.Message + ")")); } } private void BindConfigsToSync() { ConfigManager instance = ConfigManager.Instance; configSync.AddLockingConfigEntry<bool>(instance.configServerAuthority); configSync.AddConfigEntry<bool>(instance.configVerboseLogging); } private void OnDestroy() { CharacterDiskWriter.Drain(); if (configSync != null) { configSync.lockedConfigChanged -= ConfigManager.Instance.OnConfigLockChanged; configSync = null; } AdminSyncing.AdminStatusChanged -= _refreshConfigLockOnAdminChange; try { ConfigManager.Instance?.Dispose(); } catch { } Instance = null; } } public static class FiresLogger { private static readonly FiresLog Log = new FiresLog("FiresVAngarde", (Func<bool>)(() => ConfigManager.Instance?.configVerboseLogging?.Value == true)); public static bool VerboseEnabled => Log.VerboseEnabled; public static void LogInfo(string message) { Log.Info(message); } public static void LogVerbose(string message) { Log.Verbose(message); } public static void LogWarning(string message) { Log.Warning(message); } public static void LogError(string message) { Log.Error(message); } } internal static class FiresVAngardeBanner { private static bool s_compactPrinted; private static readonly Segment[][] s_compactLines = new Segment[12][] { (Segment[])(object)new Segment[1] { new Segment(" ╔═══════╗", ConsoleColor.DarkGray) }, (Segment[])(object)new Segment[5] { new Segment(" ║", ConsoleColor.DarkGray), new Segment("██ ", ConsoleColor.DarkRed), new Segment("◉", ConsoleColor.Cyan), new Segment(" ██", ConsoleColor.DarkRed), new Segment("║", ConsoleColor.DarkGray) }, (Segment[])(object)new Segment[3] { new Segment(" ╚═", ConsoleColor.DarkGray), new Segment("█████", ConsoleColor.DarkRed), new Segment("═╝", ConsoleColor.DarkGray) }, (Segment[])(object)new Segment[1] { new Segment(" ╲███╱", ConsoleColor.Red) }, (Segment[])(object)new Segment[1] { new Segment(" ╲█╱", ConsoleColor.DarkRed) }, (Segment[])(object)new Segment[1] { new Segment(" ╔══════════════════════════╗", ConsoleColor.DarkGray) }, (Segment[])(object)new Segment[5] { new Segment(" ║ ", ConsoleColor.DarkGray), new Segment("[", ConsoleColor.Gray), new Segment("██████████████████", ConsoleColor.Green), new Segment("]", ConsoleColor.Gray), new Segment(" ║", ConsoleColor.DarkGray) }, (Segment[])(object)new Segment[3] { new Segment(" ║ ", ConsoleColor.DarkGray), new Segment("\ud83c\udfdb WATCH POSTED", ConsoleColor.Yellow), new Segment(" ║", ConsoleColor.DarkGray) }, (Segment[])(object)new Segment[1] { new Segment(" ╚══════════════════════════╝", ConsoleColor.DarkGray) }, (Segment[])(object)new Segment[1] { new Segment(" FIRES VANGARDE", ConsoleColor.Cyan) }, (Segment[])(object)new Segment[0], (Segment[])(object)new Segment[1] { new Segment(" Fires VAngarde Loaded", ConsoleColor.Magenta) } }; private static readonly (string text, ConsoleColor color)[] s_bigLines = new(string, ConsoleColor)[70] { ("", ConsoleColor.Gray), ("%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%@%%%%@%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%@%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=::-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=::-%%%%%@%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=::-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%@%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=::-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%", ConsoleColor.Red), ("%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-:::::::::::::::*@%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%@%@%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%-:::::::::::::::*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%******=::-******#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=::-%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:...................................*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%:...................................*%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%:...................................*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:...................................*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:...................................*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%@%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%:...................................*%%%@%@@%%%%%%%%%%%%%%%%@%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:...................................*%%%%@%%%%%%%%%%%%%%%%%@%%%%%%%@", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:..........::----------::...........*@%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%:........:-%%%%%----%%%%%-::........*%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%:......:-%%%%%%:....:-%%%%%-:.......*%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%@%%%%%%%@%%%%%%%%%:.....:+%@%%%%-..::..:%%%%%%%:......*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:.....-%%%%%%%:.:%%:..*%%@%%%*......*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%:.....-%%%%%%%:.:%%:..*%%%%%%%......*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:.....:%%%%%%%:..::..:%%%%%%%-......*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@", ConsoleColor.DarkRed), ("%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%:......:-%%%%%+:....:-%%%%%%-.......*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:.......:--%%%%%-::-=%%%%--:........*%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%@%%%%%%%%%%%%%%%%%%%%@%%%%%%%:..........::---@%%%*---:...........*@%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%-..................................:*%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-:..............................:-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*:............................:-%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%-:.........................-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%-:......................:-%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-....................:-%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-:.................-%%%%%%%%%%%@%%@%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=:..............:-%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%@%@%%%%%%%@%%%%%%%%%%%%%%%%%%%%@%-:...........:@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%@@%%%%%%%%%%%%%%%%%%%%%-:........:-%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-......:-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkRed), ("%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%-:...-%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%@@%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%@%%%%%%%%%%%%%%%-::-%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%", ConsoleColor.DarkGray), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray), ("%%@%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@%%%%%%%%%", ConsoleColor.DarkGray), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%@%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray), ("%@%%%%%%%%%%%%%%@%%@%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%@%%", ConsoleColor.DarkGray), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%", ConsoleColor.DarkGray), ("%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray), ("%%%%%%%%%@%%%%@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%@%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.White), ("%%%%%-::::#%::::-%-::::::%%%::::-%-:::%@%-::.:::-%%%%:::::::%%%::::::::-%%-::::::::-%@-:::::::%%%%%%", ConsoleColor.Red), ("%%%%%-....--....:%:......-%%:...:%:...%%:........:%%=.......%%%.........-%-.........-@:......:%%%%%%", ConsoleColor.White), ("%%%%%#....:-....-%.......:%%:....::...%@....--....%%-.......-%%....-:...:%-....%:...:%:....%%%%%%%%%", ConsoleColor.Red), ("%%%%%%:...::....%-...:....%%:.....:...%%....--::::%%:...:...:%%....::...:%-....%:...:%:....::-%%%%%%", ConsoleColor.White), ("%%%%%%:....:...:@:...-:...-%:.........%%....-.....%%....*...:%%........:-%-....%:...:%:......:%%@%%%", ConsoleColor.Red), ("%%%%%%-........-%:...::...:%:...:.....%%....-:....%-....-....+%....-:...:%-....%:...:%:....---%%%%%%", ConsoleColor.White), ("%%%%%%%:.......+#.........:%:...-.....%%....--....%:.........-%....--...:%-....%:...:@:....%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%:......:%-....-:....*:...%:....%%:....:....%:...:-:...:%....--...:%-.........:%:.......#%%%%%", ConsoleColor.White), ("%%%%%%%-::::::-%-::::%=::::-::::%*::::%%@-:..:-:::%::::-%-::::%::::--::::%-::::::::-#%-:::::::#%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%", ConsoleColor.White), ("@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%@%%%%%%%%%%%", ConsoleColor.White), ("%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%@%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.White), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Red), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray), ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%@%%%@%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray), ("%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%", ConsoleColor.DarkGray), ("%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray), ("%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%@%@%", ConsoleColor.DarkGray), ("", ConsoleColor.Gray) }; public static void PrintBig() { try { EmitHeader("Fires VAngarde Loading..."); Banner.Print(ToSegments(s_bigLines)); } catch (Exception ex) { Debug.LogWarning((object)("[FiresVAngarde] Big banner failed: " + ex.Message)); } } public static void Print() { if (s_compactPrinted) { return; } s_compactPrinted = true; try { EmitHeader("Fires VAngarde Loaded."); Banner.Print(s_compactLines); } catch (Exception ex) { Debug.LogWarning((object)("[FiresVAngarde] Compact banner failed: " + ex.Message)); } } private static void EmitHeader(string text) { if (FiresVAngarde.Log != null) { FiresVAngarde.Log.LogInfo((object)text); } else { Debug.Log((object)("[FiresVAngarde] " + text)); } } private static Segment[][] ToSegments((string text, ConsoleColor color)[] lines) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) Segment[][] array = new Segment[lines.Length][]; for (int i = 0; i < lines.Length; i++) { array[i] = (Segment[])(object)new Segment[1] { new Segment(lines[i].text, lines[i].color) }; } return array; } } } namespace VerdantsAscent.Utilities { internal static class VangardeHelpContent { private const string ModId = "FiresVAngarde"; private static bool _registered; public static void Register() { if (!_registered && !Application.isBatchMode) { _registered = true; HelpRegistry.RegisterSection("FiresVAngarde", "", "The Server Guardian", 0, (Action<HelpContentWriter>)BuildOverview, false); HelpRegistry.RegisterSection("FiresVAngarde", "", "Mod Validation", 1, (Action<HelpContentWriter>)BuildModValidation, true); HelpRegistry.RegisterSection("FiresVAngarde", "", "Detection & Enforcement", 2, (Action<HelpContentWriter>)BuildDetection, true); HelpRegistry.RegisterSection("FiresVAngarde", "", "Character Enforcement", 3, (Action<HelpContentWriter>)BuildCharacters, true); HelpRegistry.RegisterSection("FiresVAngarde", "", "Commands", 4, (Action<HelpContentWriter>)BuildCommands, true); } } private static void BuildOverview(HelpContentWriter w) { w.Header("The Server Guardian"); w.Paragraph("FiresVAngarde keeps the server's mod list and characters honest. As a player you'll only ever notice it if something is off with your install:"); w.Bullet("If your mods don't match the server's requirements, you're disconnected with a CLEAR panel — not a silent boot. It lists exactly what to REMOVE, UPDATE (client → server version shown), or INSTALL."); w.Bullet("Fix your mod list to match and reconnect — that's all there is to it."); w.Paragraph("Character enforcement (single character, server-owned characters, hardcore) may also be active depending on the server's settings."); } private static void BuildModValidation(HelpContentWriter w) { w.AdminHeader("Mod Validation"); w.Paragraph("Connecting clients are challenged for their mod list and validated against list files in BepInEx/config/VAngarde/ on the server:"); w.Bullet("mod_blacklist.txt — reject if present"); w.Bullet("mod_exactmatch.txt — required mods at exact versions (auto-generated from the server's plugins minus server-only entries)"); w.Bullet("mod_whitelist.txt — extra client mods that are allowed"); w.Bullet("mod_serveronly.txt — server-side mods excluded from the required set"); w.Bullet("mod_adminonly.txt — mods only admins may run (overrides the whitelist)"); w.Bullet("exempt_players.txt — SteamIDs that bypass anti-cheat entirely"); w.Bullet("admin_character_exceptions.txt — demote a real admin to non-admin on named characters"); w.AdminDivider(); w.SubHeader("Keeping Lists Fresh"); w.Bullet("[VAngarde.ModValidation] EnforceModList / UseWhitelist / UseBlacklist / UseExactMatch — which checks run"); w.Bullet("AutoUpdateModList — regenerate whitelist+exactmatch from the server's own plugins each launch"); w.Bullet("AdminModsExtendWhitelist — an admin connecting with a new mod whitelists it for everyone"); w.Bullet("vangarde push mods — push YOUR client's mod list up as the new standard"); w.Bullet("Edit a list file, then: vangarde reload (all) or vangarde push whitelist|blacklist|exempt (one)"); } private static void BuildDetection(HelpContentWriter w) { w.AdminHeader("Detection & Enforcement"); w.Bullet("DetectGodMode — LOG-ONLY, never auto-kicks (vanilla admins use god legitimately)"); w.Bullet("DetectFlight / DetectSpeedHack (threshold m/s) / DetectCheatItems + BannedItems"); w.Bullet("ViolationAction — kick or log"); w.Bullet("AdminBypass — admins exempt; AdminMonitoring still scans and reports them"); w.Bullet("AdminCommandLogging — devcommands/god/fly toggles by admins post to Discord"); w.Bullet("ChallengeTimeoutSeconds / PeriodicRecheckMinutes — validation cadence"); w.Bullet("[VAngarde.HardeningV2] — audit/signature/integrity toggles are TELEMETRY only (client self-reports), not proof; leave off unless collecting data"); w.AdminDivider(); w.Paragraph("Kicks and violations post to Discord through FiresDiscordIntegration when it's installed (anti-cheat webhook)."); } private static void BuildCharacters(HelpContentWriter w) { w.AdminHeader("Character Enforcement"); w.Paragraph("Server-side character management under [Characters]:"); w.Bullet("SingleCharacterMode — one character per SteamID on this server"); w.Bullet("ForceServerCharacter — characters made elsewhere are sent back to create a fresh one here (ServerCharacters-style)"); w.Bullet("HardcoreMode — character is deleted on death"); w.Bullet("MaintenanceMode — non-admins are kicked (with a countdown) until it's lifted"); w.Bullet("AfkKickMinutes — auto-logout idle players (0 = off; admins exempt by default)"); w.Bullet("BackupsToKeep — rotating ZIP backups of server-held characters (default 25)"); w.AdminDivider(); w.Paragraph("Profile transfers ride a chunked reliable channel sized to dodge the classic 30-second big-character timeout ([Characters.Transport]). va_chan_test <sizeKB> proves the pipe with a synthetic payload. The Characters.ServerKey is auto-generated and signs saves against rollback — never edit or share it."); } private static void BuildCommands(HelpContentWriter w) { w.AdminHeader("Commands"); w.Bullet("vangarde status — config + tracked peers"); w.Bullet("vangarde scan — run an immediate cheat scan (server)"); w.Bullet("vangarde kick <steamid> — force-disconnect a peer (server)"); w.Bullet("vangarde reload — hot-reload all list files (server)"); w.Bullet("vangarde push whitelist|blacklist|exempt — reload one list (server)"); w.Bullet("vangarde push mods — push your local mod list as whitelist/exact-match (client admin)"); w.Bullet("vangarde bypass [on|off] — toggle admin bypass at runtime"); w.Bullet("vangarde log <steamid> — post a player's cached BepInEx log to Discord (server)"); w.Bullet("va_chan_test <sizeKB> — stress-test the chunked transfer channel"); } } } namespace VerdantsAscent.Modules.Characters { public static class CharactersConfig { public static ConfigEntry<bool> Enabled; public static ConfigEntry<int> ConfigChunkSizeBytes; public static ConfigEntry<int> ConfigQueueBackpressureBytes; public static ConfigEntry<int> ConfigCrossplayChunkSizeBytes; public static ConfigEntry<int> ConfigCrossplayInFlightBytes; public static ConfigEntry<float> ConfigNoProgressTimeoutSeconds; public static ConfigEntry<float> ConfigLogoutSaveWaitSeconds; public static ConfigEntry<float> ConfigShutdownSaveWaitSeconds; public static ConfigEntry<int> ConfigBackupsToKeep; public static ConfigEntry<bool> SingleCharacterMode; public static ConfigEntry<bool> SingleCharacterAdminBypass; public static ConfigEntry<bool> ForceServerCharacter; public static ConfigEntry<bool> ForceServerCharacterAdminBypass; public static ConfigEntry<bool> HardcoreMode; public static ConfigEntry<bool> MaintenanceMode; public static ConfigEntry<int> MaintenanceTimerSeconds; public static ConfigEntry<int> AfkKickMinutes; public static ConfigEntry<bool> ExcludeAdminsFromAfk; public static ConfigEntry<string> ServerKey; public static int ChunkSizeBytes => ConfigChunkSizeBytes?.Value ?? 262144; public static int QueueBackpressureBytes => ConfigQueueBackpressureBytes?.Value ?? 200000; public static int CrossplayChunkSizeBytes => ConfigCrossplayChunkSizeBytes?.Value ?? 16384; public static int CrossplayInFlightBytes => ConfigCrossplayInFlightBytes?.Value ?? 65536; public static float NoProgressTimeoutSeconds => ConfigNoProgressTimeoutSeconds?.Value ?? 120f; public static float LogoutSaveWaitSeconds => ConfigLogoutSaveWaitSeconds?.Value ?? 15f; public static float ShutdownSaveWaitSeconds => ConfigShutdownSaveWaitSeconds?.Value ?? 6f; public static int BackupsToKeep => ConfigBackupsToKeep?.Value ?? 25; public static void Initialize(ConfigFile config) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Expected O, but got Unknown //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Expected O, but got Unknown //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Expected O, but got Unknown //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Expected O, but got Unknown //IL_0194: Unknown result type (might be due to invalid IL or missing references) //IL_019e: Expected O, but got Unknown //IL_01c2: Unknown result type (might be due to invalid IL or missing references) //IL_01cc: Expected O, but got Unknown //IL_0299: Unknown result type (might be due to invalid IL or missing references) //IL_02a3: Expected O, but got Unknown //IL_02c6: Unknown result type (might be due to invalid IL or missing references) //IL_02d0: Expected O, but got Unknown Enabled = config.Bind<bool>("Characters", "Enabled", true, "Enable the server-side character persistence/transfer subsystem."); ConfigChunkSizeBytes = config.Bind<int>("Characters.Transport", "ChunkSizeBytes", 262144, new ConfigDescription("Payload bytes per chunk. Held well under Steam's 512 KB single-message limit.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(16384, 491520), Array.Empty<object>())); ConfigQueueBackpressureBytes = config.Bind<int>("Characters.Transport", "QueueBackpressureBytes", 200000, new ConfigDescription("Pause sending while the peer's socket send-queue exceeds this many bytes. Pure back-pressure — a slow link transfers slower, the transfer is never aborted for being slow.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(20000, 1000000), Array.Empty<object>())); ConfigCrossplayChunkSizeBytes = config.Bind<int>("Characters.Transport", "CrossplayChunkSizeBytes", 16384, new ConfigDescription("Payload bytes per chunk on a crossplay (PlayFab) connection. Kept small: PlayFab dropped a join that sent the profile as one large message among the join's other traffic.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(4096, 65536), Array.Empty<object>())); ConfigCrossplayInFlightBytes = config.Bind<int>("Characters.Transport", "CrossplayInFlightBytes", 65536, new ConfigDescription("On a crossplay (PlayFab) connection, pause while it has more than this many bytes sent and not yet acknowledged (all of its traffic, not only the profile). Vanilla's own world-data sender stays near 40 KB.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(16384, 524288), Array.Empty<object>())); ConfigNoProgressTimeoutSeconds = config.Bind<float>("Characters.Transport", "NoProgressTimeoutSeconds", 120f, new ConfigDescription("Abort a transfer only after this many seconds with NO progress (no ACK advance). Reported to the caller as failure; the peer connection is never closed.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(30f, 600f), Array.Empty<object>())); ConfigLogoutSaveWaitSeconds = config.Bind<float>("Characters.Transport", "LogoutSaveWaitSeconds", 15f, new ConfigDescription("At logout, keep the connection open up to this many seconds until the server confirms it saved the character. A crossplay (PlayFab) link drops whatever is still queued when it closes, so without the wait the logout's save never arrives. The logout goes ahead at once if the connection is lost.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 60f), Array.Empty<object>())); ConfigShutdownSaveWaitSeconds = config.Bind<float>("Characters.Transport", "ShutdownSaveWaitSeconds", 6f, new ConfigDescription("When the server shuts down with players online, it asks each of them to save their character now and waits up to this many seconds for the saves to arrive. 0 turns it off. A player whose save doesn't make it in time keeps a signed backup on their machine that the server restores at their next login.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>())); ConfigBackupsToKeep = config.Bind<int>("Characters", "BackupsToKeep", 25, new ConfigDescription("Rotating profile backups to keep per character (ZIP in the backups folder).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 50), Array.Empty<object>())); SingleCharacterMode = config.Bind<bool>("Characters.Enforcement", "SingleCharacterMode", false, "When enabled, each Steam ID is restricted to a single stored character. Admins are exempt."); SingleCharacterAdminBypass = config.Bind<bool>("Characters.Enforcement", "SingleCharacterAdminBypass", true, "When ON (default), admins may join with any character and bypass single-character enforcement. Turn OFF to also enforce single-character on admins (e.g. for testing)."); ForceServerCharacter = config.Bind<bool>("Characters.Enforcement", "ForceServerCharacter", false, "When ON, a player connecting with a character that has already been played elsewhere (single-player or another server) is sent back to the menu asking them to create a new character first — no data is lost. Brand-new characters are allowed and become the player's server character. Returning players get their stored server character. (ServerCharacters behavior.)"); ForceServerCharacterAdminBypass = config.Bind<bool>("Characters.Enforcement", "ForceServerCharacterAdminBypass", true, "When ON (default), admins bypass ForceServerCharacter and keep their imported character. Turn OFF to enforce ForceServerCharacter on admins too (e.g. for testing). Mirrors SingleCharacterAdminBypass."); HardcoreMode = config.Bind<bool>("Characters.Enforcement", "HardcoreMode", false, "When enabled, a character is deleted on death and the player is logged out."); MaintenanceMode = config.Bind<bool>("Characters.Maintenance", "MaintenanceMode", false, "When enabled, non-admin clients are kicked on connect and the maintenance countdown is broadcast."); MaintenanceTimerSeconds = config.Bind<int>("Characters.Maintenance", "MaintenanceTimerSeconds", 300, new ConfigDescription("Seconds of countdown before connected non-admins are kicked when MaintenanceMode is enabled.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 1800), Array.Empty<object>())); AfkKickMinutes = config.Bind<int>("Characters.Enforcement", "AfkKickMinutes", 0, new ConfigDescription("Minutes of input inactivity before a player is auto-logged-out. 0 disables.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 60), Array.Empty<object>())); ExcludeAdminsFromAfk = config.Bind<bool>("Characters.Enforcement", "ExcludeAdminsFromAfk", true, "When enabled, admins are not AFK-kicked."); ServerKey = config.Bind<string>("Characters", "ServerKey", string.Empty, "Master base64-encoded 32-byte server key used to derive per-connection AES keys for the emergency-backup signature (anti-rollback). Auto-generated on first server start when empty. Do not edit. Rotating it invalidates all outstanding emergency backups."); } public static void BindToSync(ConfigSync sync) { sync.AddConfigEntry<bool>(Enabled); sync.AddConfigEntry<int>(ConfigChunkSizeBytes); sync.AddConfigEntry<int>(ConfigQueueBackpressureBytes); sync.AddConfigEntry<int>(ConfigCrossplayChunkSizeBytes); sync.AddConfigEntry<int>(ConfigCrossplayInFlightBytes); sync.AddConfigEntry<float>(ConfigNoProgressTimeoutSeconds); sync.AddConfigEntry<float>(ConfigLogoutSaveWaitSeconds); sync.AddConfigEntry<int>(ConfigBackupsToKeep); sync.AddConfigEntry<bool>(SingleCharacterMode); sync.AddConfigEntry<bool>(SingleCharacterAdminBypass); sync.AddConfigEntry<bool>(ForceServerCharacter); sync.AddConfigEntry<bool>(ForceServerCharacterAdminBypass); sync.AddConfigEntry<bool>(HardcoreMode); sync.AddConfigEntry<bool>(MaintenanceMode); sync.AddConfigEntry<int>(MaintenanceTimerSeconds); sync.AddConfigEntry<int>(AfkKickMinutes); sync.AddConfigEntry<bool>(ExcludeAdminsFromAfk); } } public static class CharactersModule { [CompilerGenerated] private static class <>O { public static Action<long, byte[]> <0>__OnTestPayload; public static ConsoleEvent <1>__CmdChanTest; } private const string TestChannel = "va.test"; public static void OnReady() { if (CharactersConfig.Enabled == null || CharactersConfig.Enabled.Value) { ReliableChunkChannel.EnsureRpcsRegistered(); ReliableChunkChannel.RegisterReceiver("va.test", OnTestPayload); } } public static void RegisterConsoleCommands() { //IL_002f: 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.<1>__CmdChanTest; if (obj == null) { ConsoleEvent val = CmdChanTest; <>O.<1>__CmdChanTest = val; obj = (object)val; } new ConsoleCommand("va_chan_test", "va_chan_test <sizeKB> — stream a test payload over the reliable chunk channel to prove large transfers complete without the ServerCharacters-style 30s timeout.", (ConsoleEvent)obj, true, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } private static void CmdChanTest(ConsoleEventArgs args) { if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { Terminal context = args.Context; if (context != null) { context.AddString("[Characters] not connected to a world."); } return; } int sizeKB = 4096; if (args.Length >= 2) { int.TryParse(args[1], out sizeKB); } sizeKB = Mathf.Clamp(sizeKB, 1, 65536); int num = sizeKB * 1024; byte[] array = new byte[num]; for (int i = 0; i < num; i++) { array[i] = (byte)(i & 0xFF); } long num2; if (ZNet.instance.IsServer()) { List<ZNetPeer> peers = ZNet.instance.GetPeers(); if (peers == null || peers.Count == 0) { Terminal context2 = args.Context; if (context2 != null) { context2.AddString("[Characters] no connected peers to send to."); } return; } num2 = peers[0].m_uid; } else { num2 = ZRoutedRpc.instance.GetServerPeerID(); } float start = Time.realtimeSinceStartup; Terminal context3 = args.Context; if (context3 != null) { context3.AddString($"[Characters] streaming {sizeKB} KB to {num2}…"); } ReliableChunkChannel.Send(num2, "va.test", array, delegate(bool ok) { float num3 = Time.realtimeSinceStartup - start; Debug.Log((object)string.Format("[Characters][test] send {0} in {1:0.0}s ({2} KB).", ok ? "CONFIRMED" : "FAILED", num3, sizeKB)); }); } private static void OnTestPayload(long sender, byte[] payload) { bool flag = true; for (int i = 0; i < payload.Length; i++) { if (payload[i] != (byte)(i & 0xFF)) { flag = false; break; } } Debug.Log((object)string.Format("[Characters][test] received {0}B from {1} — pattern {2}.", payload.Length, sender, flag ? "OK" : "CORRUPT")); } } [HarmonyPatch] internal static class CharactersPatches { private static bool _cmdAdded; [HarmonyPatch(typeof(Game), "Start")] [HarmonyPostfix] private static void Game_Start_Postfix() { CharactersModule.OnReady(); } [HarmonyPatch(typeof(Terminal), "InitTerminal")] [HarmonyPostfix] private static void Terminal_InitTerminal_Postfix() { if (!_cmdAdded) { _cmdAdded = true; CharactersModule.RegisterConsoleCommands(); } } } public static class PlayerProfileBytes { [HarmonyPatch(typeof(PlayerProfileBytes), "LoadFromBytes")] private static class LoadFromBytesPatch { private static IEnumerable<CodeInstruction> Transpiler(ILGenerator il) { List<CodeInstruction> source = PatchProcessor.GetOriginalInstructions((MethodBase)AccessTools.DeclaredMethod(typeof(PlayerProfile), "LoadPlayerFromDisk", (Type[])null, (Type[])null), il); MethodInfo readFromDisk = AccessTools.DeclaredMethod(typeof(PlayerProfile), "LoadPlayerDataFromDisk", (Type[])null, (Type[])null); ConstructorInfo fromBytes = AccessTools.DeclaredConstructor(typeof(ZPackage), new Type[1] { typeof(byte[]) }, false); bool replaced = false; for (int i = 0; i < source.Count; i++) { if (!replaced && i + 1 < source.Count && CodeInstructionExtensions.IsLdarg(source[i], (int?)0) && CodeInstructionExtensions.Calls(source[i + 1], readFromDisk)) { yield return CodeInstructionExtensions.MoveBlocksFrom(CodeInstructionExtensions.MoveLabelsFrom(new CodeInstruction(OpCodes.Ldarg_1, (object)null), source[i]), source[i]); yield return CodeInstructionExtensions.MoveBlocksFrom(CodeInstructionExtensions.MoveLabelsFrom(new CodeInstruction(OpCodes.Newobj, (object)fromBytes), source[i + 1]), source[i + 1]); i++; replaced = true; } else { yield return source[i]; } } if (!replaced) { Debug.LogError((object)"[Characters] LoadFromBytes: PlayerProfile.LoadPlayerFromDisk no longer calls LoadPlayerDataFromDisk; uploaded profiles cannot be read, so server-held characters are not saved."); } } } public static bool LoadFromBytes(this PlayerProfile profile, byte[] data) { throw new NotImplementedException("Body is supplied by the transpiler in LoadFromBytesPatch."); } } public static class CharacterStore { private static readonly Dictionary<string, int> s_pending = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); public static string SaveDir => SaveSystem.GetCharacterFolderPath((FileSource)2); public static string FileName(string steamId, string characterName) { return steamId + "_" + characterName?.ToLowerInvariant(); } public static byte[] LoadProfileBytes(string steamId, string characterName) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) try { ZPackage obj = new PlayerProfile(FileName(steamId, characterName), (FileSource)2).LoadPlayerDataFromDisk(); return ((obj != null) ? obj.GetArray() : null) ?? Array.Empty<byte>(); } catch (Exception ex) { Debug.LogWarning((object)("[Characters] LoadProfileBytes failed for " + steamId + "/" + characterName + ": " + ex.Message)); return Array.Empty<byte>(); } } public static bool SaveProfileBytes(string steamId, string characterName, byte[] data) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown if (data == null || data.Length == 0) { return false; } if (HasPendingSave(steamId)) { CharacterDiskWriter.WaitIdle(); } try { PlayerProfile val = new PlayerProfile(FileName(steamId, characterName), (FileSource)2); if (!val.LoadFromBytes(data)) { return false; } val.SavePlayerToDisk(); QueueBackup(val); return true; } catch (Exception ex) { Debug.LogError((object)("[Characters] SaveProfileBytes failed for " + steamId + "/" + characterName + ": " + ex.Message)); return false; } } public static bool HasPendingSave(string steamId) { lock (s_pending) { int value; return s_pending.TryGetValue(steamId ?? "", out value) && value > 0; } } private static void MarkPending(string steamId, int delta) { lock (s_pending) { s_pending.TryGetValue(steamId ?? "", out var value); value += delta; if (value > 0) { s_pending[steamId ?? ""] = value; } else { s_pending.Remove(steamId ?? ""); } } } public static bool SaveProfileBytesInBackground(string steamId, string characterName, byte[] data, Action<bool, double> done) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown if (data == null || data.Length < 4) { return false; } PlayerProfile val; try { val = new PlayerProfile(FileName(steamId, characterName), (FileSource)2); if (!val.LoadFromBytes(data)) { return false; } } catch (Exception ex) { Debug.LogError((object)("[Characters] SaveProfileBytes failed for " + steamId + "/" + characterName + ": " + ex.Message)); return false; } if (BitConverter.ToInt32(data, 0) != 46 || FileHelpers.CloudStorageSupported) { bool arg; try { val.SavePlayerToDisk(); QueueBackup(val); arg = true; } catch (Exception ex2) { Debug.LogError((object)("[Characters] SaveProfileBytes failed for " + steamId + "/" + characterName + ": " + ex2.Message)); arg = false; } done?.Invoke(arg, 0.0); return true; } string filename = val.m_filename; string path = SaveDir + filename + ".fch"; string backupDir = Path.Combine(SaveDir, "backups"); int keep = Math.Max(1, CharactersConfig.BackupsToKeep); DateTime now = DateTime.Now; PlayerProfile.SavingStarted?.Invoke(); MarkPending(steamId, 1); CharacterDiskWriter.Enqueue(delegate { Stopwatch stopwatch = Stopwatch.StartNew(); bool saved = WriteProfileFile(path, data, filename); if (saved && File.Exists(path + ".old")) { WriteBackup(previous: File.ReadAllBytes(path + ".old"), zipPath: Path.Combine(backupDir, filename + ".zip"), entryName: $"{filename}-{now:yyyy-MM-ddTHH-mm-ss}.fch", keep: keep, filename: filename); } double ms = stopwatch.Elapsed.TotalMilliseconds; MarkPending(steamId, -1); MainThreadDispatcher.Enqueue((Action)delegate { if (saved) { SaveSystem.InvalidateCache((SaveDataType)1); ZNet.ConsiderAutoBackup(filename, (SaveDataType)1, now); } PlayerProfile.SavingFinished?.Invoke(); done?.Invoke(saved, ms); }); }); return true; } private static bool WriteProfileFile(string path, byte[] data, string filename) { //IL_0001: 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_0020: Expected O, but got Unknown //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Invalid comparison between Unknown and I4 //IL_006a: Unknown result type (might be due to invalid IL or missing references) try { byte[] array = new ZPackage(data).GenerateHash(); FileWriter val = new FileWriter(path + ".new", (CloudStorageFileGrouping)1, (FileHelperType)0, (FileSource)2); val.m_binary.Write(data.Length); val.m_binary.Write(data); val.m_binary.Write(array.Length); val.m_binary.Write(array); val.Finish(); if ((int)val.Status != 2) { Debug.LogWarning((object)$"[Characters] writing {filename}.fch.new failed ({val.Status}); the previous save is kept."); return false; } FileHelpers.ReplaceOldFile(path, path + ".new", path + ".old", (CloudStorageFileGrouping)1, (FileSource)2); return true; } catch (Exception ex) { Debug.LogWarning((object)("[Characters] writing " + filename + ".fch failed: " + ex.Message + "; the previous save is kept.")); return false; } } public static bool Exists(string steamId, string characterName) { try { return File.Exists(SaveDir + FileName(steamId, characterName) + ".fch"); } catch { return false; } } public static bool SteamIdHasAnyProfile(string steamId) { return GetCanonicalCharacterName(steamId) != null; } public static string GetCanonicalCharacterName(string steamId) { try { if (string.IsNullOrEmpty(steamId) || !Directory.Exists(SaveDir)) { return null; } string text = steamId + "_"; string text2 = null; DateTime dateTime = DateTime.MaxValue; string[] files = Directory.GetFiles(SaveDir, "*.fch"); foreach (string path in files) { string fileName = Path.GetFileName(path); if (IsProfilePattern(fileName) && fileName.StartsWith(text, StringComparison.OrdinalIgnoreCase)) { string text3 = fileName.Substring(text.Length, fileName.Length - text.Length - ".fch".Length); DateTime creationTimeUtc = File.GetCreationTimeUtc(path); if (creationTimeUtc < dateTime || (creationTimeUtc == dateTime && string.CompareOrdinal(text3, text2) < 0)) { dateTime = creationTimeUtc; text2 = text3; } } } return text2; } catch { return null; } } private static bool IsProfilePattern(string file) { if (file.Split(new char[1] { '_' }).Length >= 3 && file.EndsWith(".fch", StringComparison.Ordinal)) { return !file.Contains("_backup_"); } return false; } private static void QueueBackup(PlayerProfile profile) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) try { string text = SaveSystem.GetCharacterFolderPath(profile.m_fileSource) + profile.m_filename + ".fch.old"; if (FileHelpers.Exists(text, profile.m_fileSource)) { byte[] previous = ReadAll(text, profile.m_fileSource); string path = Path.Combine(SaveDir, "backups"); string zipPath = Path.Combine(path, profile.m_filename + ".zip"); string entryName = $"{profile.m_filename}-{DateTime.Now:yyyy-MM-ddTHH-mm-ss}.fch"; int keep = Math.Max(1, CharactersConfig.BackupsToKeep); string filename = profile.m_filename; CharacterDiskWriter.Enqueue(delegate { WriteBackup(zipPath, entryName, keep, previous, filename); }); } } catch (Exception ex) { Debug.LogWarning((object)("[Characters] backup failed for " + profile.m_filename + ": " + ex.Message)); } } private static byte[] ReadAll(string path, FileSource source) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Expected O, but got Unknown FileReader val = new FileReader(path, source, (FileHelperType)0); try { using MemoryStream memoryStream = new MemoryStream(); (val.m_stream?.BaseStream ?? val.m_binary.BaseStream).CopyTo(memoryStream); return memoryStream.ToArray(); } finally { val.Dispose(); } } private static void WriteBackup(string zipPath, string entryName, int keep, byte[] previous, string filename) { try { AtomicFile.Update(zipPath, (Action<FileStream>)delegate(FileStream stream) { using ZipArchive zipArchive = new ZipArchive(stream, ZipArchiveMode.Update, leaveOpen: true); while (zipArchive.Entries.Count >= keep && zipArchive.Entries.Count > 0) { zipArchive.Entries.First().Delete(); } using Stream stream2 = zipArchive.CreateEntry(entryName).Open(); stream2.Write(previous, 0, previous.Length); }); } catch (Exception ex) { Debug.LogWarning((object)("[Characters] backup failed for " + filename + ": " + ex.Message)); } } } internal static class CharacterDiskWriter { private const int DrainTimeoutMs = 10000; private const string ThreadName = "FiresVAngarde.CharacterDisk"; private static readonly BlockingCollection<Action> s_jobs = new BlockingCollection<Action>(); private static readonly object s_startLock = new object(); private static Thread s_thread; private static int s_queued; internal static void Enqueue(Action job) { if (job == null) { return; } EnsureThread(); Interlocked.Increment(ref s_queued); try { s_jobs.Add(job); } catch (InvalidOperationException) { Run(job); } } internal static void Drain() { if (s_thread == null) { return; } Stopwatch stopwatch = Stopwatch.StartNew(); ManualResetEventSlim done = new ManualResetEventSlim(initialState: false); try { s_jobs.Add(delegate { done.Set(); }); } catch (InvalidOperationException) { return; } if (done.Wait(10000)) { Debug.Log((object)$"[Characters] shutdown: character backup queue drained in {stopwatch.ElapsedMilliseconds} ms ({s_queued} backup(s) this session)."); } else { Debug.LogWarning((object)$"[Characters] character backups still writing after {10} s at shutdown."); } } internal static void WaitIdle(int timeoutMs = 5000) { if (s_thread == null || Thread.CurrentThread == s_thread) { return; } ManualResetEventSlim done = new ManualResetEventSlim(initialState: false); try { s_jobs.Add(delegate { done.Set(); }); } catch (InvalidOperationException) { return; } if (!done.Wait(timeoutMs)) { Debug.LogWarning((object)$"[Characters] the disk thread was still busy after {timeoutMs} ms; saving anyway."); } } private static void EnsureThread() { lock (s_startLock) { if (s_thread == null) { s_thread = new Thread(Loop) { IsBackground = true, Name = "FiresVAngarde.CharacterDisk" }; s_thread.Start(); } } } private static void Loop() { foreach (Action item in s_jobs.GetConsumingEnumerable()) { Run(item); } } private static void Run(Action job) { try { job(); } catch (Exception ex) { Debug.LogWarning((object)("[Characters] background disk job failed: " + ex.Message)); } } } [HarmonyPatch] internal static class CharactersVersionGate { internal const string VersionMarker = "VAngarde Anticheat"; internal const string VersionSuffix = ".VAngarde Anticheat"; [HarmonyPatch(typeof(GameVersion), "ToString")] [HarmonyPriority(200)] [HarmonyPostfix] private static void GameVersion_ToString_Postfix(GameVersion __instance, ref string __result) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) if (__instance != Version.CurrentVersion || string.IsNullOrEmpty(__result) || __result.Contains("VAngarde Anticheat")) { return; } int num = 0; for (int i = 0; i < __result.Length; i++) { if (__result[i] == '.') { num++; } } if (num < 2) { __result += ".0"; } __result += ".VAngarde Anticheat"; } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPriority(200)] [HarmonyPrefix] private static bool ZNet_RPC_PeerInfo_Prefix(ZRpc rpc, ZPackage pkg) { if (CharactersConfig.Enabled != null && !CharactersConfig.Enabled.Value) { return true; } pkg.ReadLong(); string text = pkg.ReadString(); pkg.SetPos(0); if (!ZNet.instance.IsServer() || text.Contains("VAngarde Anticheat")) { return true; } FiresConnectReason.Send(rpc, "This server is protected by VAngarde Anticheat", (IEnumerable<string>)new string[2] { "Your client connected without the FiresVAngarde mod this server requires.", "Install FiresVAngarde and reconnect." }); rpc.Invoke("Error", new object[1] { 3 }); Debug.Log((object)("[VAngarde] Kicked " + rpc.m_socket.GetHostName() + ": connected without the mod (.VAngarde Anticheat).")); return false; } } [HarmonyPatch(typeof(ServerListElement), "UpdateTextAndIcons")] internal static class ServerListVersionDisplayClean { private static FieldInfo _versionField; private static bool Prepare() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 return (int)SystemInfo.graphicsDeviceType != 4; } private static void Postfix(ServerListElement __instance) { try { if (_versionField == null) { _versionField = AccessTools.Field(typeof(ServerListElement), "m_version"); } object? obj = _versionField?.GetValue(__instance); TMP_Text val = (TMP_Text)((obj is TMP_Text) ? obj : null); if (!((Object)(object)val == (Object)null) && !string.IsNullOrEmpty(val.text) && val.text.Contains("VAngarde Anticheat")) { val.text = val.text.Replace(".VAngarde Anticheat", "").Replace("-VAngarde Anticheat", "").Replace("VAngarde Anticheat", ""); } } catch { } } } [HarmonyPatch(typeof(ZSteamMatchmaking), "RegisterServer")] internal static class VAngardeServerNameTag { internal const string NameTag = " - VAngarde Anticheat"; private static void Prefix(ref string name) { try { ConfigEntry<bool> enabled = VAngardeConfig.Enabled; if (enabled != null && enabled.Value && !string.IsNullOrEmpty(name) && !name.Contains(" - VAngarde Anticheat")) { name += " - VAngarde Anticheat"; } } catch { } } } public static class CharacterProfileTransport { private class Reassembly { public readonly SortedDictionary<int, byte[]> Fragments = new SortedDictionary<int, byte[]>(); public int Total; public float Expiry; } private static long _counter; private const float CacheExpirySeconds = 120f; private static readonly Dictionary<string, Reassembly> _incoming = new Dictionary<string, Reassembly>(); public static void SendToPeer(ZNetPeer peer, string rpcName, byte[] payload, Action onComplete) { if (peer?.m_rpc == null || (Object)(object)ZNet.instance == (Object)null) { onComplete?.Invoke(); } else { ((MonoBehaviour)ZNet.instance).StartCoroutine(SendRoutine(peer, rpcName, payload ?? Array.Empty<byte>(), onComplete)); } } private static IEnumerator SendRoutine(ZNetPeer peer, string rpcName, byte[] payload, Action onComplete) { byte[] data = Compress(payload); int chunkSize = LinkPace.ChunkBytes(peer.m_socket); int fragments = Math.Max(1, (data.Length + chunkSize - 1) / chunkSize); long packageId = ++_counter; Debug.Log((object)($"[Characters] {rpcName}: {payload.Length}B ({data.Length}B deflated) in {fragments} fragment(s); " + LinkPace.Describe(peer.m_socket) + ".")); for (int i = 0; i < fragments; i++) { while (peer.m_socket != null && peer.m_socket.IsConnected() && LinkPace.Saturated(peer.m_socket)) { yield return null; } if (peer.m_socket == null || !peer.m_socket.IsConnected()) { onComplete?.Invoke(); yield break; } ZPackage val = new ZPackage(); val.Write(packageId); val.Write(i); val.Write(fragments); val.Write(Slice(data, i * chunkSize, chunkSize)); peer.m_rpc.Invoke(rpcName, new object[1] { val }); if (i != fragments - 1) { yield return null; } } onComplete?.Invoke(); } public static Action<ZRpc, ZPackage> Receiver(Action<ZRpc, byte[]> onComplete) { return delegate(ZRpc rpc, ZPackage pkg) { float now = Time.realtimeSinceStartup; foreach (string item in (from kv in _incoming where kv.Value.Expiry < now select kv.Key).ToList()) { _incoming.Remove(item); } long num = pkg.ReadLong(); int key = pkg.ReadInt(); int num2 = pkg.ReadInt(); byte[] value = pkg.ReadByteArray(); object obj; if (rpc == null) { obj = null; } else { ISocket socket = rpc.GetSocket(); obj = ((socket != null) ? socket.GetHostName() : null); } if (obj == null) { obj = "?"; } string key2 = (string?)obj + ":" + num; if (!_incoming.TryGetValue(key2, out var value2)) { value2 = new Reassembly { Total = num2 }; _incoming[key2] = value2; } value2.Fragments[key] = value; value2.Expiry = now + 120f; if (value2.Fragments.Count < num2) { return; } _incoming.Remove(key2); byte[] arg; try { arg = Decompress(Concat(value2.Fragments.Values)); } catch (Exception ex) { Debug.LogError((object)("[Characters] decompress failed: " + ex.Message)); return; } try { onComplete?.Invoke(rpc, arg); } catch (Exception arg2) { Debug.LogError((object)$"[Characters] profile receive handler threw: {arg2}"); } }; } private static byte[] Concat(ICollection<byte[]> fragments) { int num = 0; foreach (byte[] fragment in fragments) { num += fragment.Length; } byte[] array = new byte[num]; int num2 = 0; foreach (byte[] fragment2 in fragments) { Buffer.BlockCopy(fragment2, 0, array, num2, fragment2.Length); num2 += fragment2.Length; } return array; } private static byte[] Compress(byte[] data) { using MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionLevel.Optimal)) { deflateStream.Write(data, 0, data.Length); } return memoryStream.ToArray(); } private static byte[] Decompress(byte[] data) { using MemoryStream stream = new MemoryStream(data); using DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress); using MemoryStream memoryStream = new MemoryStream(); deflateStream.CopyTo(memoryStream); return memoryStream.ToArray(); } private static byte[] Slice(byte[] src, int offset, int len) { int num = Math.Min(len, src.Length - offset); if (num <= 0) { return Array.Empty<byte>(); } byte[] array = new byte[num]; Array.Copy(src, offset, array, 0, num); return array; } } [HarmonyPatch] internal static class CharactersServer { internal class BufferingSocket : ZPlayFabSocket, ISocket { public volatile bool finished; public volatile int versionMatchQueued = -1; public readonly List<ZPackage> Package = new List<ZPackage>(); public readonly ISocket Original; public BufferingSocket(ISocket original) { Original = original; } public bool IsConnected() { return Original.IsConnected(); } public ZPackage Recv() { return Original.Recv(); } public int GetSendQueueSize() { return Original.GetSendQueueSize(); } public int GetCurrentSendRate() { return Original.GetCurrentSendRate(); } public bool IsHost() { return Original.IsHost(); } public void Dispose() { Original.Dispose(); } public bool GotNewData() { return Original.GotNewData(); } public void Close() { Original.Close(); } public string GetEndPointString() { return Original.GetEndPointString(); } public void GetAndResetStats(out int totalSent, out int totalRecv) { Original.GetAndResetStats(ref totalSent, ref totalRecv); } public void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { Original.GetConnectionQuality(ref localQuality, ref remoteQuality, ref ping, ref outByteSec, ref inByteSec); } public ISocket Accept() { return Original.Accept(); } public int GetHostPort() { return Original.GetHostPort(); } public bool Flush() { return Original.Flush(); } public string GetHostName() { return Original.GetHostName(); } public void VersionMatch() { if (finished) { Original.VersionMatch(); } else { versionMatchQueued = Package.Count; } } public void Send(ZPackage pkg) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown int pos = pkg.GetPos(); pkg.SetPos(0); int num = pkg.ReadInt(); if (!finished && (num == StringExtensionMethods.GetStableHashCode("PeerInfo") || num == StringExtensionMethods.GetStableHashCode("RoutedRPC") || num == StringExtensionMethods.GetStableHashCode("ZDOData"))) { ZPackage val = new ZPackage(pkg.GetArray()); val.SetPos(pos); Package.Add(val); } else { pkg.SetPos(pos); Original.Send(pkg); } } } internal const string RpcLogin = "VA_CharLogin"; internal const string RpcSave = "VA_CharSave"; internal const string RpcSaved = "VA_CharSaved"; internal const int NotSaved = 0; private static readonly Dictionary<ZNetPeer, (string id, string name)> _peerProfile = new Dictionary<ZNetPeer, (string, string)>(); private static bool Active { get { if (CharactersConfig.Enabled != null) { return CharactersConfig.Enabled.Value; } return true; } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] [HarmonyPostfix] private static void OnNewConnection_Postfix(ZNet __instance, ZNetPeer peer) { if (Active && __instance.IsServer()) { peer.m_rpc.Register<ZPackage>("VA_CharSave", CharacterProfileTransport.Receiver(OnServerReceivedProfile)); CharactersEmergencyBackup.EnsureServerKey(); peer.m_rpc.Register<ZPackage>("VA_CharSig", (Action<ZRpc, ZPackage>)CharactersEmergencyBackup.OnServerReceiveSignature); CharactersEmergencyBackup.SendKeyToPeer(peer); } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPriority(600)] [HarmonyPrefix] private static void RPC_PeerInfo_Prefix(ref BufferingSocket __state, ZNet __instance, ZRpc rpc) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) if (Active && __instance.IsServer()) { __state = new BufferingSocket(rpc.GetSocket()); rpc.m_socket = (ISocket)(object)__state; ZNetPeer peer = ZNet.instance.GetPeer(rpc); if (peer != null && (int)ZNet.m_onlineBackend != 0) { peer.m_socket = (ISocket)(object)__state; } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPostfix] private static void RPC_PeerInfo_Postfix(BufferingSocket __state, ZNet __instance, ZRpc rpc) { if (Active && __instance.IsServer() && __state != null) { ZNetPeer peer = __instance.GetPeer(rpc); if (peer == null) { Flush(__state, rpc, null); return; } string text = PlayerId(peer.m_socket.GetHostName()); string text2 = peer.m_playerName?.ToLower(); _peerProfile[peer] = (text, text2); ((MonoBehaviour)__instance).StartCoroutine(SendLoginProfile(__state, rpc, peer, text, text2)); } } private static IEnumerator SendLoginProfile(BufferingSocket state, ZRpc rpc, ZNetPeer peer, string steamId, string name) { float pendingWait = 0f; while (CharacterStore.HasPendingSave(steamId) && pendingWait < 10f) { pendingWait += Time.unscaledDeltaTime; yield return null; } if (peer.m_uid != 0L) { bool flag = (CharactersConfig.SingleCharacterAdminBypass?.Value ?? false) && CharactersEnforcement.IsAdmin(peer.m_socket.GetHostName()); if ((CharactersConfig.SingleCharacterMode?.Value ?? false) && !flag) { string canonicalCharacterName = CharacterStore.GetCanonicalCharacterName(steamId); if (canonicalCharacterName != null && !string.Equals(canonicalCharacterName, name, StringComparison.OrdinalIgnoreCase)) { FiresConnectReason.Send(peer.m_rpc, "One character per player on this server", (IEnumerable<string>)new string[2] { "You already have a character here: '" + canonicalCharacterName + "'.", "Reconnect with that character to play." }); peer.m_rpc.Invoke("Error", new object[1] { 845979243 }); Debug.Log((object)("[Characters] single-char: kicked " + steamId + "/" + name + " — canonical character is '" + canonicalCharacterName + "'.")); Flush(state, rpc, peer); yield break; } } byte[] payload = CharacterStore.LoadProfileBytes(steamId, name); bool done = false; CharacterProfileTransport.SendToPeer(peer, "VA_CharLogin", payload, delegate { done = true; }); float waited = 0f; while (!done && waited < CharactersConfig.NoProgressTimeoutSeconds) { waited += Time.deltaTime; yield return null; } } Flush(state, rpc, peer); } private static void OnServerReceivedProfile(ZRpc rpc, byte[] bytes) { if (bytes == null || bytes.Length == 0 || (Object)(object)ZNet.instance == (Object)null) { return; } ZNetPeer peer = ZNet.instance.GetPeer(rpc); string steamId; string name; if (peer != null && _peerProfile.TryGetValue(peer, out (string, string) value)) { (steamId, name) = value; } else { if (peer == null) { Debug.LogWarning((object)$"[Characters] a profile ({bytes.Length}B) arrived from a connection that is no longer a peer; not saved."); return; } steamId = PlayerId(peer.m_socket.GetHostName()); name = peer.m_playerName?.ToLower(); } Stopwatch timer = Stopwatch.StartNew(); int length = bytes.Length; long keyTime = CharactersEmergencyBackup.PeerKeyTime(rpc); double mainMs = -1.0; bool num = CharacterStore.SaveProfileBytesInBackground(steamId, name, bytes, delegate(bool saved, double diskMs) { double num2 = ((mainMs >= 0.0) ? mainMs : timer.Elapsed.TotalMilliseconds); if (saved) { Debug.Log((object)$"[Characters] saved profile for {steamId}/{name} ({length}B) in {timer.ElapsedMilliseconds} ms: main thread {num2:0.0} ms, disk thread {diskMs:0.0} ms (file + backup)."); } else { Debug.LogWarning((object)$"[Characters] profile for {steamId}/{name} ({length}B) was NOT saved: writing it failed (see above)."); } Acknowledge(rpc, saved ? length : 0); }); mainMs = timer.Elapsed.TotalMilliseconds; if (num) { CharactersEmergencyBackup.NoteAcceptedUpload(keyTime, steamId, name); CharactersShutdownPull.NoteReceived(rpc, steamId, name, length); } if (!num) { Debug.LogWarning((object)$"[Characters] profile for {steamId}/{name} ({length}B) was NOT saved: it did not load as a player profile."); Acknowledge(rpc, 0); } } private static void Acknowledge(ZRpc rpc, int result) { if (((rpc != null) ? rpc.GetSocket() : null) != null && rpc.GetSocket().IsConnected()) { rpc.Invoke("VA_CharSaved", new object[1] { result }); } } [HarmonyPatch(typeof(ZNet), "Disconnect")] [HarmonyPostfix] private static void Disconnect_Postfix(ZNetPeer peer) { if (peer != null) { _peerProfile.Remove(peer); } if (peer != null) { CharactersEmergencyBackup.ForgetPeer(peer.m_rpc); } } private static void Flush(BufferingSocket state, ZRpc rpc, ZNetPeer peer) { if (rpc.GetSocket() is BufferingSocket) { rpc.m_socket = state.Original; if (peer != null) { peer.m_socket = state.Original; } } state.finished = true; for (int i = 0; i < state.Package.Count; i++) { if (i == state.versionMatchQueued) { state.Original.VersionMatch(); } state.Original.Send(state.Package[i]); } if (state.Package.Count == state.versionMatchQueued) { state.Original.VersionMatch(); } } private static string PlayerId(string host) { if (string.IsNullOrEmpty(host) || !Regex.IsMatch(host, "^\\d+$")) { return host; } return "Steam_" + host; } } [HarmonyPatch] internal static class CharactersClient { [CompilerGenerated] private static class <>O { public static Method <0>__OnServerAsksSave; public static Func<bool> <1>__WantsToQuit; public static Action<ZRpc, byte[]> <2>__OnClientReceivedProfile; public static Action<ZRpc, int> <3>__OnServerSettledSave; public static Action<ZRpc, ZPackage> <4>__OnClientReceiveKey; } private const string SavingMessage = "Saving your character on the server..."; private static readonly MethodInfo s_continueLogout = AccessTools.Method(typeof(Game), "ContinueLogout", (Type[])null, (Type[])null); private static Game s_holdingFor; private static Game s_heldFor; private static int s_uploadsSent; private static int s_uploadsSettled; private static bool s_uploadConfirmed; private static bool s_holdUploaded; private static bool s_earlyQuitLogged; private static readonly Stopwatch s_holdClock = new Stopwatch(); private static bool s_quitHookAdded; private static bool s_quitPending; private static bool s_quitReleased; internal static bool IsServerCharacter; private static bool Active { get { if (CharactersConfig.Enabled != null) { return CharactersConfig.Enabled.Value; } return true; } } [HarmonyPatch(typeof(Game), "Logout")] [HarmonyPrefix] private static void Game_Logout_Prefix() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Invalid comparison between Unknown and I4 try { ConnectionStatus connectionStatus = ZNet.GetConnectionStatus(); if ((int)connectionStatus != 2 && (int)connectionStatus != 1) { CharactersEmergencyBackup.MarkEmergencyBackupPending(); } } catch { } } [HarmonyPatch(typeof(Game), "ContinueLogout")] [HarmonyPrefix] [HarmonyPriority(800)] private static bool Game_ContinueLogout_Prefix(Game __instance, bool save, bool shouldExit, bool changeToStartScene) { if ((Object)(object)s_holdingFor == (Object)(object)__instance) { return false; } if ((Object)(object)s_heldFor == (Object)(object)__instance || !save || !ServerLinkUp()) { return true; } s_holdingFor = __instance; s_holdUploaded = (s_earlyQuitLogged = false); s_holdClock.Restart(); ((MonoBehaviour)__instance).StartCoroutine(HoldLogout(__instance, save, shouldExit, changeToStartScene)); return false; } private static bool ServerLinkUp() { if (!Active || !IsServerCharacter || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return false; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null && serverPeer.IsReady() && serverPeer.m_socket != null) { return serverPeer.m_socket.IsConnected(); } return false; } private static IEnumerator HoldLogout(Game game, bool save, bool shouldExit, bool changeToStartScene) { yield return SaveAndWait(game, "logout"); s_holdingFor = null; s_heldFor = game; if ((Object)(object)game != (Object)null) { s_continueLogout.Invoke(game, new object[3] { save, shouldExit, changeToStartScene }); } } [HarmonyPatch(typeof(Game), "ContinueLogout")] [HarmonyPostfix] private static void Game_ContinueLogout_Postfix(Game __instance) { if (s_quitPending && __instance.IsShuttingDown()) { s_quitPending = false; s_quitReleased = true; Application.Quit(); } } private static bool WantsToQuit() { Game instance = Game.instance; if (s_quitReleased || (Object)(object)instance == (Object)null || instance.IsShuttingDown()) { return true; } if ((Object)(object)s_holdingFor != (Object)(object)instance && !ServerLinkUp()) { return true; } QuitAfterLogout(instance); return false; } [HarmonyPatch(typeof(Menu), "QuitGame")] [HarmonyPrefix] private static bool Menu_QuitGame_Prefix() { Game instance = Game.instance; if (s_quitReleased || (Object)(object)instance == (Object)null || instance.IsShuttingDown()) { return true; } if ((Object)(object)s_holdingFor != (Object)(object)instance && !ServerLinkUp()) { return true; } QuitAfterLogout(instance); return false; } private static void QuitAfterLogout(Game game) { s_quitPending = true; if ((Object)(object)s_holdingFor != (Object)(object)game) { game.Logout(true, false); } } [HarmonyPatch(typeof(Game), "OnApplicationQuit")] [HarmonyPrefix] [HarmonyPriority(800)] private static bool Game_OnApplicationQuit_Prefix(Game __instance) { if (s_quitReleased || __instance.IsShuttingDown() || (Object)(object)s_holdingFor != (Object)(object)__instance) { return true; } s_quitPending = true; if (s_earlyQuitLogged) { return false; } s_earlyQuitLogged = true; try { __instance.SavePlayerProfile(true, false); } catch (Exception ex) { Debug.LogWarning((object)("[Characters] local save at the early OnApplicationQuit failed: " + ex.Message)); } Debug.LogWarning((object)($"[Characters] OnApplicationQuit arrived during the held logout ({s_holdClock.ElapsedMilliseconds} ms into the hold): " + "vanilla's shutdown skipped, the profile saved to disk, the logout finishes it and then quits.")); return false; } private static IEnumerator SaveAndWait(Game game, string leaving) { Stopwatch timer = Stopwatch.StartNew(); int awaited = s_uploadsSent + 1; game.SavePlayerProfile(true, false); bool uploaded = (s_holdUploaded = s_uploadsSent >= awaited); if (uploaded && (Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, "Saving your character on the server...", 0, (Sprite)null, false, true); } while (uploaded && s_uploadsSettled < awaited && ServerLinkUp() && timer.Elapsed.TotalSeconds < (double)CharactersConfig.LogoutSaveWaitSeconds) { yield return null; } s_uploadConfirmed = uploaded && s_uploadsSettled >= awaited; string arg = ((!uploaded) ? "nothing to upload" : (s_uploadConfirmed ? "the server answered" : (ServerLinkUp() ? $"no answer within {CharactersConfig.LogoutSaveWaitSeconds:0} s" : "the connection was lost"))); Debug.Log((object)$"[Characters] {leaving} held {timer.ElapsedMilliseconds} ms for the character save: {arg}."); } private static void OnServerSettledSave(ZRpc rpc, int length) { s_uploadsSettled++; if (length == 0) { Debug.LogWarning((object)"[Characters] the server did NOT save the uploaded profile: it did not load there as a player profile."); } else { Debug.Log((object)$"[Characters] the server saved the uploaded profile ({length}B)."); } } private static bool ShouldEnforceServerCharacter() { ConfigEntry<bool> forceServerCharacter = CharactersConfig.ForceServerCharacter; if (forceServerCharacter == null || !forceServerCharacter.Value) { return false; } ConfigEntry<bool> forceServerCharacterAdminBypass = CharactersConfig.ForceServerCharacterAdminBypass; if (forceServerCharacterAdminBypass != null && !forceServerCharacterAdminBypass.Value) { return true; } bool flag = false; try { flag = (Object)(object)ZNet.instance != (Object)null && ZNet.instance.LocalPlayerIsAdminOrHost(); } catch { flag = false; } return !flag; } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] [HarmonyPostfix] private static void OnNewConnection_Postfix(ZNet __instance, ZNetPeer peer) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown if (!Active || __instance.IsServer()) { return; } IsServerCharacter = false; s_uploadsSent = (s_uploadsSettled = 0); s_uploadConfirmed = (s_quitPending = (s_earlyQuitLogged = false)); CharactersEmergencyBackup.ClearClientState(); ZRpc rpc = peer.m_rpc; object obj = <>O.<0>__OnServerAsksSave; if (obj == null) { Method val = OnServerAsksSave; <>O.<0>__OnServerAsksSave = val; obj = (object)val; } rpc.Register("VA_SaveNow", (Method)obj); if (!s_quitHookAdded) { s_quitHookAdded = true; Application.wantsToQuit += WantsToQuit; } peer.m_rpc.Register<ZPackage>("VA_CharLogin", CharacterProfileTransport.Receiver(OnClientReceivedProfile)); peer.m_rpc.Register<int>("VA_CharSaved", (Action<ZRpc, int>)OnServerSettledSave); peer.m_rpc.Register<ZPackage>("VA_CharKey", (Action<ZRpc, ZPackage>)CharactersEmergencyBackup.OnClientReceiveKey); try { PlayerProfile val2 = Game.instance?.m_playerProfile; if (val2 != null) { CharactersEmergencyBackup.TrySendStoredEmergencyBackup(peer, val2); } } catch (Exception ex) { Debug.LogWarning((object)("[Characters] emergency-backup send-on-connect failed: " + ex.Message)); } } private static void OnClientReceivedProfile(ZRpc rpc, byte[] bytes) { //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Expected O, but got Unknown if ((Object)(object)Game.instance == (Object)null) { return; } if (bytes == null || bytes.Length == 0) { PlayerProfile playerProfile = Game.instance.m_playerProfile; bool flag = playerProfile?.m_worldData != null && playerProfile.m_worldData.Count != 0; if (ShouldEnforceServerCharacter() && flag) { FiresConnectReason.SetLocalReason("This server requires a fresh character", (IEnumerable<string>)new string[2] { "You're connecting with a character that has already been played elsewhere.", "Create a NEW character before connecting, to avoid loss of data." }); Debug.LogWarning((object)("[Characters] ForceServerCharacter: rejecting '" + playerProfile.GetName() + "' — character has been played elsewhere; asked to create a new one (no data touched).")); AccessTools.Field(typeof(ZNet), "m_connectionStatus").SetValue(null, (object)(ConnectionStatus)5); Game.instance.Logout(true, true); return; } IsServerCharacter = true; if (flag) { Debug.Log((object)"[Characters] server has no stored character but local profile has world data — keeping local character (ForceServerCharacter off or admin-bypassed)."); } if (!flag) { CharactersPlayerTemplate.Mark(); } return; } try { PlayerProfile val = new PlayerProfile(Game.instance.m_playerProfile.m_filename, (FileSource)1); if (!val.LoadFromBytes(bytes)) { Debug.LogError((object)"[Characters] received profile failed to load — keeping local profile."); return; } Game.instance.m_playerProfile = val; IsServerCharacter = true; Debug.Log((object)$"[Characters] adopted server profile '{val.m_playerName}' ({bytes.Length}B)."); CharactersEmergencyBackup.CleanSidecarsIfAny(val); } catch (Exception arg) { Debug.LogError((object)$"[Characters] OnClientReceivedProfile failed: {arg}"); } } private static void OnServerAsksSave(ZRpc rpc) { try { if (Active && IsServerCharacter && !((Object)(object)Game.instance == (Object)null) && !((Object)(object)Player.m_localPlayer == (Object)null) && !Game.instance.IsShuttingDown()) { Debug.Log((object)"[Characters] the server is shutting down: saving the character now"); Game.instance.SavePlayerProfile(false, false); } } catch (Exception ex) { Debug.LogWarning((object)("[Characters] the shutdown save failed: " + ex.Message)); } } [HarmonyPatch(typeof(PlayerProfile), "SavePlayerToDisk")] [HarmonyPostfix] private static void SavePlayerToDisk_Postfix(PlayerProfile __instance) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) if (!Active || !IsServerCharacter) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || instance.IsServer()) { return; } try { ZPackage obj = new PlayerProfile(__instance.m_filename, __instance.m_fileSource).LoadPlayerDataFromDisk(); byte[] array = ((obj != null) ? obj.GetArray() : null); if (array != null && array.Length != 0) { CharactersEmergencyBackup.TryWriteSidecars(__instance, array, s_uploadsSent); ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null && serverPeer.IsReady() && !s_uploadConfirmed && (!((Object)(object)s_holdingFor != (Object)null) || !s_holdUploaded)) { CharacterProfileTransport.SendToPeer(serverPeer, "VA_CharSave", array, null); s_uploadsSent++; } } } catch (Exception ex) { Debug.LogWarning((object)("[Characters] client save upload failed: " + ex.Message)); } } } internal static class CharactersEnforcement { internal const int ErrorSingleChar = 845979243; internal const int ErrorMaintenance = 987345987; internal const string RpcPlayerDied = "VA_CharDied"; internal static bool IsAdmin(string hostName) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown if (string.IsNullOrEmpty(hostName) || (Object)(object)ZNet.instance == (Object)null) { return false; } try { SyncedList val = (SyncedList)AccessTools.Field(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); if (val == null) { return false; } List<string> list = val.GetList(); if (list.Contains(hostName)) { return true; } string item = (hostName.StartsWith("Steam_", StringComparison.Ordinal) ? hostName.Substring(6) : ("Steam_" + hostName)); return list.Contains(item); } catch { return false; } } internal static void DeleteProfile(string steamId, string characterName) { try { string saveDir = CharacterStore.SaveDir; string text = CharacterStore.FileName(steamId, characterName); string[] array = new string[4] { ".fch", ".fch.old", ".fch.signature", ".fch.serverbackup" }; foreach (string text2 in array) { string path = saveDir + text + text2; if (File.Exists(path)) { File.Delete(path); } } Debug.Log((object)("[Characters] hardcore: deleted profile " + text + ".fch (+ siblings).")); } catch (Exception ex) { Debug.LogWarning((object)("[Characters] hardcore delete failed for " + steamId + "/" + characterName + ": " + ex.Message)); } } } [HarmonyPatch] internal static class CharactersMaintenance { private static float _expireAt = -1f; private static void SendMaintenanceReason(ZNetPeer peer) { FiresConnectReason.Send(peer?.m_rpc, "\ud83d\udd27 Server Under Maintenance", (IEnumerable<string>)new string[2] { "The server is currently undergoing maintenance.", "Please stand by - you'll be able to reconnect shortly." }); } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] [HarmonyPostfix] private static void OnNewConnection_Postfix(ZNet __instance, ZNetPeer peer) { if (!__instance.IsServer()) { return; } ConfigEntry<bool> maintenanceMode = CharactersConfig.MaintenanceMode; if (maintenanceMode != null && maintenanceMode.Value) { object obj; if (peer == null) { obj = null; } else { ISocket socket = peer.m_socket; obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (!CharactersEnforcement.IsAdmin(text)) { SendMaintenanceReason(peer); peer.m_rpc.Invoke("Error", new object[1] { 987345987 }); Debug.Log((object)("[Characters] maintenance mode: kicked " + text + " on connect.")); } } } [HarmonyPatch(typeof(ZNet), "Update")] [HarmonyPostfix] private static void ZNet_Update_Postfix(ZNet __instance) { if ((Object)(object)__instance == (Object)null || !__instance.IsServer()) { return; } ConfigEntry<bool> maintenanceMode = CharactersConfig.MaintenanceMode; if (maintenanceMode == null || !maintenanceMode.Value) { _expireAt = -1f; return; } if (_expireAt < 0f) { _expireAt = Time.realtimeSinceStartup + (float)Mathf.Max(10, CharactersConfig.MaintenanceTimerSeconds?.Value ?? 300); } if (Time.realtimeSinceStartup < _expireAt) { return; } foreach (ZNetPeer connectedPeer in __instance.GetConnectedPeers()) { object obj; if (connectedPeer == null) { obj = null; } else { ISocket socket = connectedPeer.m_socket; obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (!string.IsNullOrEmpty(text) && !CharactersEnforcement.IsAdmin(text)) { SendMaintenanceReason(connectedPeer); connectedPeer.m_rpc.Invoke("Error", new object[1] { 987345987 }); } } _expireAt = Time.realtimeSinceStartup + 60f; Debug.Log((object)"[Characters] maintenance timer expired — kicked non-admins."); } } [HarmonyPatch] internal static class CharactersHardcore { [HarmonyPatch(typeof(ZNet), "OnNewConnection")] [HarmonyPostfix] private static void OnNewConnection_Postfix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { peer.m_rpc.Register<ZPackage>("VA_CharDied", (Action<ZRpc, ZPackage>)OnServerReceivedDeath); } } private static void OnServerReceivedDeath(ZRpc rpc, ZPackage _) { ConfigEntry<bool> hardcoreMode = CharactersConfig.HardcoreMode; if (hardcoreMode == null || !hardcoreMode.Value) { return; } ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetPeer(rpc) : null); if (val != null) { string hostName = val.m_socket.GetHostName(); string text = ((!string.IsNullOrEmpty(hostName) && Regex.IsMatch(hostName, "^\\d+$")) ? ("Steam_" + hostName) : hostName); string text2 = val.m_playerName?.ToLower(); if (!string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(text2)) { CharactersEnforcement.DeleteProfile(text, text2); } } } [HarmonyPatch(typeof(Player), "OnDeath")] [HarmonyPostfix] private static void Player_OnDeath_Postfix(Player __instance) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer) { return; } ConfigEntry<bool> hardcoreMode = CharactersConfig.HardcoreMode; if (hardcoreMode == null || !hardcoreMode.Value || !CharactersClient.IsServerCharacter) { return; } ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetServerPeer() : null); if (val != null && val.IsReady()) { try { val.m_rpc.Invoke("VA_CharDied", new object[1] { (object)new ZPackage() }); } catch (Exception ex) { Debug.LogWarning((object)("[Characters] hardcore RPC failed: " + ex.Message)); } if ((Object)(object)Game.instance != (Object)null) { Game.instance.Logout(true, true); } } } } [HarmonyPatch] internal static class CharactersAfkKick { private static float _lastInputAt; private static bool _kicked; private static Vector3 _lastMouse; [HarmonyPatch(typeof(Player), "SetLocalPlayer")] [HarmonyPostfix] private static void Player_SetLocalPlayer_Postfix() { _lastInputAt = Time.realtimeSinceStartup; _kicked = false; } [HarmonyPatch(typeof(Game), "Update")] [HarmonyPostfix] private static void Game_Update_Postfix() { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) if (_kicked || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)Game.instance == (Object)null) { return; } int num = CharactersConfig.AfkKickMinutes?.Value ?? 0; if (num <= 0) { return; } if (Input.anyKeyDown || Input.mousePosition != _lastMouse) { _lastInputAt = Time.realtimeSinceStartup; } _lastMouse = Input.mousePosition; if (Time.realtimeSinceStartup - _lastInputAt < (float)num * 60f) { return; } ConfigEntry<bool> excludeAdminsFromAfk = CharactersConfig.ExcludeAdminsFromAfk; if (excludeAdminsFromAfk != null && excludeAdminsFromAfk.Value) { ZNet instance = ZNet.instance; object hostName; if (instance == null) { hostName = null; } else { ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer == null) { hostName = null; } else { ISocket socket = serverPeer.m_socket; hostName = ((socket != null) ? socket.GetHostName() : null); } } if (CharactersEnforcement.IsAdmin((string)hostName)) { return; } } _kicked = true; Debug.Log((object)"[Characters] AFK kick: logging out local player."); Game.instance.Logout(true, true); } } public static class CharactersEmergencyBackup { internal const string RpcKeyExchange = "VA_CharKey"; internal const string RpcCheckSig = "VA_CharSig"; internal const string EmergencyPendingFlagKey = "VAEmergencyBackupPending"; private static byte[] _clientKey; private static long _clientKeyTime; private static bool _emergencyBackupPending; private static readonly Dictionary<ZRpc, long> s_peerKeyTime = new Dictionary<ZRpc, long>(); internal static long PeerKeyTime(ZRpc rpc) { if (rpc == null || !s_peerKeyTime.TryGetValue(rpc, out var value)) { return 0L; } return value; } internal static void NoteAcceptedUpload(long keyTime, string steamId, string charName) { if (keyTime == 0L || string.IsNullOrEmpty(steamId) || string.IsNullOrEmpty(charName)) { return; } try { string path = SessionPath(steamId, charName); ReadSession(path, out var keyTime2, out var accepted); File.WriteAllText(path, string.Format(arg1: (keyTime2 != keyTime) ? 1 : (accepted + 1), format: "{0} {1}", arg0: keyTime)); } catch (Exception ex) { Debug.LogWarning((object)("[Characters] emergency-backup: couldn't note the upload for " + steamId + "/" + charName + ": " + ex.Message)); } } internal static void ForgetPeer(ZRpc rpc) { if (rpc != null) { s_peerKeyTime.Remove(rpc); } } private static string SessionPath(string steamId, string charName) { return CharacterStore.SaveDir + CharacterStore.FileName(steamId, charName) + ".fch.session"; } private static bool ReadSession(string path, out long keyTime, out int accepted) { keyTime = 0L; accepted = 0; if (!File.Exists(path)) { return false; } string[] array = File.ReadAllText(path).Trim().Split(new char[1] { ' ' }); if (array.Length == 2 && long.TryParse(array[0], out keyTime)) { return int.TryParse(array[1], out accepted); } return false; } public static void EnsureServerKey() { if (string.IsNullOrEmpty(CharactersConfig.ServerKey?.Value)) { byte[] array = new byte[32]; using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create()) { randomNumberGenerator.GetBytes(array); } CharactersConfig.ServerKey.Value = Convert.ToBase64String(array); if (FiresLogger.VerboseEnabled) { Debug.Log((object)"[Characters] emergency-backup: generated server master key."); } } } public static void SendKeyToPeer(ZNetPeer peer) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown if (peer?.m_rpc == null) { return; } try { long ticks = DateTime.Now.Ticks; byte[] array = DeriveKey(ticks); if (array != null) { ZPackage val = new ZPackage(); val.Write(array); val.Write(ticks); peer.m_rpc.Invoke("VA_CharKey", new object[1] { val }); s_peerKeyTime[peer.m_rpc] = ticks; } } catch (Exception ex) { Debug.LogWarning((object)("[Characters] SendKeyToPeer failed: " + ex.Message)); } } public static void OnServerReceiveSignature(ZRpc rpc, ZPackage outerPkg) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown try { byte[] array = outerPkg.ReadByteArray(); byte[] signatureZpkgBytes = outerPkg.ReadByteArray(); int sentCount; bool counted; long num = VerifySignature(array, signatureZpkgBytes, out sentCount, out counted); if (num <= 0) { ISocket socket = rpc.m_socket; Debug.Log((object)("[Characters] emergency-backup: invalid signature from " + ((socket != null) ? socket.GetHostName() : null) + " — skipping.")); return; } PlayerProfile val = new PlayerProfile((string)null, (FileSource)2); if (!val.LoadFromBytes(array)) { Debug.Log((object)"[Characters] emergency-backup: profile bytes failed to decode — skipping."); return; } ISocket socket2 = rpc.m_socket; string text = ((socket2 != null) ? socket2.GetHostName() : null); string text2 = ((!string.IsNullOrEmpty(text) && Regex.IsMatch(text, "^\\d+$")) ? ("Steam_" + text) : text); string text3 = val.m_playerName?.ToLower(); if (string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(text3)) { return; } FileInfo fileInfo = new FileInfo(CharacterStore.SaveDir + CharacterStore.FileName(text2, text3) + ".fch"); if (!fileInfo.Exists) { Debug.Log((object)("[Characters] emergency-backup: no existing .fch for " + text2 + "/" + text3 + " — skipping restore.")); return; } string text4; if (counted && ReadSession(SessionPath(text2, text3), out var keyTime, out var accepted)) { if (num < keyTime) { Debug.Log((object)("[Characters] emergency-backup: " + text2 + "/" + text3 + "'s backup is from an earlier session than its last save — skipping.")); return; } if (num == keyTime && sentCount < accepted) { Debug.Log((object)$"[Characters] emergency-backup: {text2}/{text3}'s backup is older than the last save of that session (sent {sentCount}, the server took {accepted}) — skipping."); return; } text4 = ((num > keyTime) ? "a later session than its last save" : $"after the last save of that session (sent {sentCount}, the server took {accepted})"); } else { if (new DateTime(num) <= fileInfo.LastWriteTime) { Debug.Log((object)$"[Characters] emergency-backup: signature is stale (sig {new DateTime(num):o} <= current {fileInfo.LastWriteTime:o}) — skipping."); return; } text4 = "its session started after the last save"; } if (CharacterStore.SaveProfileBytes(text2, text3, array)) { Debug.Log((object)$"[Characters] emergency-backup: RESTORED {text2}/{text3} from signed backup ({array.Length}B; {text4})."); } } catch (Exception ex) { Debug.LogWarning((object)("[Characters] OnServerReceiveSignature threw: " + ex.Message)); } } private static byte[] DeriveKey(long time) { string text = CharactersConfig.ServerKey?.Value; if (string.IsNullOrEmpty(text)) { return null; } using Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(text, BitConverter.GetBytes(time), 1000); return rfc2898DeriveBytes.GetBytes(32); } private static long VerifySignature(byte[] profileData, byte[] signatureZpkgBytes, out int sentCount, out bool counted) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Expected O, but got Unknown sentCount = 0; counted = false; try { ZPackage val = new ZPackage(signatureZpkgBytes); byte[] array = val.ReadByteArray(); byte[] iV = val.ReadByteArray(); long num = val.ReadLong(); if (val.GetPos() < val.Size()) { sentCount = val.ReadInt(); counted = true; } byte[] second = SHA512.Create().ComputeHash(counted ? WithCount(profileData, sentCount) : profileData); byte[] array2 = DeriveKey(num); if (array2 == null) { return 0L; } using Aes aes = Aes.Create(); aes.Key = array2; aes.IV = iV; using ICryptoTransform cryptoTransform = aes.CreateDecryptor(); return cryptoTransform.TransformFinalBlock(array, 0, array.Length).SequenceEqual(second) ? num : 0; } catch { return 0L; } } public static void OnClientReceiveKey(ZRpc rpc, ZPackage pkg) { try { _clientKey = pkg.ReadByteArray(); _clientKeyTime = pkg.ReadLong(); Debug.Log((object)"[Characters] emergency-backup: received server-derived key on this session."); } catch (Exception ex) { Debug.LogWarning((object)("[Characters] OnClientReceiveKey failed: " + ex.Message)); } } public static void MarkEmergencyBackupPending() { _emergencyBackupPending = true; } public static void ClearClientState() { _clientKey = null; _clientKeyTime = 0L; _emergencyBackupPending = false; } public static bool TryWriteSidecars(PlayerProfile profile, byte[] profileBytes, int uploadsSent) { if (profile == null || profileBytes == null || profileBytes.Length == 0) { return false; } if (!_emergencyBackupPending || _clientKey == null) { return false; } try { string text = SidecarBase(profile); File.WriteAllBytes(text + ".fch.serverbackup", profileBytes); File.WriteAllBytes(text + ".fch.signature", BuildSignature(profileBytes, _clientKey, _clientKeyTime, uploadsSent)); Debug.Log((object)$"[Characters] emergency-backup: wrote sidecars for {profile.m_filename} ({profileBytes.Length}B, after {uploadsSent} upload(s) this session)."); return true; } catch (Exception ex) { Debug.LogWarning((object)("[Characters] TryWriteSidecars failed: " + ex.Message)); return false; } } public static void TrySendStoredEmergencyBackup(ZNetPeer serverPeer, PlayerProfile profile) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown if (serverPeer?.m_rpc == null || profile == null) { return; } try { string text = SidecarBase(profile); string path = text + ".fch.signature"; string path2 = text + ".fch.serverbackup"; if (File.Exists(path) && File.Exists(path2)) { byte[] array = File.ReadAllBytes(path2); byte[] array2 = File.ReadAllBytes(path); ZPackage val = new ZPackage(); val.Write(array); val.Write(array2); serverPeer.m_rpc.Invoke("VA_CharSig", new object[1] { val }); Debug.Log((object)("[Characters] emergency-backup: sent stored sidecars for " + profile.m_filename + " to server for verification.")); } } catch (Exception ex) { Debug.LogWarning((object)("[Characters] TrySendStoredEmergencyBackup failed: " + ex.Message))