using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using JG224.ModCore.API; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("PerPlayerSettings")] [assembly: AssemblyDescription("Per-player, server-forced Valheim mod settings.")] [assembly: AssemblyCompany("jg224")] [assembly: AssemblyProduct("PerPlayerSettings")] [assembly: AssemblyFileVersion("0.5.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("0.5.0.0")] namespace PerPlayerSettings; public sealed class EditBudget { private readonly Dictionary> _windows = new Dictionary>(); public bool Accept(long peer, double now) { if (double.IsNaN(now) || double.IsInfinity(now)) { return false; } if (!_windows.TryGetValue(peer, out var value)) { _windows.Add(peer, value = new Queue()); } while (value.Count > 0 && now - value.Peek() >= 1.0) { value.Dequeue(); } if (value.Count >= 20) { return false; } value.Enqueue(now); return true; } public void Retain(ISet peers) { foreach (long item in new List(_windows.Keys)) { if (!peers.Contains(item)) { _windows.Remove(item); } } } public void Clear() { _windows.Clear(); } } internal static class MiniJson { public static object Parse(string json) { int pos = 0; object result = ParseValue(json, ref pos); SkipWhitespace(json, ref pos); if (pos != json.Length) { throw new FormatException("Trailing characters at position " + pos); } return result; } private static object ParseValue(string s, ref int pos) { SkipWhitespace(s, ref pos); if (pos >= s.Length) { throw new FormatException("Unexpected end of JSON"); } switch (s[pos]) { case '{': return ParseObject(s, ref pos); case '[': return ParseArray(s, ref pos); case '"': return ParseString(s, ref pos); case 't': Expect(s, ref pos, "true"); return true; case 'f': Expect(s, ref pos, "false"); return false; case 'n': Expect(s, ref pos, "null"); return null; default: return ParseNumber(s, ref pos); } } private static Dictionary ParseObject(string s, ref int pos) { Dictionary dictionary = new Dictionary(StringComparer.Ordinal); pos++; SkipWhitespace(s, ref pos); if (pos < s.Length && s[pos] == '}') { pos++; return dictionary; } while (true) { SkipWhitespace(s, ref pos); if (pos >= s.Length || s[pos] != '"') { throw new FormatException("Expected object key at position " + pos); } string key = ParseString(s, ref pos); SkipWhitespace(s, ref pos); if (pos >= s.Length || s[pos] != ':') { throw new FormatException("Expected ':' at position " + pos); } pos++; dictionary[key] = ParseValue(s, ref pos); SkipWhitespace(s, ref pos); if (pos >= s.Length) { throw new FormatException("Unterminated object"); } if (s[pos] != ',') { break; } pos++; } if (s[pos] == '}') { pos++; return dictionary; } throw new FormatException("Expected ',' or '}' at position " + pos); } private static List ParseArray(string s, ref int pos) { List list = new List(); pos++; SkipWhitespace(s, ref pos); if (pos < s.Length && s[pos] == ']') { pos++; return list; } while (true) { list.Add(ParseValue(s, ref pos)); SkipWhitespace(s, ref pos); if (pos >= s.Length) { throw new FormatException("Unterminated array"); } if (s[pos] != ',') { break; } pos++; } if (s[pos] == ']') { pos++; return list; } throw new FormatException("Expected ',' or ']' at position " + pos); } private static string ParseString(string s, ref int pos) { pos++; StringBuilder stringBuilder = new StringBuilder(); while (pos < s.Length) { char c = s[pos++]; switch (c) { case '"': return stringBuilder.ToString(); default: stringBuilder.Append(c); continue; case '\\': break; } if (pos >= s.Length) { break; } char c2 = s[pos++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': if (pos + 4 > s.Length) { throw new FormatException("Bad \\u escape at position " + pos); } stringBuilder.Append((char)Convert.ToInt32(s.Substring(pos, 4), 16)); pos += 4; break; default: throw new FormatException("Bad escape '\\" + c2 + "' at position " + pos); } } throw new FormatException("Unterminated string"); } private static object ParseNumber(string s, ref int pos) { int num = pos; while (pos < s.Length && "+-0123456789.eE".IndexOf(s[pos]) >= 0) { pos++; } string text = s.Substring(num, pos - num); if (long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return result; } if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { return result2; } throw new FormatException("Invalid number '" + text + "' at position " + num); } private static void Expect(string s, ref int pos, string literal) { if (string.CompareOrdinal(s, pos, literal, 0, literal.Length) != 0) { throw new FormatException("Invalid literal at position " + pos); } pos += literal.Length; } private static void SkipWhitespace(string s, ref int pos) { while (pos < s.Length && char.IsWhiteSpace(s[pos])) { pos++; } } public static string Serialize(object value) { StringBuilder stringBuilder = new StringBuilder(); WriteValue(stringBuilder, value); return stringBuilder.ToString(); } private static void WriteValue(StringBuilder sb, object value) { if (value == null) { sb.Append("null"); } else if (value is string) { WriteString(sb, (string)value); } else if (value is bool) { sb.Append(((bool)value) ? "true" : "false"); } else if (value is double || value is float) { sb.Append(Convert.ToDouble(value, CultureInfo.InvariantCulture).ToString("R", CultureInfo.InvariantCulture)); } else if (value is Enum) { WriteString(sb, value.ToString()); } else if (value is Dictionary dictionary) { sb.Append('{'); bool flag = true; foreach (KeyValuePair item in dictionary) { if (!flag) { sb.Append(','); } flag = false; WriteString(sb, item.Key); sb.Append(':'); WriteValue(sb, item.Value); } sb.Append('}'); } else if (value is IEnumerable enumerable) { sb.Append('['); bool flag2 = true; foreach (object item2 in enumerable) { if (!flag2) { sb.Append(','); } flag2 = false; WriteValue(sb, item2); } sb.Append(']'); } else if (value is IConvertible) { sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); } else { WriteString(sb, value.ToString()); } } private static void WriteString(StringBuilder sb, string s) { sb.Append('"'); foreach (char c in s) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\b': sb.Append("\\b"); continue; case '\f': sb.Append("\\f"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder = sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { sb.Append(c); } } sb.Append('"'); } } public class PlayerData { public string Name = ""; public bool Preregistered; public Dictionary Settings = new Dictionary(StringComparer.OrdinalIgnoreCase); } public class PlayerStore { private sealed class UnsupportedSchemaException : FormatException { internal UnsupportedSchemaException() : base("Unsupported player store schema version") { } } private const long SchemaVersion = 1L; private readonly string _path; private readonly Dictionary _players = new Dictionary(StringComparer.Ordinal); private readonly List> _audit = new List>(); private bool _batching; public bool HasUnsavedChanges { get; private set; } public bool ReadOnly { get; private set; } public string LastSaveError { get; private set; } public IEnumerable AuditDescriptions => _audit.Select((Dictionary entry) => entry["id"]?.ToString() + " | " + entry["utc"]?.ToString() + " | " + entry["actor"]?.ToString() + " | " + entry["command"]); public IEnumerable> All => _players; public PlayerStore(string path) { _path = path; } public void Load() { _players.Clear(); _audit.Clear(); ReadOnly = false; HasUnsavedChanges = false; LastSaveError = null; if (TryLoadFile(_path, out var players, out var error)) { Adopt(players); LoadAudit(_path); return; } bool flag = File.Exists(_path); if (error is UnsupportedSchemaException) { ReadOnly = true; LastSaveError = error.Message; PerPlayerSettingsPlugin.Log.LogError((object)"Player store was written with an unsupported schema. The primary and backup are preserved; writes are blocked until a compatible plugin is installed."); return; } if (flag && error != null) { string text = _path + ".corrupt-" + DateTime.Now.ToString("yyyyMMdd-HHmmss-fff"); try { File.Copy(_path, text, overwrite: false); PerPlayerSettingsPlugin.Log.LogError((object)("Failed to parse " + _path + " (" + error.Message + "). Copied the damaged file to " + text + ".")); } catch (Exception ex) { PerPlayerSettingsPlugin.Log.LogError((object)("Failed to parse " + _path + " (" + error.Message + ") and could not preserve it: " + ex.Message)); } } string text2 = _path + ".bak"; if (TryLoadFile(text2, out players, out var error2)) { Adopt(players); LoadAudit(text2); try { File.Copy(text2, _path, overwrite: true); } catch (Exception ex2) { ReadOnly = true; PerPlayerSettingsPlugin.Log.LogWarning((object)("Loaded backup " + text2 + " but could not restore the primary file: " + ex2.Message)); } PerPlayerSettingsPlugin.Log.LogWarning((object)("Recovered " + _players.Count + " player record(s) from " + text2 + ".")); } else { if (error2 != null) { PerPlayerSettingsPlugin.Log.LogError((object)("Failed to parse backup " + text2 + " (" + error2.Message + "). Writes are blocked until recovery.")); } else if (flag) { PerPlayerSettingsPlugin.Log.LogError((object)"No usable backup was found. Writes are blocked until recovery."); } ReadOnly = flag || File.Exists(text2); } } private static bool TryLoadFile(string path, out Dictionary players, out Exception error) { players = null; error = null; if (!File.Exists(path)) { return false; } try { Dictionary obj = (MiniJson.Parse(File.ReadAllText(path)) as Dictionary) ?? throw new FormatException("Root is not an object"); if (obj.TryGetValue("schemaVersion", out var value) && (!(value is long) || (long)value != 1)) { throw new UnsupportedSchemaException(); } if (!TryGetObject(obj, "players", out var value2)) { throw new FormatException("Missing 'players' object"); } players = new Dictionary(StringComparer.Ordinal); foreach (KeyValuePair item in value2) { if (!(item.Value is Dictionary fields)) { throw new FormatException("Invalid player record"); } players[item.Key] = ParsePlayer(fields); } return true; } catch (Exception ex) { error = ex; return false; } } private void Adopt(Dictionary players) { _players.Clear(); foreach (KeyValuePair player in players) { _players[player.Key] = player.Value; } } private static PlayerData ParsePlayer(Dictionary fields) { PlayerData playerData = new PlayerData(); if (fields.TryGetValue("name", out var value) && value != null) { if (!(value is string)) { throw new FormatException("Invalid player name"); } playerData.Name = (string)value; } if (fields.TryGetValue("preregistered", out var value2)) { if (!(value2 is bool)) { throw new FormatException("Invalid preregistration marker"); } playerData.Preregistered = (bool)value2; if (playerData.Preregistered && string.IsNullOrWhiteSpace(playerData.Name)) { throw new FormatException("Preregistered player name is empty"); } } if (!TryGetObject(fields, "settings", out var value3)) { throw new FormatException("Invalid player settings"); } foreach (KeyValuePair item in value3) { if (!(item.Value is string)) { throw new FormatException("Invalid setting value"); } playerData.Settings[item.Key] = (string)item.Value; } return playerData; } private static bool TryGetObject(Dictionary parent, string key, out Dictionary value) { value = null; if (!parent.TryGetValue(key, out var value2)) { return false; } value = value2 as Dictionary; return value != null; } public void MarkDirty() { HasUnsavedChanges = true; } public void BeginBatch() { _batching = true; } public void EndBatch() { _batching = false; } private Dictionary PlayersObject() { Dictionary dictionary = new Dictionary(); foreach (KeyValuePair item in _players.OrderBy, string>((KeyValuePair pair) => pair.Key, StringComparer.Ordinal)) { Dictionary dictionary2 = new Dictionary(); dictionary2["name"] = item.Value.Name ?? ""; if (item.Value.Preregistered) { dictionary2["preregistered"] = true; } Dictionary dictionary3 = new Dictionary(); foreach (KeyValuePair item2 in item.Value.Settings.OrderBy, string>((KeyValuePair pair) => pair.Key, StringComparer.Ordinal)) { dictionary3[item2.Key] = item2.Value; } dictionary2["settings"] = dictionary3; dictionary[item.Key] = dictionary2; } return dictionary; } public string Snapshot() { return MiniJson.Serialize(PlayersObject()); } private static string Hash(string text) { using SHA256 sHA = SHA256.Create(); return Convert.ToBase64String(sHA.ComputeHash(Encoding.UTF8.GetBytes(text))); } public void RecordAudit(string before, string actor, string command) { string text = Snapshot(); if (!(before == text)) { Dictionary dictionary = new Dictionary(); dictionary["id"] = Guid.NewGuid().ToString("N"); dictionary["utc"] = DateTime.UtcNow.ToString("O"); dictionary["actor"] = actor; dictionary["command"] = ((command.Length > 256) ? command.Substring(0, 256) : command); dictionary["before"] = before; dictionary["afterHash"] = Hash(text); _audit.Add(dictionary); while (_audit.Count > 32 || _audit.Sum((Dictionary item) => ((string)item["before"]).Length) > 2097152) { _audit.RemoveAt(0); } MarkDirty(); } } public bool Rollback(string id, out string error) { error = null; Dictionary dictionary = _audit.FirstOrDefault((Dictionary item) => string.Equals((string)item["id"], id, StringComparison.Ordinal)); if (ReadOnly || dictionary == null) { error = "Audit entry not found or storage is read-only."; return false; } if ((string)dictionary["afterHash"] != Hash(Snapshot())) { error = "Values changed after this operation; rollback would overwrite newer work."; return false; } if (!(MiniJson.Parse((string)dictionary["before"]) is Dictionary dictionary2)) { error = "Audit snapshot is invalid."; return false; } Dictionary dictionary3 = new Dictionary(StringComparer.Ordinal); foreach (KeyValuePair item in dictionary2) { if (!(item.Value is Dictionary dictionary4) || !TryGetObject(dictionary4, "settings", out var _)) { error = "Audit snapshot is invalid."; return false; } try { dictionary3[item.Key] = ParsePlayer(dictionary4); } catch (FormatException) { error = "Audit snapshot is invalid."; return false; } } Adopt(dictionary3); MarkDirty(); return true; } private void LoadAudit(string path) { try { if (!(MiniJson.Parse(File.ReadAllText(path)) is Dictionary dictionary) || !dictionary.TryGetValue("audit", out var value) || !(value is List)) { return; } foreach (object item in ((List)value).Take(32)) { Dictionary entry = item as Dictionary; if (entry != null && !new string[6] { "id", "utc", "actor", "command", "before", "afterHash" }.Any((string key) => !entry.ContainsKey(key) || !(entry[key] is string))) { if (((string)entry["before"]).Length <= 2097152) { _audit.Add(entry); } while (_audit.Sum((Dictionary item) => ((string)item["before"]).Length) > 2097152) { _audit.RemoveAt(0); } } } } catch (Exception ex) { PerPlayerSettingsPlugin.Log.LogWarning((object)("Audit history could not be read: " + ex.Message)); } } public bool Save() { MarkDirty(); if (ReadOnly) { LastSaveError = "Storage recovery is required before writes are allowed."; return false; } if (_batching) { return false; } string text = null; try { string value = MiniJson.Serialize(new Dictionary { ["schemaVersion"] = 1L, ["players"] = PlayersObject(), ["audit"] = _audit.Cast().ToList() }); string directoryName = Path.GetDirectoryName(_path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } text = _path + ".tmp-" + Guid.NewGuid().ToString("N"); using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { using StreamWriter streamWriter = new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); streamWriter.Write(value); streamWriter.Flush(); fileStream.Flush(flushToDisk: true); } if (File.Exists(_path)) { File.Replace(text, _path, _path + ".bak", ignoreMetadataErrors: true); } else { File.Move(text, _path); } text = null; HasUnsavedChanges = false; LastSaveError = null; return true; } catch (Exception ex) { PerPlayerSettingsPlugin.Log.LogError((object)("Failed to save " + _path + ": " + ex.Message)); LastSaveError = ex.Message; return false; } finally { if (text != null) { try { File.Delete(text); } catch { } } } } public PlayerData Get(string id) { if (!_players.TryGetValue(id, out var value)) { return null; } return value; } public PlayerData GetOrCreate(string id) { if (!_players.TryGetValue(id, out var value)) { value = new PlayerData(); _players[id] = value; } return value; } public bool HasSettings(string id) { PlayerData playerData = Get(id); if (playerData != null) { return playerData.Settings.Count > 0; } return false; } public string FindPreregistered(string name) { string text = null; foreach (KeyValuePair player in _players) { if (player.Value.Preregistered && string.Equals(player.Value.Name, name, StringComparison.OrdinalIgnoreCase)) { if (text != null) { return null; } text = player.Key; } } return text; } public string GetOrCreatePreregistered(string name) { if (ReadOnly) { throw new InvalidOperationException("Player storage is read-only."); } if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("A preregistered player needs a name.", "name"); } string text = FindPreregistered(name); if (text != null) { return text; } if (_players.Values.Any((PlayerData data) => data.Preregistered && string.Equals(data.Name, name, StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException("Multiple preregistrations have this name; remove the ambiguous records by ID first."); } string text2 = "preregistered/" + Guid.NewGuid().ToString("N"); _players.Add(text2, new PlayerData { Name = name, Preregistered = true }); MarkDirty(); return text2; } public bool MergePreregistered(string accountId, string playerName) { if (ReadOnly || string.IsNullOrEmpty(accountId) || string.IsNullOrWhiteSpace(playerName)) { return false; } string text = FindPreregistered(playerName); if (text == null || text == accountId) { return false; } PlayerData playerData = _players[text]; if (playerData.Settings.Count == 0) { return false; } PlayerData orCreate = GetOrCreate(accountId); if (orCreate.Preregistered) { return false; } foreach (KeyValuePair setting in playerData.Settings) { if (!orCreate.Settings.ContainsKey(setting.Key)) { orCreate.Settings[setting.Key] = setting.Value; } } if (string.IsNullOrEmpty(orCreate.Name)) { orCreate.Name = playerName; } _players.Remove(text); MarkDirty(); return true; } public bool Remove(string id) { return _players.Remove(id); } public string FindByName(string name) { foreach (KeyValuePair player in _players) { if (string.Equals(player.Value.Name, name, StringComparison.OrdinalIgnoreCase)) { return player.Key; } } return null; } } [BepInPlugin("jg224.PerPlayerSettings", "PerPlayerSettings", "0.5.0")] [BepInDependency("com.jg224.modcore", "0.5.0")] public class PerPlayerSettingsPlugin : BaseUnityPlugin { public const string Guid = "jg224.PerPlayerSettings"; public const string Name = "PerPlayerSettings"; public const string Version = "0.5.0"; public const string ModCoreGuid = "com.jg224.modcore"; public const int ProtocolVersion = 2; public static readonly ModuleId ModuleId = new ModuleId("perplayersettings"); internal static PerPlayerSettingsPlugin Instance; internal static ManualLogSource Log; internal static ICoreServices Core; private readonly List _coreRegistrations = new List(); private bool _shutDown; private ConfigEntry _enabled; private ConfigEntry _autoCapture; private ConfigEntry _applyDelaySeconds; private ConfigEntry _watchdogSeconds; private ConfigEntry _syncedSettings; private ConfigEntry _defaultSettings; private ConfigEntry _dataFile; internal SettingRegistry Registry; internal PlayerStore Store; private readonly Dictionary _defaults = new Dictionary(StringComparer.OrdinalIgnoreCase); internal const string RpcApply = "PPS.Apply"; internal const string RpcSet = "PPS.Set"; internal const string RpcQuery = "PPS.Query"; internal const string RpcAdmin = "PPS.Admin"; internal const string RpcMsg = "PPS.Msg"; internal const string RpcStatus = "PPS.Status"; private const string HostPlayerKey = "host"; private const int MaxAdminTokens = 128; private const int MaxRpcEntries = 2048; private const int MaxRpcStringLength = 4096; private ZRoutedRpc _registeredRpc; private readonly HashSet _greetedPeers = new HashSet(); private readonly Dictionary _pendingPush = new Dictionary(); private long _feedbackPeer; private readonly Dictionary _hookedEntries = new Dictionary(); private readonly HashSet _hookedFiles = new HashSet(); private bool _suppressAutoCapture; private float _hookScanTimer; private float _peerScanTimer; private float _watchdogTimer; private bool _hostApplied; private List> _lastPayload; private ZNet _sessionZNet; private long _sessionServerUid; private readonly EditBudget _editBudget = new EditBudget(); private readonly HashSet _pendingSavePeers = new HashSet(); private readonly Dictionary _pushSequences = new Dictionary(); private readonly Dictionary _applicationStatus = new Dictionary(); private List _adminFeedback; private readonly HashSet _adminPushPeers = new HashSet(); private float _saveDue; private float _saveStarted; private float _saveRetryNotBefore; private long _nextPushSequence; private string _clientApplicationStatus = "No settings received."; private int _lastAppliedCount; private int _lastRejectedCount; private string _lastApplyError; private void Awake() { //IL_0056: 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_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Expected O, but got Unknown //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01b5: Expected O, but got Unknown //IL_02ac: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log = ((BaseUnityPlugin)this).Logger; if (!ModCoreApi.IsAvailable) { throw new InvalidOperationException("ModCore did not initialize before PerPlayerSettings."); } Core = ModCoreApi.Services; SemanticVersion val = default(SemanticVersion); if (!SemanticVersion.TryParse("0.5.0", ref val)) { throw new InvalidOperationException("PerPlayerSettings has invalid release version metadata."); } _coreRegistrations.Add(Core.Modules.Register(new ModuleDescriptor(ModuleId, "jg224.PerPlayerSettings", "PerPlayerSettings", val, 2, (ModuleSide)3, (ModuleRequirement)4, 0uL, 1, 1))); _coreRegistrations.Add(Core.Namespaces.Register(ModuleId, (NamespaceKind)6, "PPS.", 1, Array.Empty())); _coreRegistrations.Add(RoutedRpcIngress.Register(ModuleId, new string[6] { "PPS.Apply", "PPS.Set", "PPS.Query", "PPS.Admin", "PPS.Msg", "PPS.Status" })); _coreRegistrations.Add(Core.Namespaces.Register(ModuleId, (NamespaceKind)4, "jg224.PerPlayerSettings", 1, Array.Empty())); _enabled = ((BaseUnityPlugin)this).Config.Bind("1. General", "Enabled", true, "Master switch for the whole plugin."); _applyDelaySeconds = ((BaseUnityPlugin)this).Config.Bind("1. General", "ApplyDelaySeconds", 2f, new ConfigDescription("Seconds after a player joins before their settings are pushed to them. Small delay so client mod loading settles first.", (AcceptableValueBase)(object)new AcceptableValueRange(0.5f, 30f), Array.Empty())); _watchdogSeconds = ((BaseUnityPlugin)this).Config.Bind("1. General", "WatchdogSeconds", 10f, new ConfigDescription("How often (seconds) the client re-asserts the stored values while playing. Managed changes made through the config manager are captured automatically; unmanaged or rejected ones get reverted. 0 disables.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 120f), Array.Empty())); _autoCapture = ((BaseUnityPlugin)this).Config.Bind("1. General", "AutoCapturePlayerChanges", true, "Server side: when a player changes a managed, player-settable setting through their own config manager (F1) during gameplay, save it as their personal value automatically. false = players must use /ppset explicitly, and config-manager edits get reverted."); _syncedSettings = ((BaseUnityPlugin)this).Config.Bind("2. Settings registry", "SyncedSettings", "", "Managed settings, one per line: alias|ModGuid|Section|Key|playersCanSetOwn.\nEmpty by default - add settings with: perplayer add | = \nAliases are internal only - players and admins use the plain setting names. playersCanSetOwn: true = the player may change it themselves."); _defaultSettings = ((BaseUnityPlugin)this).Config.Bind("2. Settings registry", "DefaultSettings", "", "Optional server defaults for players with no stored value, one per line: alias=value. Example: separatedodge=true. Player's own stored value always wins over these."); _dataFile = ((BaseUnityPlugin)this).Config.Bind("3. Storage", "PlayerDataFile", "jg224.PerPlayerSettings.players.json", "File name (inside BepInEx/config) where the server stores per-player values."); Registry = SettingRegistry.Parse(_syncedSettings.Value); ParseDefaults(_defaultSettings.Value); Store = new PlayerStore(Path.Combine(Paths.ConfigPath, _dataFile.Value)); Store.Load(); RegisterCommands(); Core.Modules.SetState(ModuleId, (ModuleRuntimeState)2, "Required server-authoritative per-player settings protocol ready."); Log.LogInfo((object)("PerPlayerSettings 0.5.0 loaded. Managing " + Registry.Defs.Count + " setting(s).")); } private void ParseDefaults(string text) { _defaults.Clear(); if (string.IsNullOrEmpty(text)) { return; } string[] array = text.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim().TrimEnd(new char[1] { '\r' }); if (text2.Length != 0 && !text2.StartsWith("#", StringComparison.Ordinal)) { int num = text2.IndexOf('='); if (num <= 0) { Log.LogWarning((object)("Ignoring malformed DefaultSettings line: " + text2)); } else { _defaults[text2.Substring(0, num).Trim()] = text2.Substring(num + 1).Trim(); } } } } private void RegisterCommands() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown //IL_0030: Expected O, but got Unknown //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_0061: Expected O, but got Unknown //IL_005c: Unknown result type (might be due to invalid IL or missing references) new ConsoleCommand("ppset", "set your own per-player settings. Examples: /ppset Keyboard Dodge Button = Mouse2 ; /ppset Keyboard Dodge Jump + Block = false ; /ppset (shows your current settings)", (ConsoleEvent)delegate(ConsoleEventArgs args) { CmdPlayerSet(args); }, false, false, false, false, false, false, new ConsoleOptionsFetcher(PpsetOptions), true, false, false); new ConsoleCommand("perplayer", "manage per-player settings (admin). Examples: perplayer whitelist ZenDragon.ZenCombat | Keyboard Dodge Button ; perplayer add Tiny ZenDragon.ZenCombat | Keyboard Dodge Button = CapsLock ; perplayer Tiny ; perplayer list ; perplayer mods ; perplayer entries [filter] ; perplayer remove ZenDragon.ZenCombat | Keyboard Dodge Button ; perplayer reload", (ConsoleEvent)delegate(ConsoleEventArgs args) { CmdAdmin(args); }, false, false, false, false, false, false, new ConsoleOptionsFetcher(PerplayerOptions), true, false, false); } private List PpsetOptions() { List list = new List(); if (Registry != null) { foreach (SettingDef def in Registry.Defs) { if (!list.Contains(def.Key)) { list.Add(def.Key); } } } return list; } private List PerplayerOptions() { return new List { "whitelist | [admin]", "add | = ", "remove | ", "set \"\" ", "clear [\"\"]", "list", "", "mods", "entries [filter]", "reload" }; } private void CmdPlayerSet(ConsoleEventArgs args) { //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Expected O, but got Unknown //IL_0271: Unknown result type (might be due to invalid IL or missing references) //IL_0278: Expected O, but got Unknown if (_shutDown) { return; } if (!_enabled.Value) { Feedback(args.Context, "PerPlayerSettings is disabled on this machine."); return; } IList args2 = args.Args; if ((Object)(object)ZNet.instance == (Object)null) { Feedback(args.Context, "Not in a world yet."); return; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 1; i < args2.Count; i++) { if (i > 1) { stringBuilder.Append(' '); } stringBuilder.Append(args2[i]); } string text = stringBuilder.ToString(); if (ZNet.instance.IsServer()) { if (ZNet.instance.IsDedicated()) { Feedback(args.Context, "This is the dedicated server console - use: perplayer set \"\" "); } else if (args2.Count < 2) { Feedback(args.Context, "Usage: /ppset = (or just /ppset to see your settings)"); } else { HandleHostSet(text, args); } return; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer == null) { Feedback(args.Context, "Not connected to a server."); return; } if (args2.Count < 2) { ZPackage val = new ZPackage(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeer.m_uid, "PPS.Query", new object[1] { val }); Feedback(args.Context, "Requesting your stored settings from the server..."); return; } int num = text.IndexOf('='); string text2; string text3; if (num >= 0) { text2 = text.Substring(0, num).Trim().Trim(new char[1] { '"' }); text3 = text.Substring(num + 1).Trim().Trim(new char[1] { '"' }); } else { if (args2.Count != 3) { Feedback(args.Context, "Usage: /ppset = (or just /ppset to see your settings)"); return; } text2 = args2[1]; text3 = args2[2]; } if (text2.Length == 0 || text3.Length == 0) { Feedback(args.Context, "Usage: /ppset = "); return; } if (!ResolveSetting(text2, out var def, out var error)) { Feedback(args.Context, error + ". Use /ppset alone to see your settings."); return; } if (!def.PlayerSettable) { Feedback(args.Context, "'" + def.Key + "' can only be changed by a server admin."); return; } string normalized; string text4 = Registry.ValidateOnly(def, text3, out normalized); if (text4 != null) { Feedback(args.Context, "'" + def.Key + "': " + text4); return; } ZPackage val2 = new ZPackage(); val2.Write(def.Alias); val2.Write(text3); val2.Write(0); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeer.m_uid, "PPS.Set", new object[1] { val2 }); } private void HandleHostSet(string joined, ConsoleEventArgs args) { if (_shutDown) { return; } int num = joined.IndexOf('='); string name; string rawValue; if (num >= 0) { name = joined.Substring(0, num).Trim().Trim(new char[1] { '"' }); rawValue = joined.Substring(num + 1).Trim().Trim(new char[1] { '"' }); } else { int num2 = joined.LastIndexOf(' '); if (num2 <= 0 || num2 >= joined.Length - 1) { Feedback(args.Context, "Usage: /ppset = "); return; } name = joined.Substring(0, num2).Trim(); rawValue = joined.Substring(num2 + 1).Trim(); } if (!ResolveSetting(name, out var def, out var error)) { Feedback(args.Context, error); return; } if (!def.PlayerSettable) { Feedback(args.Context, "'" + def.Key + "' can only be changed by a server admin."); return; } string normalized; string text = Registry.ValidateOnly(def, rawValue, out normalized); if (text != null) { Feedback(args.Context, "'" + def.Key + "': " + text); return; } PlayerData orCreate = Store.GetOrCreate("host"); if (Store.ReadOnly) { Feedback(args.Context, "Rejected: the server player store needs recovery."); return; } orCreate.Name = "host"; orCreate.Settings[def.Alias] = normalized; bool flag = Store.Save(); _suppressAutoCapture = true; string text2; try { text2 = Registry.TryApply(def, normalized, silent: false); } finally { _suppressAutoCapture = false; } Feedback(args.Context, (!flag) ? ("Pending persistence; retrying. " + ((text2 == null) ? "Applied locally." : text2)) : ((text2 == null) ? ("Saved " + def.ModGuid + " | \"" + def.Key + " = " + normalized + "\" (applied now)") : ("Saved server-side, but not applied locally: " + text2))); } private void CmdAdmin(ConsoleEventArgs args) { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Expected O, but got Unknown if (_shutDown) { return; } if (!_enabled.Value) { Feedback(args.Context, "PerPlayerSettings is disabled."); } else if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { ZNetPeer val = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetServerPeer() : null); if (val != null && ZRoutedRpc.instance != null) { IList args2 = args.Args; ZPackage val2 = new ZPackage(); val2.Write(args2.Count - 1); for (int i = 1; i < args2.Count; i++) { val2.Write(args2[i]); } ZRoutedRpc.instance.InvokeRoutedRPC(val.m_uid, "PPS.Admin", new object[1] { val2 }); } else { Feedback(args.Context, "perplayer runs on the server; connect to a server first."); } } else { RunAdminCore(args.Args, args.Context, 0L); } } private void RunAdminCore(IList a, Terminal context, long feedbackPeer) { if (_shutDown) { return; } string before = Store.Snapshot(); string actor = ((feedbackPeer == 0L) ? "server console/host" : ResolvePeerId(ZNet.instance.GetPeer(feedbackPeer))); _adminFeedback = new List(); Store.BeginBatch(); try { RunAdminCommand(a, context, feedbackPeer); } finally { Store.RecordAudit(before, actor, JoinArgs(a, 1, a.Count)); Store.EndBatch(); if (Store.HasUnsavedChanges && !Store.Save()) { _saveDue = (_saveRetryNotBefore = Time.unscaledTime + 5f); } List adminFeedback = _adminFeedback; _adminFeedback = null; foreach (long adminPushPeer in _adminPushPeers) { if (Store.HasUnsavedChanges) { _pendingSavePeers.Add(adminPushPeer); } PushToPeer(adminPushPeer); } _adminPushPeers.Clear(); _feedbackPeer = feedbackPeer; foreach (string item in adminFeedback) { Feedback(context, item); } if (Store.HasUnsavedChanges) { Feedback(context, "Pending persistence; retrying: " + Store.LastSaveError); } _feedbackPeer = 0L; } } private void RunAdminCommand(IList a, Terminal context, long feedbackPeer) { if (_shutDown) { return; } _feedbackPeer = feedbackPeer; try { string text = ((a.Count > 1) ? a[1].ToLowerInvariant() : "help"); switch (text) { case "audit": foreach (string auditDescription in Store.AuditDescriptions) { Feedback(context, auditDescription); } Feedback(context, "Rollback a value change with perplayer rollback . Newer edits are never overwritten."); return; case "rollback": { string error = null; if (a.Count != 3 || !Store.Rollback(a[2], out error)) { Feedback(context, (a.Count != 3) ? "Use perplayer rollback ." : error); return; } PushAllToOnline(); if (!ZNet.instance.IsDedicated()) { ApplyPairs(BuildPairs("host"), silent: false); } Feedback(context, "Restored the audited player values; rollback is itself audited."); return; } case "status": Feedback(context, Store.ReadOnly ? "Storage is read-only: restore a valid store/backup and restart." : (Store.HasUnsavedChanges ? ("Persistence pending; retrying: " + Store.LastSaveError) : "All player values are stored on disk.")); { foreach (KeyValuePair item in _applicationStatus) { Feedback(context, "Peer " + item.Key + ": " + item.Value); } return; } } if (Store.ReadOnly) { switch (text) { case "set": case "add": case "clear": Feedback(context, "Rejected: restore the player store before changing values."); return; } } switch (text) { case "help": Feedback(context, "perplayer add | = (quotes optional)"); Feedback(context, "perplayer (show that player's settings; same as get)"); Feedback(context, "perplayer list / get / set \"\" / clear [\"\"]"); Feedback(context, "perplayer whitelist | [admin] (manage it per player)"); Feedback(context, "perplayer remove | (stop managing it)"); Feedback(context, "perplayer reload / mods / entries [filter]"); Feedback(context, " is a stored id, stored name, or the name of an online player."); return; case "list": { int num = 0; foreach (KeyValuePair item2 in Store.All) { PlayerData value = item2.Value; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(item2.Key).Append(" (").Append(string.IsNullOrEmpty(value.Name) ? "?" : value.Name) .Append("): "); bool flag = true; foreach (KeyValuePair setting in value.Settings) { if (!flag) { stringBuilder.Append(", "); } flag = false; stringBuilder.Append(setting.Key).Append("=").Append(setting.Value); } if (flag) { stringBuilder.Append("(no settings)"); } Feedback(context, stringBuilder.ToString()); num++; } Feedback(context, num + " player(s) stored. Managed settings: " + Registry.Keys()); return; } case "mods": Feedback(context, "Loaded plugins (GUID - Name):"); foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { string text2 = ((pluginInfo.Value != null && pluginInfo.Value.Metadata != null) ? pluginInfo.Value.Metadata.Name : "?"); Feedback(context, " " + pluginInfo.Key + " - " + text2); } Feedback(context, "Mods installed only on clients are not listed here; add those from their cfg file."); return; case "entries": { if (a.Count < 3) { Feedback(context, "Usage: perplayer entries [filter text]"); return; } BaseUnityPlugin val2 = FindPlugin(a[2]); if ((Object)(object)val2 == (Object)null) { Feedback(context, "No loaded plugin with GUID '" + a[2] + "'. See: perplayer mods"); return; } string text3 = ((a.Count > 3) ? StripOuterQuotes(JoinArgs(a, 3, a.Count)) : null); int num2 = 0; foreach (KeyValuePair item3 in (IEnumerable>)val2.Config) { if (text3 == null || item3.Key.Section.IndexOf(text3, StringComparison.OrdinalIgnoreCase) >= 0 || item3.Key.Key.IndexOf(text3, StringComparison.OrdinalIgnoreCase) >= 0) { string text4 = ((item3.Value.SettingType != null) ? item3.Value.SettingType.Name : "?"); string text5 = ((item3.Value.BoxedValue != null) ? item3.Value.BoxedValue.ToString() : "?"); Feedback(context, " " + item3.Key.Section + " | " + item3.Key.Key + " | " + text4 + " | " + text5); num2++; } } Feedback(context, num2 + " entr" + ((num2 == 1) ? "y" : "ies") + ". Quick-add one with:"); Feedback(context, " perplayer add " + a[2] + " | \" = \""); return; } case "reload": ((BaseUnityPlugin)this).Config.Reload(); Registry = SettingRegistry.Parse(_syncedSettings.Value); ParseDefaults(_defaultSettings.Value); PushAllToOnline(); Feedback(context, "Reloaded config from disk. Managed settings: " + Registry.Keys()); return; case "whitelist": case "manage": { if (a.Count < 3) { Feedback(context, "Usage: perplayer whitelist | [admin]"); return; } if (!TryParseGuidKey(a, 2, out var modGuid, out var keyName, out var adminOnly)) { Feedback(context, "Usage: perplayer whitelist | [admin]"); return; } BaseUnityPlugin val = FindPlugin(modGuid); if ((Object)(object)val == (Object)null) { Feedback(context, "No loaded plugin '" + modGuid + "'. See: perplayer mods"); return; } List list = SectionsForKey(val, keyName); if (list.Count == 0) { Feedback(context, "'" + modGuid + "' has no setting named '" + keyName + "'. Browse with: perplayer entries " + modGuid); return; } if (list.Count > 1) { Feedback(context, "'" + keyName + "' exists in several sections (" + string.Join(", ", list.ToArray()) + "). Whitelist a specific one via SyncedSettings in jg224.PerPlayerSettings.cfg, then: perplayer reload"); return; } SettingDef settingDef = Registry.FindByKey(modGuid, list[0], keyName); if (settingDef == null) { settingDef = new SettingDef(); settingDef.ModGuid = modGuid; settingDef.Section = list[0]; settingDef.Key = keyName; settingDef.PlayerSettable = !adminOnly; settingDef.Alias = UniqueAlias(keyName); Registry.Add(settingDef); SaveRegistryToConfig(); } else if (settingDef.PlayerSettable == adminOnly) { settingDef.PlayerSettable = !adminOnly; SaveRegistryToConfig(); } PushAllToOnline(); Feedback(context, "Whitelisted: " + modGuid + " | " + keyName + " [" + list[0] + "]" + (settingDef.PlayerSettable ? " - players set their own via F1 or /ppset" : " - admin-only (players cannot change it)")); return; } } string text6 = ((a.Count > 2) ? a[2] : null); string id = ((text6 != null) ? ResolveTargetId(text6) : null); switch (text) { case "add": { bool flag2 = false; for (int j = 2; j < a.Count; j++) { if (a[j].IndexOf('=') >= 0) { flag2 = true; break; } } if (flag2) { CmdAdminQuickAdd(a, context); break; } if (a.Count < 3) { Feedback(context, "Usage: perplayer add | \" = \""); Feedback(context, " or perplayer add \"
\" \"\" [true|false]"); break; } SettingDef def4; if (a[2].IndexOf('|') >= 0) { if (!TryParseDefLine(a[2], out def4, out var error6)) { Feedback(context, error6); break; } } else { List list3 = Retokenize(a, 2); if (list3.Count < 4 || list3.Count > 5) { Feedback(context, "Positional form needs: \"
\" \"\" [true|false]"); break; } def4 = new SettingDef(); def4.Alias = list3[0]; def4.ModGuid = list3[1]; def4.Section = list3[2]; def4.Key = list3[3]; def4.PlayerSettable = true; if (list3.Count == 5 && !bool.TryParse(list3[4], out def4.PlayerSettable)) { Feedback(context, "playersCanSetOwn must be true or false."); break; } if (def4.Alias.Length == 0 || def4.ModGuid.Length == 0 || def4.Key.Length == 0) { Feedback(context, "alias, ModGuid and Key cannot be empty."); break; } } if (!Registry.Add(def4)) { Feedback(context, "Alias '" + def4.Alias + "' already exists."); break; } SaveRegistryToConfig(); ConfigEntryBase val3 = Registry.ResolveEntry(def4); PushAllToOnline(); Feedback(context, (val3 != null) ? ("Added '" + def4.Alias + "' -> " + def4.ModGuid + " (" + val3.SettingType.Name + "). Pushed to all online players.") : ("Added '" + def4.Alias + "' -> " + def4.ModGuid + " (target mod not loaded on this machine; values validated on clients). Pushed to all online players.")); break; } case "remove": { if (a.Count < 3) { Feedback(context, "Usage: perplayer remove | "); break; } if (a[2] == "|" || (Object)(object)FindPlugin(a[2]) != (Object)null) { if (!TryParseGuidKey(a, 2, out var modGuid2, out var keyName2, out var _)) { Feedback(context, "Usage: perplayer remove | "); break; } List list2 = Registry.FindByGuidKey(modGuid2, keyName2); if (list2.Count == 0) { Feedback(context, "Nothing whitelisted for " + modGuid2 + " | " + keyName2 + ". Managed: " + Registry.Keys()); break; } foreach (SettingDef item4 in list2) { Registry.Remove(item4.Alias); } SaveRegistryToConfig(); PushAllToOnline(); Feedback(context, "Removed from whitelist: " + modGuid2 + " | " + keyName2 + ". Stored player values are kept but no longer pushed."); break; } StringBuilder stringBuilder2 = new StringBuilder(); for (int i = 2; i < a.Count; i++) { if (!(a[i] == "|")) { if (stringBuilder2.Length > 0) { stringBuilder2.Append(' '); } stringBuilder2.Append(a[i]); } } if (!ResolveSetting(stringBuilder2.ToString(), out var def, out var _) || !Registry.Remove(def.Alias)) { Feedback(context, "No managed setting '" + stringBuilder2?.ToString() + "'. Managed: " + Registry.Keys()); break; } SaveRegistryToConfig(); PushAllToOnline(); Feedback(context, "Removed '" + def.Key + "' from the whitelist. Stored player values are kept but no longer pushed."); break; } case "set": { if (a.Count < 5) { Feedback(context, "Usage: perplayer set \"\" "); break; } if (!TryParseSettingAndValue(a, 3, out var def2, out var rawValue, out var error3)) { Feedback(context, error3 + ". Managed: " + Registry.Keys()); break; } string normalized; string text7 = Registry.ValidateOnly(def2, rawValue, out normalized); if (text7 != null) { Feedback(context, "'" + def2.Alias + "': " + text7); break; } if (!TryResolveWriteTarget(text6, out id, out var error4)) { Feedback(context, error4); break; } PlayerData orCreate = Store.GetOrCreate(id); if (string.IsNullOrEmpty(orCreate.Name)) { orCreate.Name = text6; } orCreate.Settings[def2.Alias] = normalized; Store.Save(); long? num3 = FindOnlineUid(id, text6); if (num3.HasValue) { PushToPeer(num3.Value); Feedback(context, "Saved " + def2.ModGuid + " | \"" + def2.Key + " = " + normalized + "\" for " + text6 + " and pushed it to them."); } else { Feedback(context, "Saved " + def2.ModGuid + " | \"" + def2.Key + " = " + normalized + "\" for " + text6 + " (not online; applies when they join)."); } break; } case "get": if (a.Count < 3) { Feedback(context, "Usage: perplayer get "); } else { ShowPlayer(a[2], ResolveTargetId(a[2]), context); } break; case "clear": { if (a.Count < 3) { Feedback(context, "Usage: perplayer clear [\"\"]"); break; } if (id == null) { Feedback(context, "No stored player matching '" + text6 + "'."); break; } PlayerData playerData = Store.Get(id); if (playerData == null) { Feedback(context, "Nothing stored for " + text6 + "."); break; } if (a.Count >= 4) { string text8 = StripOuterQuotes(JoinArgs(a, 3, a.Count)); SettingDef def3; string error5; string key = (ResolveSetting(text8, out def3, out error5) ? def3.Alias : text8); if (!playerData.Settings.Remove(key)) { Feedback(context, "Nothing stored for '" + text8 + "' on " + text6 + "."); break; } Feedback(context, "Cleared " + text8 + " for " + text6 + ". Server default (if any) applies next join."); } else { playerData.Settings.Clear(); Feedback(context, "Cleared all stored settings for " + text6 + "."); } Store.Save(); long? num4 = FindOnlineUid(id, text6); if (num4.HasValue) { PushToPeer(num4.Value); } break; } default: if (a.Count >= 2) { ShowPlayer(a[1], ResolveTargetId(a[1]), context); } else { Feedback(context, "Unknown subcommand '" + text + "'. See: perplayer help"); } break; } } finally { _feedbackPeer = 0L; } } private void RpcAdminHandler(long sender, ZPackage pkg) { if (_shutDown || !_enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer == null) { return; } bool flag = false; try { flag = ZNet.instance.IsAdmin(peer.m_socket.GetHostName()); } catch (Exception) { flag = false; } if (!flag) { SendMsg(sender, "perplayer: admins only."); return; } try { int num = pkg.ReadInt(); if (num < 0 || num > 128) { SendMsg(sender, "perplayer: rejected malformed command."); Log.LogWarning((object)("Rejected PPS.Admin with " + num + " token(s) from " + sender + ".")); return; } List list = new List(); list.Add("perplayer"); for (int i = 0; i < num; i++) { string text = pkg.ReadString(); if (text == null || text.Length > 4096) { SendMsg(sender, "perplayer: rejected malformed command."); return; } list.Add(text); } RunAdminCore(list, null, sender); } catch (Exception ex2) { Log.LogWarning((object)("Rejected malformed PPS.Admin from " + sender + ": " + ex2.Message)); SendMsg(sender, "perplayer: rejected malformed command."); } } private void CmdAdminQuickAdd(IList a, Terminal context) { if (_shutDown) { return; } List list = new List(); for (int i = 2; i < a.Count; i++) { if (a[i] != "|") { list.Add(a[i]); } } if (list.Count < 3) { Feedback(context, "Usage: perplayer add | = (quotes optional)"); return; } string text = list[0]; string text2 = list[1]; StringBuilder stringBuilder = new StringBuilder(); for (int j = 2; j < list.Count; j++) { if (j > 2) { stringBuilder.Append(' '); } stringBuilder.Append(list[j]); } string text3 = stringBuilder.ToString(); int num = text3.IndexOf('='); if (num <= 0 || num >= text3.Length - 1) { Feedback(context, "The last part must be \" = \"."); return; } string text4 = text3.Substring(0, num).Trim().Trim(new char[1] { '"' }); string rawValue = text3.Substring(num + 1).Trim().Trim(new char[1] { '"' }); BaseUnityPlugin val = FindPlugin(text2); if ((Object)(object)val == (Object)null) { Feedback(context, "No loaded plugin '" + text2 + "'. See: perplayer mods"); return; } List list2 = new List(); foreach (KeyValuePair item in (IEnumerable>)val.Config) { if (string.Equals(item.Key.Key, text4, StringComparison.OrdinalIgnoreCase) && !list2.Contains(item.Key.Section)) { list2.Add(item.Key.Section); } } if (list2.Count == 0) { Feedback(context, "'" + text2 + "' has no setting named '" + text4 + "'. Browse with: perplayer entries " + text2); return; } if (list2.Count > 1) { Feedback(context, "'" + text4 + "' exists in several sections (" + string.Join(", ", list2.ToArray()) + "). Add a specific one via SyncedSettings in jg224.PerPlayerSettings.cfg, then: perplayer reload"); return; } SettingDef settingDef = Registry.FindByKey(text2, list2[0], text4); bool flag = false; if (settingDef == null) { settingDef = new SettingDef(); settingDef.ModGuid = text2; settingDef.Section = list2[0]; settingDef.Key = text4; settingDef.PlayerSettable = true; settingDef.Alias = UniqueAlias(text4); Registry.Add(settingDef); SaveRegistryToConfig(); flag = true; } string normalized; string text5 = Registry.ValidateOnly(settingDef, rawValue, out normalized); if (text5 != null) { if (flag) { Registry.Remove(settingDef.Alias); SaveRegistryToConfig(); } Feedback(context, "'" + text4 + "': " + text5); return; } if (!TryResolveWriteTarget(text, out var id, out var error)) { Feedback(context, error); return; } PlayerData orCreate = Store.GetOrCreate(id); if (string.IsNullOrEmpty(orCreate.Name)) { orCreate.Name = text; } orCreate.Settings[settingDef.Alias] = normalized; Store.Save(); if (flag) { PushAllToOnline(); } long? num2 = FindOnlineUid(id, text); if (num2.HasValue) { PushToPeer(num2.Value); Feedback(context, "Saved for " + text + ": " + text2 + " | \"" + text4 + " = " + normalized + "\"" + (flag ? " (newly managed)" : "") + " and pushed to them."); } else { Feedback(context, "Saved for " + text + ": " + text2 + " | \"" + text4 + " = " + normalized + "\"" + (flag ? " (newly managed)" : "") + ". Not online; applies when they join."); } } private void ShowPlayer(string playerArg, string id, Terminal context) { if (playerArg == null || id == null) { Feedback(context, "No stored player matching '" + (playerArg ?? "?") + "'."); return; } PlayerData playerData = Store.Get(id); if (playerData == null || playerData.Settings.Count == 0) { Feedback(context, playerArg + " has no stored settings (defaults, if any, still apply)."); return; } Feedback(context, playerArg + " (" + id + "):"); foreach (KeyValuePair setting in playerData.Settings) { SettingDef settingDef = Registry.Find(setting.Key); if (settingDef != null) { Feedback(context, " " + settingDef.ModGuid + " | \"" + settingDef.Key + " = " + setting.Value + "\" [" + settingDef.Section + "]"); } else { Feedback(context, " (unregistered) " + setting.Key + " = " + setting.Value); } } } private string UniqueAlias(string keyName) { StringBuilder stringBuilder = new StringBuilder(); string text = keyName.ToLowerInvariant(); foreach (char c in text) { if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { stringBuilder.Append(c); } } string text2 = ((stringBuilder.Length > 0) ? stringBuilder.ToString() : "setting"); string text3 = text2; int num = 2; while (Registry.Find(text3) != null) { text3 = text2 + num; num++; } return text3; } private string ResolveTargetId(string playerArg) { if (playerArg.StartsWith("name:", StringComparison.OrdinalIgnoreCase)) { return Store.FindPreregistered(playerArg.Substring(5).Trim()); } if (Store.Get(playerArg) != null) { return playerArg; } string text = Store.FindByName(playerArg); if (text != null) { return text; } ZNetPeer val = FindOnlinePeer(playerArg); if (val == null) { return null; } return ResolvePeerId(val); } private bool TryResolveWriteTarget(string playerArg, out string id, out string error) { id = null; error = null; if (!playerArg.StartsWith("name:", StringComparison.OrdinalIgnoreCase)) { id = ResolveTargetId(playerArg) ?? playerArg; return true; } string text = playerArg.Substring(5).Trim(); if (text.Length == 0) { error = "Use name: with a nonempty display name for explicit offline preregistration."; return false; } try { id = Store.GetOrCreatePreregistered(text); return true; } catch (InvalidOperationException ex) { error = ex.Message; return false; } } private long? FindOnlineUid(string id, string playerArg) { ZNetPeer val = FindOnlinePeer(playerArg); if (val == null) { foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer != null && ResolvePeerId(peer) == id) { return peer.m_uid; } } return null; } return val.m_uid; } private ZNetPeer FindOnlinePeer(string playerArg) { foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer != null) { if (string.Equals(peer.m_playerName, playerArg, StringComparison.OrdinalIgnoreCase)) { return peer; } if (ResolvePeerId(peer) == playerArg) { return peer; } } } return null; } private static bool TryParseGuidKey(IList a, int start, out string modGuid, out string keyName, out bool adminOnly) { modGuid = null; keyName = null; adminOnly = false; List list = new List(); for (int i = start; i < a.Count; i++) { if (a[i] != "|") { list.Add(a[i]); } } if (list.Count >= 2) { string a2 = list[list.Count - 1]; if (string.Equals(a2, "admin", StringComparison.OrdinalIgnoreCase) || string.Equals(a2, "false", StringComparison.OrdinalIgnoreCase)) { adminOnly = true; list.RemoveAt(list.Count - 1); } } if (list.Count < 2) { return false; } modGuid = list[0]; StringBuilder stringBuilder = new StringBuilder(); for (int j = 1; j < list.Count; j++) { if (j > 1) { stringBuilder.Append(' '); } stringBuilder.Append(list[j]); } keyName = stringBuilder.ToString().Trim().Trim(new char[1] { '"' }); return keyName.Length > 0; } private bool TryParseSettingAndValue(IList args, int start, out SettingDef def, out string rawValue, out string error) { def = null; rawValue = null; error = null; if (args == null || start < 0 || start >= args.Count - 1) { error = "Expected = "; return false; } string text = JoinArgs(args, start, args.Count); int num = text.IndexOf('='); if (num >= 0) { string name = StripOuterQuotes(text.Substring(0, num)); rawValue = StripOuterQuotes(text.Substring(num + 1)); if (rawValue.Length == 0) { error = "Setting value cannot be empty"; return false; } return ResolveSetting(name, out def, out error); } string text2 = null; int num2 = args.Count - 1; while (num2 > start) { string name2 = StripOuterQuotes(JoinArgs(args, start, num2)); if (!ResolveSetting(name2, out var def2, out var error2)) { text2 = error2; num2--; continue; } string text3 = StripOuterQuotes(JoinArgs(args, num2, args.Count)); if (text3.Length == 0) { error = "Setting value cannot be empty"; return false; } def = def2; rawValue = text3; return true; } error = text2 ?? "Unknown setting"; return false; } private static string JoinArgs(IList args, int start, int end) { StringBuilder stringBuilder = new StringBuilder(); for (int i = start; i < end && i < args.Count; i++) { if (stringBuilder.Length > 0) { stringBuilder.Append(' '); } stringBuilder.Append(args[i]); } return stringBuilder.ToString(); } private static string StripOuterQuotes(string text) { string text2 = (text ?? "").Trim(); if (text2.Length >= 2 && ((text2[0] == '"' && text2[text2.Length - 1] == '"') || (text2[0] == '\'' && text2[text2.Length - 1] == '\''))) { text2 = text2.Substring(1, text2.Length - 2).Trim(); } return text2; } private static List Retokenize(IList args, int start) { string text = JoinArgs(args, start, args.Count); List list = new List(); StringBuilder stringBuilder = new StringBuilder(); char c = '\0'; foreach (char c2 in text) { if (c != 0) { if (c2 == c) { c = '\0'; } else { stringBuilder.Append(c2); } } else if (c2 == '"' || c2 == '\'') { c = c2; } else if (char.IsWhiteSpace(c2)) { if (stringBuilder.Length > 0) { list.Add(stringBuilder.ToString()); stringBuilder.Length = 0; } } else { stringBuilder.Append(c2); } } if (c != 0) { return new List(); } if (stringBuilder.Length > 0) { list.Add(stringBuilder.ToString()); } return list; } private static List SectionsForKey(BaseUnityPlugin plugin, string keyName) { List list = new List(); foreach (KeyValuePair item in (IEnumerable>)plugin.Config) { if (string.Equals(item.Key.Key, keyName, StringComparison.OrdinalIgnoreCase) && !list.Contains(item.Key.Section)) { list.Add(item.Key.Section); } } return list; } private static BaseUnityPlugin FindPlugin(string modGuid) { foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { if (string.Equals(pluginInfo.Key, modGuid, StringComparison.OrdinalIgnoreCase)) { return (pluginInfo.Value != null) ? pluginInfo.Value.Instance : null; } } return null; } private static bool TryParseDefLine(string line, out SettingDef def, out string error) { def = null; error = null; string[] array = (line ?? "").Split(new char[1] { '|' }); if (array.Length < 4) { error = "Need alias|ModGuid|Section|Key[|playersCanSetOwn] (quote the whole thing)."; return false; } SettingDef settingDef = new SettingDef(); settingDef.Alias = array[0].Trim(); settingDef.ModGuid = array[1].Trim(); settingDef.Section = array[2].Trim(); settingDef.Key = array[3].Trim(); settingDef.PlayerSettable = true; if (array.Length >= 5 && !bool.TryParse(array[4].Trim(), out settingDef.PlayerSettable)) { error = "playersCanSetOwn must be true or false."; return false; } if (settingDef.Alias.Length == 0 || settingDef.ModGuid.Length == 0 || settingDef.Key.Length == 0) { error = "alias, ModGuid and Key cannot be empty."; return false; } def = settingDef; return true; } private void SaveRegistryToConfig() { List list = Registry.ToLines(); _syncedSettings.Value = ((list.Count > 0) ? string.Join("\n", list.ToArray()) : ""); ((BaseUnityPlugin)this).Config.Save(); } private void Feedback(Terminal context, string message) { if (_adminFeedback != null) { _adminFeedback.Add(message); return; } if (Store != null && Store.HasUnsavedChanges && message.StartsWith("Saved", StringComparison.Ordinal)) { message = "Pending persistence (retrying)" + message.Substring(5); } if ((Object)(object)context != (Object)null) { context.AddString("[PPS] " + message); } if (_feedbackPeer != 0L) { SendMsg(_feedbackPeer, message); } else { ShowToast(message); } } private static void ShowToast(string message) { try { if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsDedicated() && (Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)1, message, 0, (Sprite)null, false, true); } } catch (Exception) { } } private void Update() { if (_shutDown || !_enabled.Value) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { if (_sessionZNet != null) { ResetNetworkSessionState(); } _sessionZNet = null; _sessionServerUid = 0L; _hostApplied = false; return; } long num = ((!instance.IsServer()) ? instance.GetServerPeer() : null)?.m_uid ?? 0; bool num2 = _sessionZNet != instance; bool flag = !instance.IsServer() && _sessionServerUid != 0L && num == 0; bool flag2 = !instance.IsServer() && _sessionServerUid != 0L && num != 0L && _sessionServerUid != num; if (num2 || flag || flag2) { ResetNetworkSessionState(); _sessionZNet = instance; } _sessionServerUid = num; EnsureRpcRegistered(); if (ZRoutedRpc.instance != null) { _hookScanTimer -= Time.deltaTime; if (_hookScanTimer <= 0f) { _hookScanTimer = 5f; EnsureHooks(); } ServerUpdate(); ClientWatchdog(); } } private void ResetNetworkSessionState() { _lastPayload = null; _watchdogTimer = 0f; _greetedPeers.Clear(); _pendingPush.Clear(); _editBudget.Clear(); _pendingSavePeers.Clear(); _adminPushPeers.Clear(); _pushSequences.Clear(); _applicationStatus.Clear(); _adminFeedback = null; _saveDue = (_saveStarted = (_saveRetryNotBefore = 0f)); _clientApplicationStatus = "No settings received."; _feedbackPeer = 0L; _hookedEntries.Clear(); Registry = SettingRegistry.Parse(_syncedSettings.Value); } private void EnsureHooks() { if (Registry == null) { return; } foreach (SettingDef def in Registry.Defs) { ConfigEntryBase val = Registry.ResolveEntry(def); if (val != null) { if (!_hookedEntries.ContainsKey(val)) { _hookedEntries[val] = def.Alias; } ConfigFile val2 = OwnerConfig(def.ModGuid); if (val2 != null && !_hookedFiles.Contains(val2)) { _hookedFiles.Add(val2); val2.SettingChanged += OnConfigFileSettingChanged; } } } } private ConfigFile OwnerConfig(string modGuid) { foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { if (string.Equals(pluginInfo.Key, modGuid, StringComparison.OrdinalIgnoreCase)) { return (pluginInfo.Value != null && (Object)(object)pluginInfo.Value.Instance != (Object)null) ? pluginInfo.Value.Instance.Config : null; } } return null; } private void OnConfigFileSettingChanged(object sender, SettingChangedEventArgs e) { //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Expected O, but got Unknown if (_shutDown || _suppressAutoCapture || !_enabled.Value || !_autoCapture.Value) { return; } ConfigEntryBase val = ((e != null) ? e.ChangedSetting : null); if (val == null || val.BoxedValue == null || !_hookedEntries.TryGetValue(val, out var value)) { return; } SettingDef settingDef = Registry.Find(value); if (settingDef == null || !settingDef.PlayerSettable || (Object)(object)ZNet.instance == (Object)null) { return; } string text = Convert.ToString(val.BoxedValue, CultureInfo.InvariantCulture); if (ZNet.instance.IsServer()) { if (!ZNet.instance.IsDedicated()) { if (Store.ReadOnly) { ShowToast("PerPlayer: rejected local change; server storage needs recovery."); return; } PlayerData orCreate = Store.GetOrCreate("host"); orCreate.Name = "host"; orCreate.Settings[settingDef.Alias] = text; ScheduleSave(0L); } return; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null) { _watchdogTimer = Math.Max(_watchdogTimer, 3f); ZPackage val2 = new ZPackage(); val2.Write(settingDef.Alias); val2.Write(text); val2.Write(1); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeer.m_uid, "PPS.Set", new object[1] { val2 }); } } private void EnsureRpcRegistered() { if (!_shutDown) { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null && _registeredRpc != instance) { instance.Register("PPS.Apply", (Action)RpcApplyHandler); instance.Register("PPS.Set", (Action)RpcSetHandler); instance.Register("PPS.Query", (Action)RpcQueryHandler); instance.Register("PPS.Admin", (Action)RpcAdminHandler); instance.Register("PPS.Msg", (Action)RpcMsgHandler); instance.Register("PPS.Status", (Action)RpcStatusHandler); _registeredRpc = instance; _greetedPeers.Clear(); _pendingPush.Clear(); Log.LogInfo((object)"Routed RPCs registered."); } } } private void ServerUpdate() { if (_shutDown || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } if (Store.HasUnsavedChanges && !Store.ReadOnly && Time.unscaledTime >= _saveDue) { bool flag = Store.Save(); _saveRetryNotBefore = (flag ? 0f : (Time.unscaledTime + 5f)); _saveDue = _saveRetryNotBefore; _saveStarted = 0f; foreach (long pendingSavePeer in _pendingSavePeers) { if (pendingSavePeer == 0L) { ShowToast(flag ? "PerPlayer: stored your changes." : "PerPlayer: persistence failed; retrying."); } else if (ZNet.instance.GetPeer(pendingSavePeer) != null) { SendMsg(pendingSavePeer, flag ? "Stored on server; awaiting client application." : "Pending persistence; server will retry."); if (flag) { PushToPeer(pendingSavePeer); } } } if (flag) { _pendingSavePeers.Clear(); } } if (!ZNet.instance.IsDedicated() && !_hostApplied) { _hostApplied = true; List> list = BuildPairs("host"); if (list.Count > 0) { ApplyPairs(list, silent: false); } } _peerScanTimer -= Time.deltaTime; if (_peerScanTimer <= 0f) { _peerScanTimer = 1f; HashSet currentUids = new HashSet(); foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer == null) { continue; } currentUids.Add(peer.m_uid); string text = ResolvePeerId(peer); if (!Store.ReadOnly && !string.IsNullOrEmpty(peer.m_playerName)) { PlayerData orCreate = Store.GetOrCreate(text); if (!string.Equals(orCreate.Name, peer.m_playerName, StringComparison.Ordinal)) { orCreate.Name = peer.m_playerName; ScheduleSave(0L); } if (MergePreregistered(text, peer.m_playerName)) { _pendingPush[peer.m_uid] = Time.time + 0.5f; } } if (_greetedPeers.Add(peer.m_uid) && (Registry.Defs.Count > 0 || Store.HasSettings(text) || _defaults.Count > 0)) { _pendingPush[peer.m_uid] = Time.time + _applyDelaySeconds.Value; } } _greetedPeers.RemoveWhere((long uid) => !currentUids.Contains(uid)); _editBudget.Retain(currentUids); foreach (long item in new List(_applicationStatus.Keys)) { if (!currentUids.Contains(item)) { _applicationStatus.Remove(item); _pushSequences.Remove(item); } } _pendingSavePeers.RemoveWhere((long uid) => uid != 0L && !currentUids.Contains(uid)); } if (_pendingPush.Count == 0) { return; } List list2 = null; foreach (KeyValuePair item2 in _pendingPush) { if (!(item2.Value > Time.time)) { if (list2 == null) { list2 = new List(); } list2.Add(item2.Key); } } if (list2 == null) { return; } foreach (long item3 in list2) { _pendingPush.Remove(item3); PushToPeer(item3); } } private bool MergePreregistered(string realId, string playerName) { if (!Store.MergePreregistered(realId, playerName)) { return false; } ScheduleSave(0L); Log.LogInfo((object)("Applied explicitly pre-registered settings for '" + playerName + "' to id " + realId + ".")); return true; } private void ClientWatchdog() { if (!_shutDown && _lastPayload != null && !(_watchdogSeconds.Value <= 0f) && !((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer()) { _watchdogTimer -= Time.deltaTime; if (!(_watchdogTimer > 0f)) { _watchdogTimer = _watchdogSeconds.Value; ApplyPairs(_lastPayload, silent: true); } } } private void RpcSetHandler(long sender, ZPackage pkg) { if (_shutDown || !_enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer == null || !_editBudget.Accept(sender, Time.unscaledTime) || pkg == null || pkg.Size() > 32768) { return; } if (Store.ReadOnly) { SendMsg(sender, "Rejected: server storage needs recovery."); return; } string text; string text2; int num; try { text = pkg.ReadString(); text2 = pkg.ReadString(); num = pkg.ReadInt(); if (pkg.GetPos() != pkg.Size()) { throw new FormatException("unexpected trailing data"); } if (text == null || text2 == null || text.Length > 4096 || text2.Length > 4096 || (num != 0 && num != 1)) { throw new FormatException("invalid alias, value, or source"); } } catch (Exception ex) { Log.LogWarning((object)("Rejected malformed PPS.Set from " + sender + ": " + ex.Message)); SendMsg(sender, "PerPlayer: rejected malformed setting update."); return; } SettingDef settingDef = Registry.Find(text); if (settingDef == null) { SendMsg(sender, "'" + text + "' is not a known setting on this server."); return; } if (!settingDef.PlayerSettable) { SendMsg(sender, "'" + settingDef.Alias + "' can only be changed by a server admin."); return; } string normalized; string text3 = Registry.ValidateOnly(settingDef, text2, out normalized); if (text3 != null) { SendMsg(sender, "'" + settingDef.Alias + "': " + text3); return; } if (num == 1 && !_autoCapture.Value) { SendMsg(sender, "Auto-captured changes are disabled on this server. Use /ppset = instead."); PushToPeer(sender); return; } string text4 = ResolvePeerId(peer); PlayerData orCreate = Store.GetOrCreate(text4); if (orCreate.Settings.TryGetValue(settingDef.Alias, out var value) && value == normalized) { if (num == 0) { SendMsg(sender, Store.HasUnsavedChanges ? "Unchanged; persistence pending." : "Unchanged; value is stored."); } return; } if (!string.IsNullOrEmpty(peer.m_playerName)) { orCreate.Name = peer.m_playerName; } orCreate.Settings[settingDef.Alias] = normalized; ScheduleSave(sender); if (num == 0) { SendMsg(sender, "Queued for storage; server will confirm persistence and application separately."); } Log.LogInfo((object)("Player " + (string.IsNullOrEmpty(orCreate.Name) ? text4 : orCreate.Name) + " set " + settingDef.Alias + " = " + normalized)); } private void ScheduleSave(long peer) { Store.MarkDirty(); _pendingSavePeers.Add(peer); float unscaledTime = Time.unscaledTime; if (_saveStarted == 0f) { _saveStarted = unscaledTime; } _saveDue = Math.Max(_saveRetryNotBefore, Math.Min(unscaledTime + 0.5f, _saveStarted + 2f)); } private void RpcStatusHandler(long sender, ZPackage pkg) { if (_shutDown || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZNet.instance.GetPeer(sender) == null || pkg == null || pkg.Size() > 2048) { return; } try { long num = pkg.ReadLong(); int num2 = pkg.ReadInt(); int num3 = pkg.ReadInt(); string text = pkg.ReadString(); if (_pushSequences.TryGetValue(sender, out var value) && num == value && num2 >= 0 && num3 >= 0 && num2 + num3 <= 2048 && text.Length <= 256 && pkg.GetPos() == pkg.Size()) { _applicationStatus[sender] = "Client reports " + num2 + " applied, " + num3 + " rejected" + ((num3 > 0) ? (": " + text) : "."); _pushSequences.Remove(sender); } } catch (Exception ex) { Log.LogWarning((object)("Rejected application status: " + ex.Message)); } } private void RpcQueryHandler(long sender, ZPackage pkg) { if (_shutDown || !_enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer != null) { string text = ResolvePeerId(peer); PlayerData playerData = Store.Get(text); PushToPeer(sender); if (playerData == null || playerData.Settings.Count == 0) { SendMsg(sender, (_defaults.Count > 0) ? ("You have no personal settings stored. Server defaults: " + DescribePairsFriendly(BuildPairs(text))) : "You have no personal settings stored."); } else { SendMsg(sender, "Your settings: " + DescribePairsFriendly(BuildPairs(text))); } } } private void RpcApplyHandler(long sender, ZPackage pkg) { //IL_0210: Unknown result type (might be due to invalid IL or missing references) //IL_0217: Expected O, but got Unknown if (_shutDown || !_enabled.Value || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer == null || sender != serverPeer.m_uid) { Log.LogWarning((object)("Ignored PPS.Apply from non-server sender " + sender)); return; } try { int num = pkg.ReadInt(); if (num < 0 || num > 2048) { throw new FormatException("invalid registry count " + num); } List list = new List(); for (int i = 0; i < num; i++) { string text = pkg.ReadString(); if (text == null || text.Length > 4096) { throw new FormatException("registry line is too long"); } list.Add(text); } SettingRegistry registry = SettingRegistry.Parse(string.Join("\n", list.ToArray())); int num2 = pkg.ReadInt(); if (num2 < 0 || num2 > 2048) { throw new FormatException("invalid value count " + num2); } List> list2 = new List>(); for (int j = 0; j < num2; j++) { string text2 = pkg.ReadString(); string text3 = pkg.ReadString(); if (text2 == null || text3 == null || text2.Length > 4096 || text3.Length > 4096) { throw new FormatException("setting alias or value is too long"); } list2.Add(new KeyValuePair(text2, text3)); } long num3 = pkg.ReadLong(); bool flag = pkg.ReadBool(); if (pkg.GetPos() != pkg.Size()) { throw new FormatException("unexpected snapshot data"); } Registry = registry; _hookedEntries.Clear(); EnsureHooks(); Log.LogInfo((object)("Adopted server settings registry: " + Registry.Defs.Count + " setting(s).")); _lastPayload = list2; _watchdogTimer = _watchdogSeconds.Value; ApplyPairs(list2, silent: false); ZPackage val = new ZPackage(); val.Write(num3); val.Write(_lastAppliedCount); val.Write(_lastRejectedCount); val.Write(_lastApplyError ?? ""); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "PPS.Status", new object[1] { val }); _clientApplicationStatus = (flag ? "Stored on server. " : "Server persistence pending. ") + _lastAppliedCount + " applied; " + _lastRejectedCount + " rejected."; ShowToast(_clientApplicationStatus); } catch (Exception ex) { Log.LogWarning((object)("Rejected malformed PPS.Apply from server: " + ex.Message)); } } private void RpcMsgHandler(long sender, ZPackage pkg) { if (_shutDown || !_enabled.Value || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer == null || sender != serverPeer.m_uid) { Log.LogWarning((object)("Ignored PPS.Msg from non-server sender " + sender)); return; } try { string text = pkg.ReadString(); if (text == null || text.Length > 4096) { throw new FormatException("message is too long"); } ShowToast(text); } catch (Exception ex) { Log.LogWarning((object)("Rejected malformed PPS.Msg from server: " + ex.Message)); } } private List> BuildPairs(string playerId) { Dictionary dictionary = new Dictionary(_defaults, StringComparer.OrdinalIgnoreCase); PlayerData playerData = Store.Get(playerId); if (playerData != null) { foreach (KeyValuePair setting in playerData.Settings) { dictionary[setting.Key] = setting.Value; } } List> list = new List>(); foreach (KeyValuePair item in dictionary) { SettingDef settingDef = Registry.Find(item.Key); if (settingDef != null) { list.Add(new KeyValuePair(settingDef.Alias, item.Value)); } } return list; } private void PushToPeer(long uid) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Expected O, but got Unknown if (_adminFeedback != null) { _adminPushPeers.Add(uid); } else { if ((Object)(object)ZNet.instance == (Object)null) { return; } ZNetPeer peer = ZNet.instance.GetPeer(uid); if (peer == null) { return; } List> list = BuildPairs(ResolvePeerId(peer)); ZPackage val = new ZPackage(); List list2 = Registry.ToLines(); if (list2.Count > 2048 || list.Count > 2048) { Log.LogError((object)("Cannot push settings snapshot: registry/value count exceeds " + 2048 + ".")); return; } val.Write(list2.Count); foreach (string item in list2) { val.Write(item); } val.Write(list.Count); foreach (KeyValuePair item2 in list) { val.Write(item2.Key); val.Write(item2.Value); } long num = ++_nextPushSequence; _pushSequences[uid] = num; _applicationStatus[uid] = (Store.HasUnsavedChanges ? "Persistence pending; awaiting client result." : "Stored; awaiting client result."); val.Write(num); val.Write(!Store.HasUnsavedChanges); ZRoutedRpc.instance.InvokeRoutedRPC(uid, "PPS.Apply", new object[1] { val }); } } private void PushAllToOnline() { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer != null) { PushToPeer(peer.m_uid); } } } private void SendMsg(long uid, string message) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown if (message == null) { message = ""; } if (message.Length > 4096) { message = message.Substring(0, 4096); } ZPackage val = new ZPackage(); val.Write(message); ZRoutedRpc.instance.InvokeRoutedRPC(uid, "PPS.Msg", new object[1] { val }); } private void ApplyPairs(List> pairs, bool silent) { if (_shutDown) { return; } int num = 0; string text = null; _suppressAutoCapture = true; try { foreach (KeyValuePair pair in pairs) { SettingDef settingDef = Registry.Find(pair.Key); if (settingDef != null) { string text2 = Registry.TryApply(settingDef, pair.Value, silent); if (text2 == null) { num++; } else if (text == null) { text = settingDef.Alias + ": " + text2; } } } } finally { _suppressAutoCapture = false; } _lastAppliedCount = num; _lastRejectedCount = pairs.Count - num; _lastApplyError = ((text == null) ? "" : text.Substring(0, Math.Min(text.Length, 256))); if (!silent) { if (num > 0 && text == null) { ShowToast("PerPlayer: " + num + " setting(s) applied"); } else if (num > 0) { ShowToast("PerPlayer: " + num + " applied; failed: " + text); } else if (text != null) { ShowToast("PerPlayer: " + text); } } } private string DescribePairsFriendly(List> pairs) { StringBuilder stringBuilder = new StringBuilder(); int num = 0; foreach (KeyValuePair pair in pairs) { SettingDef settingDef = Registry.Find(pair.Key); if (settingDef != null) { if (num > 0) { stringBuilder.Append(", "); } stringBuilder.Append(settingDef.ModGuid).Append(" | ").Append(settingDef.Key) .Append(" = ") .Append(pair.Value); num++; } } if (stringBuilder.Length <= 0) { return "(none)"; } return stringBuilder.ToString(); } private bool ResolveSetting(string name, out SettingDef def, out string error) { def = null; error = null; if (string.IsNullOrEmpty(name)) { error = "Empty setting name"; return false; } def = Registry.Find(name); if (def != null) { return true; } List list = Registry.FindAllByKey(name); if (list.Count == 1) { def = list[0]; return true; } if (list.Count > 1) { StringBuilder stringBuilder = new StringBuilder("'" + name + "' exists in several mods: "); for (int i = 0; i < list.Count; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(list[i].ModGuid); } error = stringBuilder.ToString(); return false; } error = "No managed setting '" + name + "'"; return false; } private static string DescribePairs(List> pairs) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < pairs.Count; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(pairs[i].Key).Append("=").Append(pairs[i].Value); } if (stringBuilder.Length <= 0) { return "(none)"; } return stringBuilder.ToString(); } internal static string EndpointId(ZNetPeer peer) { try { if (peer != null && peer.m_socket != null) { string name = ((object)peer.m_socket).GetType().Name; bool flag = string.Equals(name, "ZSocket", StringComparison.Ordinal) || string.Equals(name, "ZSocket2", StringComparison.Ordinal); if (!flag) { string hostName = peer.m_socket.GetHostName(); if (!string.IsNullOrEmpty(hostName) && !string.Equals(hostName, "None", StringComparison.OrdinalIgnoreCase)) { return hostName; } } string endPointString = peer.m_socket.GetEndPointString(); if (!string.IsNullOrEmpty(endPointString) && !string.Equals(endPointString, "None", StringComparison.OrdinalIgnoreCase)) { return flag ? ("endpoint/" + endPointString) : endPointString; } } } catch (Exception) { } if (peer == null) { return "?"; } return peer.m_uid.ToString(); } private static string LegacyEndpointId(ZNetPeer peer) { try { if (peer != null && peer.m_socket != null) { string endPointString = peer.m_socket.GetEndPointString(); if (!string.IsNullOrEmpty(endPointString)) { int num = endPointString.IndexOf(':'); return (num > 0) ? endPointString.Substring(0, num) : endPointString; } } } catch (Exception) { } if (peer == null) { return "?"; } return peer.m_uid.ToString(); } private string ResolvePeerId(ZNetPeer peer) { string text = EndpointId(peer); string text2 = LegacyEndpointId(peer); if (Store.ReadOnly) { return text; } if (string.Equals(text, text2, StringComparison.Ordinal) || !text2.StartsWith("playfab/", StringComparison.OrdinalIgnoreCase)) { return text; } PlayerData playerData = Store.Get(text2); if (playerData == null) { return text; } PlayerData orCreate = Store.GetOrCreate(text); if (string.IsNullOrEmpty(orCreate.Name)) { orCreate.Name = playerData.Name; } foreach (KeyValuePair setting in playerData.Settings) { if (!orCreate.Settings.ContainsKey(setting.Key)) { orCreate.Settings[setting.Key] = setting.Value; } } Store.Remove(text2); Store.Save(); Log.LogInfo((object)("Migrated player settings key '" + text2 + "' -> '" + text + "'.")); return text; } private void OnDestroy() { //IL_00d1: Unknown result type (might be due to invalid IL or missing references) if (_shutDown) { return; } _shutDown = true; foreach (ConfigFile hookedFile in _hookedFiles) { hookedFile.SettingChanged -= OnConfigFileSettingChanged; } _hookedFiles.Clear(); _hookedEntries.Clear(); for (int num = _coreRegistrations.Count - 1; num >= 0; num--) { try { _coreRegistrations[num].Dispose(); } catch (Exception ex) { ManualLogSource logger = ((BaseUnityPlugin)this).Logger; if (logger != null) { logger.LogWarning((object)("Registration cleanup failed: " + ex.Message)); } } } _coreRegistrations.Clear(); if (Core != null) { Core.Metrics.RemoveOwner(ModuleId); } Core = null; } } public class SettingDef { public string Alias; public string ModGuid; public string Section; public string Key; public bool PlayerSettable; } public class SettingRegistry { private const int MaxValueLength = 4096; private readonly List _defs = new List(); public IList Defs => _defs; public static SettingRegistry Parse(string configText) { SettingRegistry settingRegistry = new SettingRegistry(); if (string.IsNullOrEmpty(configText)) { return settingRegistry; } string[] array = configText.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim().TrimEnd(new char[1] { '\r' }); if (text.Length == 0 || text.StartsWith("#", StringComparison.Ordinal)) { continue; } string[] array2 = text.Split(new char[1] { '|' }); if (array2.Length < 4) { PerPlayerSettingsPlugin.Log.LogWarning((object)("Ignoring malformed SyncedSettings line (need alias|ModGuid|Section|Key[|playersCanSetOwn]): " + text)); continue; } SettingDef settingDef = new SettingDef(); settingDef.Alias = array2[0].Trim(); settingDef.ModGuid = array2[1].Trim(); settingDef.Section = array2[2].Trim(); settingDef.Key = array2[3].Trim(); settingDef.PlayerSettable = true; if (array2.Length >= 5 && !bool.TryParse(array2[4].Trim(), out settingDef.PlayerSettable)) { PerPlayerSettingsPlugin.Log.LogWarning((object)("Ignoring SyncedSettings line with invalid playersCanSetOwn value: " + text)); } else if (settingDef.Alias.Length == 0 || settingDef.ModGuid.Length == 0 || settingDef.Key.Length == 0) { PerPlayerSettingsPlugin.Log.LogWarning((object)("Ignoring incomplete SyncedSettings line: " + text)); } else if (settingRegistry.Find(settingDef.Alias) != null) { PerPlayerSettingsPlugin.Log.LogWarning((object)("Ignoring duplicate alias '" + settingDef.Alias + "'.")); } else { settingRegistry._defs.Add(settingDef); } } return settingRegistry; } public SettingDef Find(string alias) { foreach (SettingDef def in _defs) { if (string.Equals(def.Alias, alias, StringComparison.OrdinalIgnoreCase)) { return def; } } return null; } public SettingDef FindByKey(string modGuid, string section, string key) { foreach (SettingDef def in _defs) { if (string.Equals(def.ModGuid, modGuid, StringComparison.OrdinalIgnoreCase) && string.Equals(def.Section, section, StringComparison.OrdinalIgnoreCase) && string.Equals(def.Key, key, StringComparison.OrdinalIgnoreCase)) { return def; } } return null; } public List FindAllByKey(string key) { List list = new List(); foreach (SettingDef def in _defs) { if (string.Equals(def.Key, key, StringComparison.OrdinalIgnoreCase)) { list.Add(def); } } return list; } public string Keys() { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < _defs.Count; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(_defs[i].Key).Append(" (").Append(_defs[i].ModGuid) .Append(")"); if (!_defs[i].PlayerSettable) { stringBuilder.Append(" [admin only]"); } } if (stringBuilder.Length <= 0) { return "(none configured)"; } return stringBuilder.ToString(); } public List FindByGuidKey(string modGuid, string key) { List list = new List(); foreach (SettingDef def in _defs) { if (string.Equals(def.ModGuid, modGuid, StringComparison.OrdinalIgnoreCase) && string.Equals(def.Key, key, StringComparison.OrdinalIgnoreCase)) { list.Add(def); } } return list; } public bool Add(SettingDef def) { if (def == null || Find(def.Alias) != null) { return false; } _defs.Add(def); return true; } public bool Remove(string alias) { SettingDef settingDef = Find(alias); if (settingDef != null) { return _defs.Remove(settingDef); } return false; } public List ToLines() { List list = new List(); foreach (SettingDef def in _defs) { list.Add(def.Alias + "|" + def.ModGuid + "|" + def.Section + "|" + def.Key + "|" + (def.PlayerSettable ? "true" : "false")); } return list; } public string Aliases() { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < _defs.Count; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(_defs[i].Alias); if (!_defs[i].PlayerSettable) { stringBuilder.Append(" (admin only)"); } } if (stringBuilder.Length <= 0) { return "(none configured)"; } return stringBuilder.ToString(); } public ConfigEntryBase ResolveEntry(SettingDef def) { if (def == null || Chainloader.PluginInfos == null) { return null; } BaseUnityPlugin val = null; foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { if (string.Equals(pluginInfo.Key, def.ModGuid, StringComparison.OrdinalIgnoreCase)) { val = ((pluginInfo.Value != null) ? pluginInfo.Value.Instance : null); break; } } if ((Object)(object)val == (Object)null || val.Config == null) { return null; } foreach (KeyValuePair item in (IEnumerable>)val.Config) { if (string.Equals(item.Key.Section, def.Section, StringComparison.OrdinalIgnoreCase) && string.Equals(item.Key.Key, def.Key, StringComparison.OrdinalIgnoreCase)) { return item.Value; } } return null; } public static string TryParseValue(Type settingType, string raw, out object boxed) { //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_01ad: Unknown result type (might be due to invalid IL or missing references) //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_019b: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Unknown result type (might be due to invalid IL or missing references) boxed = null; if (settingType == null) { return "unknown setting type"; } if (settingType == typeof(string)) { boxed = raw; return null; } if (settingType.IsEnum) { try { boxed = Enum.Parse(settingType, raw, ignoreCase: true); if (!Attribute.IsDefined(settingType, typeof(FlagsAttribute)) && !Enum.IsDefined(settingType, boxed)) { boxed = null; return "'" + raw + "' is not a defined " + settingType.Name + " value"; } return null; } catch (ArgumentException) { return "'" + raw + "' is not a valid " + settingType.Name + " (example: " + Enum.GetNames(settingType)[0] + ")"; } } if (settingType == typeof(KeyboardShortcut)) { string text = raw.Trim(); if (text.Length == 0 || string.Equals(text, "None", StringComparison.OrdinalIgnoreCase)) { boxed = KeyboardShortcut.Empty; return null; } string[] array = text.Split(new char[1] { '+' }); KeyCode val = (KeyCode)0; List list = new List(); for (int i = 0; i < array.Length; i++) { if (!Enum.TryParse(array[i].Trim(), ignoreCase: true, out KeyCode result)) { return "'" + array[i].Trim() + "' is not a valid KeyCode"; } if (i == 0) { val = result; } else { list.Add(result); } } try { boxed = ((list.Count > 0) ? ((object)new KeyboardShortcut(val, list.ToArray())) : ((object)new KeyboardShortcut(val, Array.Empty()))); return null; } catch (ArgumentException ex2) { return ex2.Message; } } if (settingType == typeof(bool)) { if (bool.TryParse(raw, out var result2)) { boxed = result2; return null; } return "expected true or false"; } if (settingType == typeof(int)) { if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result3)) { boxed = result3; return null; } return "expected a whole number"; } if (settingType == typeof(long)) { if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result4)) { boxed = result4; return null; } return "expected a whole number"; } if (settingType == typeof(float) || settingType == typeof(double)) { if (double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var result5)) { if (double.IsNaN(result5) || double.IsInfinity(result5)) { return "expected a finite number"; } boxed = ((settingType == typeof(float)) ? ((object)(float)result5) : ((object)result5)); if (settingType == typeof(float) && (float.IsNaN((float)boxed) || float.IsInfinity((float)boxed))) { boxed = null; return "expected a finite number"; } return null; } return "expected a number"; } try { boxed = Convert.ChangeType(raw, settingType, CultureInfo.InvariantCulture); return null; } catch (Exception) { return "cannot convert '" + raw + "' to " + settingType.Name; } } public string TryApply(SettingDef def, string rawValue, bool silent) { ConfigEntryBase val = ResolveEntry(def); if (val == null) { return "mod not installed here (" + def.ModGuid + ") - nothing to apply"; } object boxed; string normalized; string text = TryParseEntryValue(val, rawValue, out boxed, out normalized); if (text != null) { return text; } if (!object.Equals(val.BoxedValue, boxed)) { val.BoxedValue = boxed; } if (!silent) { PerPlayerSettingsPlugin.Log.LogInfo((object)("Applied " + def.Alias + " = " + rawValue)); } return null; } public string ValidateOnly(SettingDef def, string rawValue, out string normalized) { normalized = null; if (rawValue == null) { return "value is missing"; } if (rawValue.Length > 4096) { return "value is too long"; } ConfigEntryBase val = ResolveEntry(def); if (val == null) { if (string.IsNullOrEmpty(rawValue)) { return "value is empty"; } normalized = rawValue.Trim(); return null; } object boxed; return TryParseEntryValue(val, rawValue, out boxed, out normalized); } private static string TryParseEntryValue(ConfigEntryBase entry, string rawValue, out object boxed, out string normalized) { boxed = null; normalized = null; if (rawValue == null) { return "value is missing"; } if (rawValue.Length > 4096) { return "value is too long"; } string text = TryParseValue(entry.SettingType, rawValue, out boxed); if (text != null) { return text; } AcceptableValueBase val = ((entry.Description != null) ? entry.Description.AcceptableValues : null); if (val != null && !val.IsValid(boxed)) { string text2 = val.ToDescriptionString(); if (!string.IsNullOrEmpty(text2)) { return "value is not allowed (" + text2 + ")"; } return "value is outside the allowed range/list"; } normalized = NormalizeValue(boxed); return null; } private static string NormalizeValue(object value) { if (value is float num) { return num.ToString("R", CultureInfo.InvariantCulture); } if (value is double num2) { return num2.ToString("R", CultureInfo.InvariantCulture); } return Convert.ToString(value, CultureInfo.InvariantCulture); } }