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 ModCore v0.5.0
JG224.ModCore.dll
Decompiled 8 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Concurrent; 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 System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JG224.ModCore.API; using JG224.ModCore.Patches; using JG224.ModCore.Runtime; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("JG224.ModCore.CoreTests")] [assembly: InternalsVisibleTo("JG224.ModCore.ApiTests")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("JG224.ModCore")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.5.0.0")] [assembly: AssemblyInformationalVersion("0.5.0")] [assembly: AssemblyProduct("JG224.ModCore")] [assembly: AssemblyTitle("JG224.ModCore")] [assembly: AssemblyVersion("0.5.0.0")] [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 JG224.ModCore { [BepInPlugin("com.jg224.modcore", "ModCore", "0.5.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.jg224.modcore"; public const string PluginName = "ModCore"; public const string PluginVersion = "0.5.0"; private readonly List<IDisposable> _registrations = new List<IDisposable>(); private Harmony _harmony; private CoreServices _services; private CoreRuntime _runtime; private ConfigFile _configuration; private ConfigEntry<bool> _debugLogging; private ConfigEntry<bool> _writeCompatibilityReport; private ConfigEntry<bool> _enforceRequiredModules; private ConfigEntry<int> _maximumPacketBytes; private ConfigEntry<int> _maximumRpcPerSecond; private ConfigEntry<float> _handshakeTimeoutSeconds; private ConfigEntry<int> _maximumDispatcherQueue; private ConfigEntry<string> _exactVersionModules; private ConfigEntry<KeyboardShortcut> _statusShortcut; private readonly StatusOverlay _status = new StatusOverlay(); internal static Plugin Instance { get; private set; } internal static CoreRuntime Runtime { get; private set; } internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; _configuration = ConfigFileMigration.Open((BaseUnityPlugin)(object)this, Paths.ConfigPath, ((BaseUnityPlugin)this).Logger); BindConfiguration(); BepInExLogSink log = new BepInExLogSink(((BaseUnityPlugin)this).Logger, () => _debugLogging.Value); try { _services = new CoreServices(log, _maximumDispatcherQueue.Value, () => _maximumPacketBytes.Value, () => _maximumRpcPerSecond.Value, () => _handshakeTimeoutSeconds.Value, () => _enforceRequiredModules.Value, () => _exactVersionModules.Value); _runtime = new CoreRuntime(_services, log, () => _writeCompatibilityReport.Value); Runtime = _runtime; ModCoreApi.Initialize(_services); RegisterConfigurationMetadata(); RegisterCoreCommand(); _harmony = new Harmony("com.jg224.modcore"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); Game.isModded = true; _runtime.Publish(LifecycleEventKind.CoreReady, this, 0L); ((BaseUnityPlugin)this).Logger.LogInfo((object)"ModCore 0.5.0 loaded. It provides coordination only and changes no gameplay by itself."); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("ModCore failed to initialize safely: " + ex)); Shutdown(); throw; } } private void Update() { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) _runtime?.Tick(); if (_services != null && Object.op_Implicit((Object)(object)Player.m_localPlayer) && !Console.IsVisible() && !TextInput.IsVisible() && ((Object)(object)Chat.instance == (Object)null || !Chat.instance.HasFocus())) { KeyboardShortcut value = _statusShortcut.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { _status.Toggle(_services); } } } private void OnGUI() { _status.Draw(_services); } private void OnDestroy() { Shutdown(); } private void BindConfiguration() { //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Expected O, but got Unknown //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0128: 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_0197: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Expected O, but got Unknown _debugLogging = _configuration.Bind<bool>("Diagnostics", "DebugLogging", false, "Enable verbose Mod Core diagnostics. Normal gameplay should leave this disabled."); _writeCompatibilityReport = _configuration.Bind<bool>("Diagnostics", "WriteCompatibilityReport", true, "Write a redacted compatibility report when a world starts."); _enforceRequiredModules = _configuration.Bind<bool>("Networking", "EnforceRequiredModules", true, "Disconnect a peer when a migrated module marked RequiredOnBoth is missing or protocol-incompatible."); _exactVersionModules = _configuration.Bind<string>("Networking", "ExactVersionModules", "*", "Comma-separated module IDs that must match exact versions. * covers RequiredOnBoth modules; empty uses protocol compatibility only. Applies on the next connection."); _statusShortcut = _configuration.Bind<KeyboardShortcut>("Display", "StatusShortcut", new KeyboardShortcut((KeyCode)291, Array.Empty<KeyCode>()), "Open the read-only, redacted compatibility status panel. Press again to close."); _maximumPacketBytes = _configuration.Bind<int>("Networking", "MaximumPacketBytes", 1048576, new ConfigDescription("Maximum accepted Mod Core envelope size.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(4096, 1048576), Array.Empty<object>())); _maximumRpcPerSecond = _configuration.Bind<int>("Networking", "MaximumRpcPerPeerPerSecond", 120, new ConfigDescription("Per-peer Mod Core message rate limit.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 2000), Array.Empty<object>())); _handshakeTimeoutSeconds = _configuration.Bind<float>("Networking", "HandshakeTimeoutSeconds", 5f, new ConfigDescription("Time allowed for Mod Core capability negotiation.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f), Array.Empty<object>())); _maximumDispatcherQueue = _configuration.Bind<int>("Runtime", "MaximumMainThreadQueue", 4096, new ConfigDescription("Maximum queued cross-thread game actions before new work is rejected.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(64, 100000), Array.Empty<object>())); } private void RegisterConfigurationMetadata() { _registrations.Add(RegisterConfig<bool>(_debugLogging, ConfigScope.Developer)); _registrations.Add(RegisterConfig<bool>(_writeCompatibilityReport, ConfigScope.LocalPreference)); _registrations.Add(RegisterConfig<bool>(_enforceRequiredModules, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig<int>(_maximumPacketBytes, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig<int>(_maximumRpcPerSecond, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig<float>(_handshakeTimeoutSeconds, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig<int>(_maximumDispatcherQueue, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig<string>(_exactVersionModules, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig<KeyboardShortcut>(_statusShortcut, ConfigScope.LocalPreference)); } private IDisposable RegisterConfig<T>(ConfigEntry<T> entry, ConfigScope scope) { return _services.Configuration.Register(new ConfigSettingDescriptor(CoreRuntime.CoreId, ((ConfigEntryBase)entry).Definition.Section, ((ConfigEntryBase)entry).Definition.Key, scope, () => Convert.ToString(entry.Value, CultureInfo.InvariantCulture))); } private void RegisterCoreCommand() { CommandDescriptor descriptor = new CommandDescriptor(CoreRuntime.CoreId, "modcore.report", "Write a fresh redacted ModCore compatibility report.", true, TimeSpan.FromSeconds(2.0)); _registrations.Add(_services.Commands.Register(descriptor, delegate { _runtime.WriteReport(); return CommandResult.Success("Compatibility report written."); })); } private void Shutdown() { _status.Close(); if (Runtime == _runtime) { Runtime = null; } Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _harmony = null; for (int num = _registrations.Count - 1; num >= 0; num--) { _registrations[num].Dispose(); } _registrations.Clear(); if (_services != null) { ModCoreApi.Reset(_services); } _runtime?.Dispose(); _runtime = null; _services = null; _configuration = null; Instance = null; Log = null; } } } namespace JG224.ModCore.Runtime { internal sealed class AtomicStoreFactory : IAtomicStoreFactory { public IAtomicStore Open(ModuleId owner, string rootDirectory, string storeName, int maximumBytes) { return new AtomicStore(owner, rootDirectory, storeName, maximumBytes); } } internal sealed class AtomicStore : IAtomicStore { private const int Magic = 1246186819; private const int FormatVersion = 1; private const int HashBytes = 32; private readonly object _gate = new object(); private readonly int _maximumBytes; public string Path { get; } internal AtomicStore(ModuleId owner, string rootDirectory, string storeName, int maximumBytes) { if (owner.IsEmpty) { throw new ArgumentException("owner"); } if (string.IsNullOrWhiteSpace(rootDirectory)) { throw new ArgumentException("rootDirectory"); } if (string.IsNullOrWhiteSpace(storeName) || storeName.Length > 128 || !string.Equals(System.IO.Path.GetFileName(storeName), storeName, StringComparison.Ordinal) || storeName.IndexOfAny(System.IO.Path.GetInvalidFileNameChars()) >= 0) { throw new ArgumentException("Store name must be one safe file name.", "storeName"); } if (maximumBytes <= 0 || maximumBytes > 67108864) { throw new ArgumentOutOfRangeException("maximumBytes"); } string fullPath = System.IO.Path.GetFullPath(rootDirectory); string fullPath2 = System.IO.Path.GetFullPath(System.IO.Path.Combine(fullPath, owner.Value)); string text = fullPath.TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); char directorySeparatorChar = System.IO.Path.DirectorySeparatorChar; string value = text + directorySeparatorChar; if (!fullPath2.StartsWith(value, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("Resolved module store path escaped its root."); } Path = System.IO.Path.Combine(fullPath2, storeName); _maximumBytes = maximumBytes; } public bool TryLoad(out AtomicStoreRecord record, out string error) { lock (_gate) { if (TryRead(Path, backup: false, out record, out error)) { return true; } string text = error; string path = Path + ".bak"; if (TryRead(path, backup: true, out record, out var error2)) { error = "Primary invalid; recovered backup. Primary: " + text; return true; } error = "Primary: " + text + " Backup: " + error2; return false; } } public void Save(int schemaVersion, long worldOrPlayerId, byte[] payload) { if (schemaVersion <= 0) { throw new ArgumentOutOfRangeException("schemaVersion"); } if (payload == null) { throw new ArgumentNullException("payload"); } if (payload.Length > _maximumBytes) { throw new InvalidOperationException("Store payload exceeds configured limit."); } lock (_gate) { Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Path)); string text = Path + ".tmp." + Guid.NewGuid().ToString("N"); try { byte[] buffer; using (SHA256 sHA = SHA256.Create()) { buffer = sHA.ComputeHash(payload); } using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { using BinaryWriter binaryWriter = new BinaryWriter(fileStream); binaryWriter.Write(1246186819); binaryWriter.Write(1); binaryWriter.Write(schemaVersion); binaryWriter.Write(worldOrPlayerId); binaryWriter.Write(payload.Length); binaryWriter.Write(32); binaryWriter.Write(buffer); binaryWriter.Write(payload); binaryWriter.Flush(); fileStream.Flush(flushToDisk: true); } if (File.Exists(Path)) { AtomicStoreRecord record; string error; string destinationBackupFileName = (TryRead(Path, backup: false, out record, out error) ? (Path + ".bak") : null); File.Replace(text, Path, destinationBackupFileName, ignoreMetadataErrors: true); } else { File.Move(text, Path); } } finally { if (File.Exists(text)) { File.Delete(text); } } } } private bool TryRead(string path, bool backup, out AtomicStoreRecord record, out string error) { record = null; if (!File.Exists(path)) { error = "not found"; return false; } try { long num = (long)_maximumBytes + 128L; FileInfo fileInfo = new FileInfo(path); if (fileInfo.Length <= 0 || fileInfo.Length > num) { throw new InvalidDataException("File size is outside the configured bound."); } using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); using BinaryReader binaryReader = new BinaryReader(fileStream); if (binaryReader.ReadInt32() != 1246186819) { throw new InvalidDataException("Magic does not match."); } if (binaryReader.ReadInt32() != 1) { throw new InvalidDataException("Unsupported store format."); } int num2 = binaryReader.ReadInt32(); long ownerId = binaryReader.ReadInt64(); int num3 = binaryReader.ReadInt32(); int num4 = binaryReader.ReadInt32(); if (num2 <= 0 || num3 < 0 || num3 > _maximumBytes || num4 != 32) { throw new InvalidDataException("Header contains invalid bounds."); } byte[] array = binaryReader.ReadBytes(num4); byte[] array2 = binaryReader.ReadBytes(num3); if (array.Length != num4 || array2.Length != num3 || fileStream.Position != fileStream.Length) { throw new InvalidDataException("Store is truncated or contains trailing data."); } byte[] right; using (SHA256 sHA = SHA256.Create()) { right = sHA.ComputeHash(array2); } if (!FixedTimeEquals(array, right)) { throw new InvalidDataException("Payload hash does not match."); } record = new AtomicStoreRecord(num2, ownerId, array2, backup); error = string.Empty; return true; } catch (Exception ex) { error = ex.GetType().Name + ": " + ex.Message; return false; } } private static bool FixedTimeEquals(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } } internal sealed class AuthoritativePolicyRegistry : IAuthoritativePolicyRegistry { private sealed class Entry { internal PolicyDescriptor Descriptor; internal PolicySnapshot Current; } private readonly object _gate = new object(); private readonly Dictionary<ModuleId, Entry> _entries = new Dictionary<ModuleId, Entry>(); public IDisposable Register(PolicyDescriptor descriptor) { if (descriptor == null) { throw new ArgumentNullException("descriptor"); } Entry entry = new Entry { Descriptor = descriptor }; lock (_gate) { if (_entries.ContainsKey(descriptor.Owner)) { throw new InvalidOperationException("Policy owner already registered: " + descriptor.Owner); } _entries.Add(descriptor.Owner, entry); } return new Registration(delegate { lock (_gate) { if (_entries.TryGetValue(descriptor.Owner, out var value) && value == entry) { _entries.Remove(descriptor.Owner); } } }); } public bool TryApply(ModuleId owner, long revision, byte[] payload, bool authoritative, out PolicySnapshot snapshot, out string error) { snapshot = null; error = string.Empty; if (revision <= 0) { error = "Policy revision must be positive."; return false; } if (payload == null) { error = "Policy payload is missing."; return false; } Entry value; lock (_gate) { if (!_entries.TryGetValue(owner, out value)) { error = "Policy owner is not registered."; return false; } if (payload.Length > value.Descriptor.MaximumBytes) { error = "Policy payload exceeds its registered bound."; return false; } if (value.Current != null && revision <= value.Current.Revision) { error = "Policy revision is stale or duplicated."; snapshot = Clone(value.Current); return false; } } byte[] array = (byte[])payload.Clone(); try { string text = value.Descriptor.Validator(array); if (!string.IsNullOrEmpty(text)) { error = text; lock (_gate) { snapshot = ((value.Current == null) ? null : Clone(value.Current)); } return false; } } catch (Exception ex) { error = "Policy validator failed: " + ex.GetType().Name + ": " + ex.Message; lock (_gate) { snapshot = ((value.Current == null) ? null : Clone(value.Current)); } return false; } string sha; using (SHA256 sHA = SHA256.Create()) { sha = BitConverter.ToString(sHA.ComputeHash(array)).Replace("-", string.Empty).ToLowerInvariant(); } PolicySnapshot policySnapshot = new PolicySnapshot(owner, value.Descriptor.ProtocolVersion, revision, sha, array, authoritative); lock (_gate) { if (!_entries.TryGetValue(owner, out var value2) || value2 != value) { error = "Policy owner was unregistered during validation."; return false; } if (value.Current != null && revision <= value.Current.Revision) { error = "Policy revision became stale during validation."; snapshot = Clone(value.Current); return false; } value.Current = policySnapshot; snapshot = Clone(policySnapshot); return true; } } public bool TryGet(ModuleId owner, out PolicySnapshot snapshot) { lock (_gate) { if (_entries.TryGetValue(owner, out var value) && value.Current != null) { snapshot = Clone(value.Current); return true; } } snapshot = null; return false; } public IReadOnlyList<PolicySnapshot> Snapshot() { lock (_gate) { return Array.AsReadOnly((from value in _entries.Values where value.Current != null select Clone(value.Current) into value orderby value.Owner select value).ToArray()); } } private static PolicySnapshot Clone(PolicySnapshot value) { return new PolicySnapshot(value.Owner, value.ProtocolVersion, value.Revision, value.Sha256, value.Payload, value.IsAuthoritative); } } internal sealed class CombatStateService : ICombatStateService { private sealed class Entry { internal CombatSnapshot Snapshot; internal double ClearAt; } private sealed class Subscriber { internal ModuleId Owner; internal Action<CombatTransition> Handler; } private readonly object _gate = new object(); private readonly Dictionary<string, Entry> _states = new Dictionary<string, Entry>(StringComparer.Ordinal); private readonly List<Subscriber> _subscribers = new List<Subscriber>(); private readonly IFeatureCircuitBreaker _breakers; internal CombatStateService(IFeatureCircuitBreaker breakers) { _breakers = breakers; } public CombatSnapshot Get(string playerId) { if (string.IsNullOrEmpty(playerId)) { return CombatSnapshot.Empty; } lock (_gate) { Entry value; return _states.TryGetValue(playerId, out value) ? value.Snapshot : CombatSnapshot.Empty; } } public void Observe(CombatObservation observation) { if (observation == null) { throw new ArgumentNullException("observation"); } CombatTransition combatTransition = null; lock (_gate) { Entry value; CombatSnapshot combatSnapshot = (_states.TryGetValue(observation.PlayerId, out value) ? value.Snapshot : CombatSnapshot.Empty); if (combatSnapshot.IsActive && combatSnapshot.IsAuthoritative && !observation.IsAuthoritative) { return; } bool num = (observation.Context & (CombatContextFlags.Spectator | CombatContextFlags.Dead | CombatContextFlags.Transitioning)) != 0; CombatContextFlags combatContextFlags = CombatContextFlags.Normal | CombatContextFlags.Boss | CombatContextFlags.PlayerVersusPlayer | CombatContextFlags.Arena | CombatContextFlags.Training; if (num || (observation.Context & combatContextFlags) == 0) { _states.Remove(observation.PlayerId); if (combatSnapshot.IsActive) { combatTransition = new CombatTransition(combatSnapshot, CombatSnapshot.Empty, "context-cleared"); } } else { double enteredAt = (combatSnapshot.IsActive ? combatSnapshot.EnteredAt : observation.MonotonicSeconds); CombatSnapshot combatSnapshot2 = new CombatSnapshot(observation.PlayerId, active: true, observation.Context, enteredAt, observation.MonotonicSeconds, observation.OpponentId, observation.BossId, observation.IsAuthoritative); _states[observation.PlayerId] = new Entry { Snapshot = combatSnapshot2, ClearAt = observation.MonotonicSeconds + observation.ClearDelaySeconds }; if (!Equivalent(combatSnapshot, combatSnapshot2)) { combatTransition = new CombatTransition(combatSnapshot, combatSnapshot2, combatSnapshot.IsActive ? "updated" : "entered"); } } } if (combatTransition != null) { Publish(combatTransition); } } public void Clear(string playerId, string reason = "") { if (string.IsNullOrEmpty(playerId)) { return; } CombatTransition combatTransition = null; lock (_gate) { if (_states.TryGetValue(playerId, out var value)) { _states.Remove(playerId); combatTransition = new CombatTransition(value.Snapshot, CombatSnapshot.Empty, string.IsNullOrEmpty(reason) ? "cleared" : reason); } } if (combatTransition != null) { Publish(combatTransition); } } public void Tick(double monotonicSeconds) { if (double.IsNaN(monotonicSeconds) || double.IsInfinity(monotonicSeconds) || monotonicSeconds < 0.0) { return; } List<CombatTransition> list = null; lock (_gate) { string[] array = (from pair in _states where monotonicSeconds >= pair.Value.ClearAt select pair.Key).ToArray(); if (array.Length != 0) { list = new List<CombatTransition>(array.Length); } for (int num = 0; num < array.Length; num++) { Entry entry = _states[array[num]]; _states.Remove(array[num]); list.Add(new CombatTransition(entry.Snapshot, CombatSnapshot.Empty, "quiet-window-expired")); } } if (list != null) { for (int num2 = 0; num2 < list.Count; num2++) { Publish(list[num2]); } } } public IDisposable Subscribe(ModuleId owner, Action<CombatTransition> handler) { if (handler == null) { throw new ArgumentNullException("handler"); } Subscriber subscriber = new Subscriber { Owner = owner, Handler = handler }; lock (_gate) { _subscribers.Add(subscriber); } return new Registration(delegate { lock (_gate) { _subscribers.Remove(subscriber); } }); } internal void Reset(string reason) { List<CombatTransition> list; lock (_gate) { list = _states.Values.Select((Entry value) => new CombatTransition(value.Snapshot, CombatSnapshot.Empty, reason ?? "reset")).ToList(); _states.Clear(); } for (int num = 0; num < list.Count; num++) { Publish(list[num]); } } private void Publish(CombatTransition transition) { Subscriber[] array; lock (_gate) { array = _subscribers.OrderBy((Subscriber value) => value.Owner).ToArray(); } foreach (Subscriber subscriber in array) { _breakers.Execute(subscriber.Owner, "combat-state-observer", delegate { subscriber.Handler(transition); }); } } private static bool Equivalent(CombatSnapshot left, CombatSnapshot right) { if (left.IsActive == right.IsActive && left.Context == right.Context && string.Equals(left.OpponentId, right.OpponentId, StringComparison.Ordinal) && string.Equals(left.BossId, right.BossId, StringComparison.Ordinal)) { return left.IsAuthoritative == right.IsAuthoritative; } return false; } } internal sealed class CommandRegistry : ICommandRegistry { private sealed class Entry { internal CommandDescriptor Descriptor; internal Func<CommandContext, CommandResult> Handler; } private readonly object _gate = new object(); private readonly Dictionary<string, Entry> _commands = new Dictionary<string, Entry>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, long> _lastRuns = new Dictionary<string, long>(StringComparer.Ordinal); private readonly ILogSink _log; internal CommandRegistry(ILogSink log) { _log = log ?? NullLogSink.Instance; } public IDisposable Register(CommandDescriptor descriptor, Func<CommandContext, CommandResult> handler) { if (descriptor == null) { throw new ArgumentNullException("descriptor"); } if (handler == null) { throw new ArgumentNullException("handler"); } List<string> names = new List<string> { descriptor.Name }; names.AddRange(descriptor.Aliases); if (names.Any(string.IsNullOrWhiteSpace) || names.Any((string value) => value.Length > 64) || names.Distinct<string>(StringComparer.OrdinalIgnoreCase).Count() != names.Count) { throw new ArgumentException("Command names and aliases must be unique and bounded.", "descriptor"); } Entry entry = new Entry { Descriptor = descriptor, Handler = handler }; lock (_gate) { string text = names.FirstOrDefault(_commands.ContainsKey); if (text != null) { throw new InvalidOperationException("Command name already registered: " + text); } for (int num = 0; num < names.Count; num++) { _commands.Add(names[num], entry); } } return new Registration(delegate { lock (_gate) { foreach (string item in names) { if (_commands.TryGetValue(item, out var value) && value == entry) { _commands.Remove(item); } } } }); } public CommandResult Execute(CommandContext context, string input) { if (context == null) { throw new ArgumentNullException("context"); } if (!TryTokenize(input, out var tokens, out var error)) { return CommandResult.Failure(error); } if (tokens.Count == 0) { return CommandResult.NotHandled(); } Entry value; lock (_gate) { if (!_commands.TryGetValue(tokens[0], out value)) { return CommandResult.NotHandled(); } if (value.Descriptor.RequiresAdmin && !context.IsAdmin && !context.IsServer) { return CommandResult.Failure("Administrator permission is required."); } if (value.Descriptor.MinimumInterval > TimeSpan.Zero) { string key = context.SenderId + "\n" + value.Descriptor.Owner.ToString() + "\n" + value.Descriptor.Name; long timestamp = Stopwatch.GetTimestamp(); long num = (long)(value.Descriptor.MinimumInterval.TotalSeconds * (double)Stopwatch.Frequency); if (_lastRuns.TryGetValue(key, out var value2) && timestamp - value2 < num) { return CommandResult.Failure("Command is being used too quickly."); } _lastRuns[key] = timestamp; } } string[] array = tokens.Skip(1).ToArray(); CommandContext arg = new CommandContext(context.SenderId, context.SenderName, context.IsAdmin, context.IsServer, Array.AsReadOnly(array)); try { return value.Handler(arg) ?? CommandResult.Failure("Command returned no result."); } catch (Exception exception) { _log.Error("Command failed: " + value.Descriptor.Owner.ToString() + "/" + value.Descriptor.Name, exception); return CommandResult.Failure("Command failed safely; see the server log."); } } public IReadOnlyList<CommandDescriptor> Snapshot() { lock (_gate) { return Array.AsReadOnly((from value in _commands.Values.Distinct() select value.Descriptor).OrderBy<CommandDescriptor, string>((CommandDescriptor value) => value.Name, StringComparer.OrdinalIgnoreCase).ToArray()); } } private static bool TryTokenize(string input, out List<string> tokens, out string error) { tokens = new List<string>(); error = string.Empty; if (string.IsNullOrWhiteSpace(input)) { return true; } StringBuilder stringBuilder = new StringBuilder(); bool flag = false; bool flag2 = false; foreach (char c in input) { if (flag2) { stringBuilder.Append(c); flag2 = false; continue; } switch (c) { case '\\': flag2 = true; continue; case '"': flag = !flag; continue; } if (char.IsWhiteSpace(c) && !flag) { if (stringBuilder.Length > 0) { tokens.Add(stringBuilder.ToString()); stringBuilder.Clear(); } } else { stringBuilder.Append(c); } } if (flag2 || flag) { error = "Command contains an unfinished escape or quote."; return false; } if (stringBuilder.Length > 0) { tokens.Add(stringBuilder.ToString()); } if (tokens.Count > 64 || tokens.Any((string value) => value.Length > 1024)) { error = "Command exceeds the allowed size."; return false; } return true; } } internal sealed class CompatibilityRegistry : ICompatibilityRegistry { private readonly object _gate = new object(); private readonly List<CompatibilityRule> _rules = new List<CompatibilityRule>(); public IReadOnlyList<CompatibilityRule> Rules { get { lock (_gate) { return Array.AsReadOnly(_rules.ToArray()); } } } public IDisposable Register(CompatibilityRule rule) { if (rule == null) { throw new ArgumentNullException("rule"); } lock (_gate) { if (_rules.Any((CompatibilityRule existing) => string.Equals(existing.OwnerPluginGuid, rule.OwnerPluginGuid, StringComparison.Ordinal) && string.Equals(existing.TargetPluginGuid, rule.TargetPluginGuid, StringComparison.Ordinal) && existing.Kind == rule.Kind && string.Equals(existing.Feature, rule.Feature, StringComparison.Ordinal))) { throw new InvalidOperationException("Duplicate compatibility rule for " + rule.OwnerPluginGuid + " and " + rule.TargetPluginGuid + "."); } _rules.Add(rule); } return new Registration(delegate { lock (_gate) { _rules.Remove(rule); } }); } public IReadOnlyList<CompatibilityIssue> Evaluate(ISet<string> loadedPluginGuids) { if (loadedPluginGuids == null) { throw new ArgumentNullException("loadedPluginGuids"); } CompatibilityRule[] array; lock (_gate) { array = _rules.ToArray(); } List<CompatibilityIssue> list = new List<CompatibilityIssue>(); foreach (CompatibilityRule compatibilityRule in array) { bool flag = loadedPluginGuids.Contains(compatibilityRule.OwnerPluginGuid); bool flag2 = loadedPluginGuids.Contains(compatibilityRule.TargetPluginGuid); if (compatibilityRule.Kind switch { CompatibilityRuleKind.SoftDependency => flag && !flag2, CompatibilityRuleKind.ExternalAdapter => flag2, _ => flag && flag2, }) { list.Add(new CompatibilityIssue(compatibilityRule, flag, flag2)); } } return list.AsReadOnly(); } } internal static class CompatibilityReport { internal static string Write(CoreServices services, string directory, ILogSink log) { if (services == null) { throw new ArgumentNullException("services"); } Directory.CreateDirectory(directory); string text = Path.Combine(directory, "compatibility-report.txt"); string text2 = text + ".tmp." + Guid.NewGuid().ToString("N"); string contents = Build(services); File.WriteAllText(text2, contents, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (File.Exists(text)) { File.Replace(text2, text, text + ".bak", ignoreMetadataErrors: true); } else { File.Move(text2, text); } log.Info("Compatibility report written to " + text + "."); return text; } internal static string Build(CoreServices services) { StringBuilder stringBuilder = new StringBuilder(16384); stringBuilder.AppendLine("ModCore compatibility report"); stringBuilder.AppendLine("Generated UTC: " + DateTime.UtcNow.ToString("O")); stringBuilder.AppendLine("Core version: 0.5.0"); stringBuilder.AppendLine("Core API: " + 1); stringBuilder.AppendLine("Network protocol: " + 1); stringBuilder.AppendLine(); stringBuilder.AppendLine("REGISTERED CORE MODULES"); IReadOnlyList<ModuleSnapshot> readOnlyList = services.Modules.Snapshot(); for (int i = 0; i < readOnlyList.Count; i++) { ModuleDescriptor descriptor = readOnlyList[i].Descriptor; stringBuilder.Append("- ").Append(descriptor.Id).Append(" | ") .Append(descriptor.DisplayName) .Append(' ') .Append(descriptor.Version) .Append(" | protocol ") .Append(descriptor.ProtocolVersion) .Append(" | ") .Append(descriptor.Requirement) .Append(" | ") .Append(readOnlyList[i].State); if (!string.IsNullOrEmpty(readOnlyList[i].Detail)) { stringBuilder.Append(" | ").Append(readOnlyList[i].Detail); } stringBuilder.AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("LOADED BEPINEX PLUGINS"); foreach (KeyValuePair<string, PluginInfo> item in Chainloader.PluginInfos.OrderBy<KeyValuePair<string, PluginInfo>, string>((KeyValuePair<string, PluginInfo> keyValuePair) => keyValuePair.Key, StringComparer.Ordinal)) { stringBuilder.Append("- ").Append(item.Key).Append(" | ") .Append(item.Value.Metadata.Name) .Append(' ') .Append(item.Value.Metadata.Version) .AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("AUTHORED SUITE COVERAGE"); ISet<string> set = KnownModCatalog.LoadedPluginGuids(); HashSet<string> hashSet = new HashSet<string>(readOnlyList.Select((ModuleSnapshot moduleSnapshot) => moduleSnapshot.Descriptor.PluginGuid), StringComparer.Ordinal); for (int num = 0; num < KnownModCatalog.Mods.Count; num++) { KnownMod knownMod = KnownModCatalog.Mods[num]; string value = ((!set.Contains(knownMod.Guid)) ? "not loaded" : (hashSet.Contains(knownMod.Guid) ? "integrated" : "loaded; legacy direct-patch mode")); stringBuilder.Append("- ").Append(knownMod.Name).Append(" [") .Append(knownMod.Guid) .Append("] | ") .Append(value) .Append(" | ") .Append(knownMod.Role) .Append(" | ") .Append(knownMod.Disposition) .AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("COMPATIBILITY FINDINGS"); IReadOnlyList<CompatibilityIssue> readOnlyList2 = services.Compatibility.Evaluate(set); if (readOnlyList2.Count == 0) { stringBuilder.AppendLine("- none"); } for (int num2 = 0; num2 < readOnlyList2.Count; num2++) { CompatibilityRule rule = readOnlyList2[num2].Rule; stringBuilder.Append("- ").Append(rule.Severity).Append(" | ") .Append(rule.Kind) .Append(" | ") .Append(rule.OwnerPluginGuid) .Append(" + ") .Append(rule.TargetPluginGuid) .Append(" | ") .Append(rule.Message) .AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("INPUT COLLISIONS"); IReadOnlyList<InputCollision> readOnlyList3 = services.Input.Collisions(); if (readOnlyList3.Count == 0) { stringBuilder.AppendLine("- none registered"); } for (int num3 = 0; num3 < readOnlyList3.Count; num3++) { stringBuilder.Append("- ").Append(readOnlyList3[num3].First.Owner).Append('/') .Append(readOnlyList3[num3].First.ActionId) .Append(" conflicts with ") .Append(readOnlyList3[num3].Second.Owner) .Append('/') .Append(readOnlyList3[num3].Second.ActionId) .Append(" on ") .Append(readOnlyList3[num3].First.Binding) .AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("REGISTERED NAMESPACES"); IReadOnlyList<NamespaceSnapshot> readOnlyList4 = services.Namespaces.Snapshot(); if (readOnlyList4.Count == 0) { stringBuilder.AppendLine("- none"); } for (int num4 = 0; num4 < readOnlyList4.Count; num4++) { stringBuilder.Append("- ").Append(readOnlyList4[num4].Kind).Append(" | ") .Append(readOnlyList4[num4].Prefix) .Append(" | owner ") .Append(readOnlyList4[num4].Owner) .Append(" | schema ") .Append(readOnlyList4[num4].SchemaVersion) .AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("RULE PIPELINES"); IReadOnlyList<string> pipelineIds = services.Rules.PipelineIds; if (pipelineIds.Count == 0) { stringBuilder.AppendLine("- none (core remains gameplay-inert)"); } for (int num5 = 0; num5 < pipelineIds.Count; num5++) { stringBuilder.AppendLine("- " + pipelineIds[num5]); } stringBuilder.AppendLine(); stringBuilder.AppendLine("CIRCUIT BREAKERS"); IReadOnlyList<CircuitBreakerSnapshot> readOnlyList5 = services.CircuitBreakers.Snapshot(); if (readOnlyList5.Count == 0) { stringBuilder.AppendLine("- none tripped"); } for (int num6 = 0; num6 < readOnlyList5.Count; num6++) { stringBuilder.Append("- ").Append(readOnlyList5[num6].Owner).Append('/') .Append(readOnlyList5[num6].Feature) .Append(" | open=") .Append(readOnlyList5[num6].IsOpen) .Append(" | failures=") .Append(readOnlyList5[num6].ConsecutiveFailures) .Append(" | ") .Append(readOnlyList5[num6].Reason) .AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("CONFIGURATION METADATA (SECRETS REDACTED)"); IReadOnlyList<ConfigValueSnapshot> readOnlyList6 = services.Configuration.Snapshot(); for (int num7 = 0; num7 < readOnlyList6.Count; num7++) { stringBuilder.Append("- ").Append(readOnlyList6[num7].Descriptor.Owner).Append('/') .Append(readOnlyList6[num7].Descriptor.Section) .Append('/') .Append(readOnlyList6[num7].Descriptor.Key) .Append(" | ") .Append(readOnlyList6[num7].Descriptor.Scope) .Append(" | ") .Append(readOnlyList6[num7].Value) .AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("AUTHORITATIVE POLICY SNAPSHOTS"); IReadOnlyList<PolicySnapshot> readOnlyList7 = services.Policies.Snapshot(); if (readOnlyList7.Count == 0) { stringBuilder.AppendLine("- none"); } for (int num8 = 0; num8 < readOnlyList7.Count; num8++) { stringBuilder.Append("- ").Append(readOnlyList7[num8].Owner).Append(" | protocol ") .Append(readOnlyList7[num8].ProtocolVersion) .Append(" | revision ") .Append(readOnlyList7[num8].Revision) .Append(" | authoritative=") .Append(readOnlyList7[num8].IsAuthoritative) .Append(" | sha256 ") .Append(readOnlyList7[num8].Sha256) .AppendLine(); } stringBuilder.AppendLine(); stringBuilder.AppendLine("HARMONY PATCH OWNERSHIP"); try { foreach (MethodBase item2 in from methodBase in Harmony.GetAllPatchedMethods() orderby methodBase.DeclaringType?.FullName, methodBase.Name select methodBase) { Patches patchInfo = Harmony.GetPatchInfo(item2); string value2 = ((patchInfo == null) ? string.Empty : string.Join(",", patchInfo.Owners.ToArray())); stringBuilder.Append("- ").Append(item2.DeclaringType?.FullName).Append('.') .Append(item2.Name) .Append(" | ") .Append(value2) .AppendLine(); } } catch (Exception ex) { stringBuilder.AppendLine("- patch enumeration failed safely: " + ex.Message); } return stringBuilder.ToString(); } } internal static class ConfigFileMigration { internal const string CurrentFileName = "jg224.modcore.cfg"; internal const string LegacyFileName = "com.jg224.modcore.cfg"; internal static ConfigFile Open(BaseUnityPlugin plugin, string configDirectory, ManualLogSource log) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown if (MoveLegacy(configDirectory) && log != null) { log.LogInfo((object)"Renamed legacy config com.jg224.modcore.cfg to jg224.modcore.cfg."); } return PluginConfigFiles.Attach(plugin, new ConfigFile(Path.Combine(configDirectory, "jg224.modcore.cfg"), true, plugin.Info.Metadata)); } internal static bool MoveLegacy(string configDirectory) { if (!Directory.Exists(configDirectory)) { Directory.CreateDirectory(configDirectory); } if (ExactPath(configDirectory, "jg224.modcore.cfg") != null) { return false; } string text = ExactPath(configDirectory, "com.jg224.modcore.cfg"); if (text == null) { return false; } string text2 = Path.Combine(configDirectory, "jg224.modcore.cfg"); if (!string.Equals(text, text2, StringComparison.OrdinalIgnoreCase)) { File.Move(text, text2); return true; } string text3 = text2 + ".rename-" + Guid.NewGuid().ToString("N") + ".tmp"; File.Move(text, text3); try { File.Move(text3, text2); } catch { if (File.Exists(text3) && !File.Exists(text)) { File.Move(text3, text); } throw; } return true; } private static string ExactPath(string directory, string fileName) { return Directory.EnumerateFiles(directory, "*.cfg", SearchOption.TopDirectoryOnly).FirstOrDefault((string path) => string.Equals(Path.GetFileName(path), fileName, StringComparison.Ordinal)); } } internal sealed class ConfigurationRegistry : IConfigurationRegistry { private readonly object _gate = new object(); private readonly Dictionary<string, ConfigSettingDescriptor> _settings = new Dictionary<string, ConfigSettingDescriptor>(StringComparer.Ordinal); public IDisposable Register(ConfigSettingDescriptor descriptor) { if (descriptor == null) { throw new ArgumentNullException("descriptor"); } string key = Key(descriptor.Owner, descriptor.Section, descriptor.Key); lock (_gate) { if (_settings.ContainsKey(key)) { throw new InvalidOperationException("Configuration setting already registered: " + key.Replace('\n', '/')); } _settings.Add(key, descriptor); } return new Registration(delegate { lock (_gate) { if (_settings.TryGetValue(key, out var value) && value == descriptor) { _settings.Remove(key); } } }); } public IReadOnlyList<ConfigValueSnapshot> Snapshot(bool includeSecrets = false) { ConfigSettingDescriptor[] array; lock (_gate) { array = _settings.Values.OrderBy((ConfigSettingDescriptor configSettingDescriptor2) => configSettingDescriptor2.Owner).ThenBy<ConfigSettingDescriptor, string>((ConfigSettingDescriptor configSettingDescriptor2) => configSettingDescriptor2.Section, StringComparer.Ordinal).ThenBy<ConfigSettingDescriptor, string>((ConfigSettingDescriptor configSettingDescriptor2) => configSettingDescriptor2.Key, StringComparer.Ordinal) .ToArray(); } List<ConfigValueSnapshot> list = new List<ConfigValueSnapshot>(array.Length); foreach (ConfigSettingDescriptor configSettingDescriptor in array) { bool flag = configSettingDescriptor.Scope == ConfigScope.Secret && !includeSecrets; string value; if (flag) { value = "<redacted>"; } else { try { value = configSettingDescriptor.ValueProvider() ?? string.Empty; } catch (Exception ex) { value = "<error:" + ex.GetType().Name + ">"; } } list.Add(new ConfigValueSnapshot(configSettingDescriptor, value, flag)); } return list.AsReadOnly(); } private static string Key(ModuleId owner, string section, string key) { return owner.Value + "\n" + section + "\n" + key; } } internal sealed class CoreRuntime : IDisposable { internal static readonly ModuleId CoreId = new ModuleId("modcore"); private readonly CoreServices _services; private readonly ILogSink _log; private readonly string _reportDirectory; private readonly Func<bool> _writeReport; private readonly List<IDisposable> _registrations = new List<IDisposable>(); private int _ticking; private bool _disposed; internal CoreServices Services => _services; internal CoreRuntime(CoreServices services, ILogSink log, Func<bool> writeReport) { _services = services; _log = log; _writeReport = writeReport; _reportDirectory = Path.Combine(Paths.BepInExRootPath, "JG224ModCore", "reports"); _registrations.Add(_services.Modules.Register(new ModuleDescriptor(CoreId, "com.jg224.modcore", "ModCore", ParseVersion("0.5.0"), 1, ModuleSide.Both, ModuleRequirement.OptionalNegotiated, 0uL))); _services.Modules.SetState(CoreId, ModuleRuntimeState.Compatible, "gameplay-neutral core ready"); _registrations.Add(_services.Namespaces.Register(CoreId, NamespaceKind.RawRpc, "com.jg224.modcore", 1)); _registrations.Add(_services.Namespaces.Register(CoreId, NamespaceKind.Metric, "modcore.", 1)); KnownModCatalog.RegisterRules(_services.Compatibility); } internal void Tick() { if (_disposed || !_services.MainThread.IsMainThread || Interlocked.Exchange(ref _ticking, 1) != 0) { return; } try { _services.MainThread.Drain(); _services.Scheduler.Tick(); _services.NetworkRuntime.Tick(); _services.CombatState.Tick((double)Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency); _services.Metrics.SetGauge(CoreId, "main_thread_queue", _services.MainThread.PendingCount); } finally { Volatile.Write(ref _ticking, 0); } } internal void Publish(LifecycleEventKind kind, object subject = null, long peerId = 0L, string detail = "") { if (!_disposed) { _services.Lifecycle.Publish(new LifecycleEvent(kind, subject, peerId, detail)); } } internal void OnGameAwake(Game game) { Publish(LifecycleEventKind.WorldLoading, game, 0L); if (_writeReport()) { WriteReport(); } } internal void OnGameDestroyed(Game game) { Publish(LifecycleEventKind.WorldUnloading, game, 0L); _services.CombatRuntime.Reset("world-unloaded"); } internal void WriteReport() { try { CompatibilityReport.Write(_services, _reportDirectory, _log); } catch (Exception exception) { _log.Error("Could not write the compatibility report.", exception); } } public void Dispose() { if (!_disposed) { _disposed = true; for (int num = _registrations.Count - 1; num >= 0; num--) { _registrations[num].Dispose(); } _registrations.Clear(); _services.Dispose(); } } private static SemanticVersion ParseVersion(string value) { if (!SemanticVersion.TryParse(value, out var version)) { throw new InvalidOperationException("Plugin version is not valid semantic versioning: " + value); } return version; } } internal sealed class CoreServices : ICoreServices, IDisposable { private readonly CoreScheduler _scheduler; private readonly NetworkRouter _network; private bool _disposed; public IModuleRegistry Modules { get; } public ICompatibilityRegistry Compatibility { get; } public ILifecycleBus Lifecycle { get; } public IGameEventBus Events { get; } public IMetricRegistry Metrics { get; } public IFeatureCircuitBreaker CircuitBreakers { get; } public IMainThreadDispatcher MainThread { get; } public ICoreScheduler Scheduler { get; } public IConfigurationRegistry Configuration { get; } public IAuthoritativePolicyRegistry Policies { get; } public INamespaceRegistry Namespaces { get; } public IAtomicStoreFactory Stores { get; } public ICombatStateService CombatState { get; } public IRulePipelineRegistry Rules { get; } public IInventoryBroker Inventory { get; } public IUiRegistry Ui { get; } public IInputRegistry Input { get; } public ILocalizationRegistry Localization { get; } public INotificationService Notifications { get; } public ICommandRegistry Commands { get; } public IPlayerIdentityService Identity { get; } public INetworkRouter Network { get; } internal NetworkRouter NetworkRuntime => _network; internal RoutedRpcIngressRegistry RoutedIngress { get; } internal CombatStateService CombatRuntime => (CombatStateService)CombatState; internal CoreServices(ILogSink log, int maximumDispatcherQueue, Func<int> maximumPacketBytes, Func<int> maximumRpcPerSecond, Func<double> handshakeTimeoutSeconds, Func<bool> enforceRequired, Func<string> exactVersionModules = null) { ModuleRegistry modules = new ModuleRegistry(); MetricRegistry metrics = new MetricRegistry(); FeatureCircuitBreaker breakers = new FeatureCircuitBreaker(log); LifecycleBus lifecycle = new LifecycleBus(breakers, log); MainThreadDispatcher dispatcher = new MainThreadDispatcher(log, maximumDispatcherQueue); _scheduler = new CoreScheduler(dispatcher, log); PlayerIdentityService identity = new PlayerIdentityService(); Modules = modules; Compatibility = new CompatibilityRegistry(); Lifecycle = lifecycle; Events = new GameEventBus(breakers); Metrics = metrics; CircuitBreakers = breakers; MainThread = dispatcher; Scheduler = _scheduler; Configuration = new ConfigurationRegistry(); Policies = new AuthoritativePolicyRegistry(); Namespaces = new NamespaceRegistry(); Stores = new AtomicStoreFactory(); CombatState = new CombatStateService(breakers); Rules = new RulePipelineRegistry(breakers, log); Inventory = new InventoryBroker(log); Ui = new UiRegistry(); Input = new InputRegistry(); Localization = new LocalizationRegistry(); Notifications = new NotificationService(log); Commands = new CommandRegistry(log); Identity = identity; RoutedIngress = new RoutedRpcIngressRegistry(identity, delegate { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return default(RoutedRpcIngressRegistry.Endpoint); } bool flag = instance.IsServer(); return new RoutedRpcIngressRegistry.Endpoint(flag, ZNet.GetUID(), flag ? 0 : (instance.GetServerPeer()?.m_uid ?? 0)); }); _network = new NetworkRouter(modules, identity, lifecycle, _scheduler, metrics, breakers, log, maximumPacketBytes, maximumRpcPerSecond, handshakeTimeoutSeconds, enforceRequired, exactVersionModules); Network = _network; } public void Dispose() { if (!_disposed) { _disposed = true; _network.OnShutdown(); RoutedIngress.Dispose(); CombatRuntime.Reset("core-shutdown"); _scheduler.Dispose(); } } } internal sealed class MetricRegistry : IMetricRegistry { private sealed class Cell { internal long Value; internal volatile bool Gauge; } private readonly ConcurrentDictionary<string, Cell> _values = new ConcurrentDictionary<string, Cell>(StringComparer.Ordinal); public void Increment(ModuleId owner, string name, long amount = 1L) { string key = Key(owner, name); Interlocked.Add(ref _values.GetOrAdd(key, (string _) => new Cell()).Value, amount); } public void SetGauge(ModuleId owner, string name, long value) { string key = Key(owner, name); Cell orAdd = _values.GetOrAdd(key, (string _) => new Cell()); orAdd.Gauge = true; Interlocked.Exchange(ref orAdd.Value, value); } public IReadOnlyList<MetricSnapshot> Snapshot() { return Array.AsReadOnly(_values.OrderBy<KeyValuePair<string, Cell>, string>((KeyValuePair<string, Cell> pair) => pair.Key, StringComparer.Ordinal).Select(delegate(KeyValuePair<string, Cell> pair) { int num = pair.Key.IndexOf('\n'); return new MetricSnapshot(pair.Key.Substring(0, num), pair.Key.Substring(num + 1), Interlocked.Read(in pair.Value.Value), pair.Value.Gauge); }).ToArray()); } public void RemoveOwner(ModuleId owner) { string value = owner.Value + "\n"; foreach (string key in _values.Keys) { if (key.StartsWith(value, StringComparison.Ordinal)) { _values.TryRemove(key, out var _); } } } private static string Key(ModuleId owner, string name) { if (owner.IsEmpty) { throw new ArgumentException("Metric owner is required.", "owner"); } if (string.IsNullOrWhiteSpace(name) || name.Length > 128) { throw new ArgumentException("name"); } return owner.Value + "\n" + name; } } internal sealed class FeatureCircuitBreaker : IFeatureCircuitBreaker { private sealed class State { internal int ConsecutiveFailures; internal bool Open; internal string Reason = string.Empty; } private readonly ConcurrentDictionary<string, State> _states = new ConcurrentDictionary<string, State>(StringComparer.Ordinal); private readonly ILogSink _log; private readonly int _threshold; internal FeatureCircuitBreaker(ILogSink log, int threshold = 3) { if (threshold <= 0) { throw new ArgumentOutOfRangeException("threshold"); } _log = log ?? NullLogSink.Instance; _threshold = threshold; } public bool IsOpen(ModuleId owner, string feature) { if (_states.TryGetValue(Key(owner, feature), out var value)) { return value.Open; } return false; } public bool Execute(ModuleId owner, string feature, Action action) { if (action == null) { throw new ArgumentNullException("action"); } string key = Key(owner, feature); State orAdd = _states.GetOrAdd(key, (string _) => new State()); lock (orAdd) { if (orAdd.Open) { return false; } } try { action(); lock (orAdd) { orAdd.ConsecutiveFailures = 0; orAdd.Reason = string.Empty; } return true; } catch (Exception ex) { lock (orAdd) { orAdd.ConsecutiveFailures++; orAdd.Reason = ex.GetType().Name + ": " + ex.Message; if (orAdd.ConsecutiveFailures >= _threshold) { orAdd.Open = true; } } _log.Error("Feature failure in " + owner.ToString() + "/" + feature, ex); return false; } } public T Execute<T>(ModuleId owner, string feature, Func<T> action, T fallback) { T result = fallback; if (!Execute(owner, feature, delegate { result = action(); })) { return fallback; } return result; } public void Reset(ModuleId owner, string feature) { _states.TryRemove(Key(owner, feature), out var _); } public IReadOnlyList<CircuitBreakerSnapshot> Snapshot() { List<CircuitBreakerSnapshot> list = new List<CircuitBreakerSnapshot>(); foreach (KeyValuePair<string, State> item in _states.OrderBy<KeyValuePair<string, State>, string>((KeyValuePair<string, State> keyValuePair) => keyValuePair.Key, StringComparer.Ordinal)) { int num = item.Key.IndexOf('\n'); ModuleId owner = new ModuleId(item.Key.Substring(0, num)); State value = item.Value; lock (value) { list.Add(new CircuitBreakerSnapshot(owner, item.Key.Substring(num + 1), value.Open, value.ConsecutiveFailures, value.Reason)); } } return list.AsReadOnly(); } private static string Key(ModuleId owner, string feature) { if (owner.IsEmpty) { throw new ArgumentException("Feature owner is required.", "owner"); } if (string.IsNullOrWhiteSpace(feature) || feature.Length > 256) { throw new ArgumentException("feature"); } return owner.Value + "\n" + feature; } } internal sealed class GameEventBus : IGameEventBus { private sealed class Subscription { internal ModuleId Owner; internal Type EventType; internal Delegate Observer; internal int Priority; internal long Order; } private readonly object _gate = new object(); private readonly List<Subscription> _subscriptions = new List<Subscription>(); private readonly IFeatureCircuitBreaker _breakers; private long _nextOrder; internal GameEventBus(IFeatureCircuitBreaker breakers) { _breakers = breakers; } public IDisposable Subscribe<TEvent>(ModuleId owner, Action<TEvent> observer, int priority = 0) { if (owner.IsEmpty) { throw new ArgumentException("owner"); } if (observer == null) { throw new ArgumentNullException("observer"); } Subscription subscription = new Subscription { Owner = owner, EventType = typeof(TEvent), Observer = observer, Priority = priority, Order = Interlocked.Increment(ref _nextOrder) }; lock (_gate) { _subscriptions.Add(subscription); } return new Registration(delegate { lock (_gate) { _subscriptions.Remove(subscription); } }); } public void Publish<TEvent>(ModuleId publisher, TEvent value) { if (publisher.IsEmpty) { throw new ArgumentException("publisher"); } Subscription[] array; lock (_gate) { array = (from item in _subscriptions where item.EventType == typeof(TEvent) orderby item.Priority descending, item.Owner, item.Order select item).ToArray(); } foreach (Subscription current in array) { _breakers.Execute(current.Owner, "event." + typeof(TEvent).FullName, delegate { ((Action<TEvent>)current.Observer)(value); }); } } } internal sealed class InventoryBroker : IInventoryBroker { private sealed class ProviderEntry { internal ModuleId Owner; internal IInventoryProvider Provider; } private sealed class PolicyEntry { internal ModuleId Owner; internal IItemProtectionPolicy Policy; } private sealed class PendingTransaction { internal InventoryTransactionPlan Plan; internal ProviderEntry[] Providers; internal bool Busy; } private readonly object _gate = new object(); private readonly List<ProviderEntry> _providers = new List<ProviderEntry>(); private readonly List<PolicyEntry> _policies = new List<PolicyEntry>(); private readonly Dictionary<string, PendingTransaction> _pending = new Dictionary<string, PendingTransaction>(StringComparer.Ordinal); private readonly HashSet<string> _completed = new HashSet<string>(StringComparer.Ordinal); private readonly Queue<string> _completedOrder = new Queue<string>(); private readonly ILogSink _log; internal InventoryBroker(ILogSink log) { _log = log ?? NullLogSink.Instance; } public IDisposable RegisterProvider(ModuleId owner, IInventoryProvider provider) { if (provider == null) { throw new ArgumentNullException("provider"); } if (string.IsNullOrWhiteSpace(provider.ProviderId) || provider.ProviderId.Length > 128) { throw new ArgumentException("Provider ID is invalid.", "provider"); } ProviderEntry entry = new ProviderEntry { Owner = owner, Provider = provider }; lock (_gate) { if (_providers.Any((ProviderEntry value) => string.Equals(value.Provider.ProviderId, provider.ProviderId, StringComparison.Ordinal))) { throw new InvalidOperationException("Inventory provider already registered: " + provider.ProviderId); } _providers.Add(entry); } return new Registration(delegate { lock (_gate) { _providers.Remove(entry); } }); } public IDisposable RegisterProtectionPolicy(ModuleId owner, IItemProtectionPolicy policy) { if (policy == null) { throw new ArgumentNullException("policy"); } if (string.IsNullOrWhiteSpace(policy.PolicyId) || policy.PolicyId.Length > 128) { throw new ArgumentException("Policy ID is invalid.", "policy"); } PolicyEntry entry = new PolicyEntry { Owner = owner, Policy = policy }; lock (_gate) { if (_policies.Any((PolicyEntry value) => string.Equals(value.Policy.PolicyId, policy.PolicyId, StringComparison.Ordinal))) { throw new InvalidOperationException("Item protection policy already registered: " + policy.PolicyId); } _policies.Add(entry); } return new Registration(delegate { lock (_gate) { _policies.Remove(entry); } }); } public InventoryTransactionPlan Plan(ResourceRequest request) { if (request == null) { throw new ArgumentNullException("request"); } ProviderEntry[] array; PolicyEntry[] array2; lock (_gate) { if (_pending.ContainsKey(request.TransactionId) || _completed.Contains(request.TransactionId)) { return Invalid(request, "Transaction ID is already pending or completed."); } array = _providers.OrderByDescending((ProviderEntry value) => value.Provider.Priority).ThenBy<ProviderEntry, string>((ProviderEntry value) => value.Provider.ProviderId, StringComparer.Ordinal).ToArray(); array2 = _policies.OrderByDescending((PolicyEntry value) => value.Policy.Priority).ThenBy<PolicyEntry, string>((PolicyEntry value) => value.Policy.PolicyId, StringComparer.Ordinal).ToArray(); } List<Tuple<ProviderEntry, InventoryCandidate>> list = new List<Tuple<ProviderEntry, InventoryCandidate>>(); for (int num = 0; num < array.Length; num++) { IReadOnlyList<InventoryCandidate> readOnlyList; try { readOnlyList = array[num].Provider.FindCandidates(request) ?? Array.Empty<InventoryCandidate>(); } catch (Exception exception) { _log.Error("Inventory provider candidate lookup failed: " + array[num].Provider.ProviderId, exception); continue; } for (int num2 = 0; num2 < readOnlyList.Count; num2++) { InventoryCandidate inventoryCandidate = readOnlyList[num2]; if (inventoryCandidate == null || inventoryCandidate.Available <= 0 || !string.Equals(inventoryCandidate.ProviderId, array[num].Provider.ProviderId, StringComparison.Ordinal) || !string.Equals(inventoryCandidate.PrefabName, request.PrefabName, StringComparison.Ordinal) || (inventoryCandidate.Flags & (InventoryCandidateFlags.Protected | InventoryCandidateFlags.NonConsumable)) != InventoryCandidateFlags.None) { continue; } bool flag = false; for (int num3 = 0; num3 < array2.Length; num3++) { try { if (array2[num3].Policy.IsProtected(request, inventoryCandidate, out var _)) { flag = true; break; } } catch (Exception exception2) { _log.Error("Item protection policy failed: " + array2[num3].Policy.PolicyId, exception2); flag = true; break; } } if (!flag) { list.Add(Tuple.Create(array[num], inventoryCandidate)); } } } list.Sort(delegate(Tuple<ProviderEntry, InventoryCandidate> left, Tuple<ProviderEntry, InventoryCandidate> right) { int num7 = right.Item1.Provider.Priority.CompareTo(left.Item1.Provider.Priority); if (num7 != 0) { return num7; } num7 = right.Item2.ProviderPriority.CompareTo(left.Item2.ProviderPriority); if (num7 != 0) { return num7; } num7 = string.Compare(left.Item1.Provider.ProviderId, right.Item1.Provider.ProviderId, StringComparison.Ordinal); return (num7 == 0) ? string.Compare(left.Item2.ItemId, right.Item2.ItemId, StringComparison.Ordinal) : num7; }); int num4 = request.Amount; List<InventoryReservation> list2 = new List<InventoryReservation>(); for (int num5 = 0; num5 < list.Count; num5++) { if (num4 <= 0) { break; } ProviderEntry item = list[num5].Item1; InventoryCandidate item2 = list[num5].Item2; int num6 = Math.Min(item2.Available, num4); try { if (!item.Provider.TryReserve(request, item2, num6, out var reservation, out var error) || reservation == null || reservation.Amount != num6 || !string.Equals(reservation.ProviderId, item.Provider.ProviderId, StringComparison.Ordinal)) { RollbackReservations(list2, array); return Invalid(request, "Provider could not reserve resources: " + (error ?? item.Provider.ProviderId)); } list2.Add(reservation); num4 -= num6; } catch (Exception ex) { RollbackReservations(list2, array); return Invalid(request, "Provider reservation failed: " + ex.Message); } } if (num4 > 0) { RollbackReservations(list2, array); return Invalid(request, "Not enough eligible resources. Missing " + num4 + "."); } InventoryTransactionPlan inventoryTransactionPlan = new InventoryTransactionPlan(request, list2.AsReadOnly(), valid: true, string.Empty); bool flag2; lock (_gate) { flag2 = !_pending.ContainsKey(request.TransactionId) && !_completed.Contains(request.TransactionId); if (flag2) { _pending.Add(request.TransactionId, new PendingTransaction { Plan = inventoryTransactionPlan, Providers = array }); } } if (!flag2) { RollbackReservations(list2, array); return Invalid(request, "Transaction ID was claimed concurrently."); } return inventoryTransactionPlan; } public InventoryTransactionResult Commit(InventoryTransactionPlan plan) { if (plan == null) { throw new ArgumentNullException("plan"); } if (!plan.IsValid) { return new InventoryTransactionResult(succeeded: false, rolledBack: false, plan.Error); } PendingTransaction value; lock (_gate) { if (!_pending.TryGetValue(plan.Request.TransactionId, out value) || value.Plan != plan || value.Busy) { return new InventoryTransactionResult(succeeded: false, rolledBack: false, "Transaction is not pending or is not the original plan."); } value.Busy = true; } try { for (int i = 0; i < plan.Reservations.Count; i++) { FindProvider(value.Providers, plan.Reservations[i].ProviderId).Commit(plan.Reservations[i]); } } catch (Exception ex) { bool flag = RollbackReservations(plan.Reservations, value.Providers); lock (_gate) { _pending.Remove(plan.Request.TransactionId); RememberCompleted(plan.Request.TransactionId); } return new InventoryTransactionResult(succeeded: false, flag, (flag ? "Commit failed and was rolled back: " : "Commit and rollback failed; recovery is required. Do not retry this transaction: ") + ex.Message); } lock (_gate) { _pending.Remove(plan.Request.TransactionId); RememberCompleted(plan.Request.TransactionId); } return new InventoryTransactionResult(succeeded: true, rolledBack: false, string.Empty); } public void Cancel(InventoryTransactionPlan plan) { if (plan == null || !plan.IsValid) { return; } PendingTransaction value; lock (_gate) { if (!_pending.TryGetValue(plan.Request.TransactionId, out value) || value.Plan != plan || value.Busy) { return; } _pending.Remove(plan.Request.TransactionId); RememberCompleted(plan.Request.TransactionId); } RollbackReservations(plan.Reservations, value.Providers); } private void RememberCompleted(string transactionId) { if (_completed.Add(transactionId)) { _completedOrder.Enqueue(transactionId); } while (_completedOrder.Count > 4096) { _completed.Remove(_completedOrder.Dequeue()); } } private static InventoryTransactionPlan Invalid(ResourceRequest request, string error) { return new InventoryTransactionPlan(request, Array.Empty<InventoryReservation>(), valid: false, error); } private static IInventoryProvider FindProvider(IEnumerable<ProviderEntry> providers, string id) { return (providers.FirstOrDefault((ProviderEntry value) => string.Equals(value.Provider.ProviderId, id, StringComparison.Ordinal)) ?? throw new InvalidOperationException("Inventory provider disappeared: " + id)).Provider; } private bool RollbackReservations(IReadOnlyList<InventoryReservation> reservations, IEnumerable<ProviderEntry> providers) { bool result = true; ProviderEntry[] providers2 = (providers as ProviderEntry[]) ?? providers.ToArray(); for (int num = reservations.Count - 1; num >= 0; num--) { try { FindProvider(providers2, reservations[num].ProviderId).Rollback(reservations[num]); } catch (Exception exception) { result = false; _log.Error("Inventory rollback failed for " + reservations[num].ProviderId, exception); } } return result; } } internal sealed class KnownMod { internal string Guid { get; } internal string Name { get; } internal string Role { get; } internal string Disposition { get; } internal KnownMod(string guid, string name, string role, string disposition) { Guid = guid; Name = name; Role = role; Disposition = disposition; } } internal static class KnownModCatalog { internal static readonly IReadOnlyList<KnownMod> Mods = Array.AsReadOnly(new KnownMod[19] { new KnownMod("com.jg224.gearslots", "GearSlots", "equipment", "retain; core adapter pending"), new KnownMod("com.jg224.chestflow", "ChestFlow", "storage/resources", "retain; core adapter pending"), new KnownMod("jg224.BoatRadiusGuard", "SailRange", "exploration", "retain; network migration pending"), new KnownMod("jg224.FoodGuard", "FoodGuard", "readiness", "retain; combat-state migration pending"), new KnownMod("com.inventoryux.valheim", "CraftIndex", "crafting UI", "retain; UI/input migration pending"), new KnownMod("jg224.lumencore", "LumenCore", "environment", "retain; network/policy migration pending"), new KnownMod("jg224.performanceguard", "PerformanceGuard", "diagnostics", "retain; metrics consumer pending"), new KnownMod("jg224.worldstagedirector", "WorldStageDirector", "progression", "retain; pipeline/context migration pending"), new KnownMod("jg224.skaldhall", "SkaldHall", "arenas", "retain; pipeline/inventory migration pending"), new KnownMod("com.glm.valheimbuddy", "ValheimBuddy", "optional assistant server", "retain as optional"), new KnownMod("com.glm.valheimbuddy.client", "ValheimBuddy Client", "optional assistant client", "retain as optional"), new KnownMod("garst.SleepGuard", "SleepGuard", "sleep adapter", "consolidate"), new KnownMod("jg224.SleepSkipBossBlocker", "SleepSkip Boss Blocker", "sleep adapter", "consolidate; do not ship with SleepGuard"), new KnownMod("garst.RestartGuard", "RestartGuard", "server operations", "separate administrator package"), new KnownMod("garst.AnnounceRestart", "AnnounceRestart", "one-shot server utility", "maintenance only; timing defect known"), new KnownMod("zcode.hitchprober", "HitchProber", "developer diagnostics", "developer profile only"), new KnownMod("garst.NoSmokeGuard", "NoSmokeGuard", "legacy environment", "retire; replaced by LumenCore"), new KnownMod("nearbear_ServerSyncedBoatMapExploreRadius", "ServerSyncedBoatMapExploreRadius", "legacy exploration", "retire; replaced by SailRange"), new KnownMod("jg224.progressguard", "ProgressGuard", "legacy progression", "retire; replaced by WorldStageDirector") }); internal static void RegisterRules(ICompatibilityRegistry compatibility) { Register(compatibility, "jg224.lumencore", "garst.NoSmokeGuard", CompatibilityRuleKind.Replacement, CompatibilitySeverity.Error, "Remove NoSmokeGuard; LumenCore is its maintained replacement."); Register(compatibility, "jg224.lumencore", "TastyChickenLegs.NoSmokeStayLit", CompatibilityRuleKind.FeatureConflict, CompatibilitySeverity.Error, "Both mods own smoke/fire behavior; disable the overlapping feature or remove one mod.", "smoke-fire"); Register(compatibility, "jg224.lumencore", "smoke_collision", CompatibilityRuleKind.FeatureConflict, CompatibilitySeverity.Error, "Both mods own smoke behavior; disable the overlapping feature or remove one mod.", "smoke"); Register(compatibility, "jg224.BoatRadiusGuard", "nearbear_ServerSyncedBoatMapExploreRadius", CompatibilityRuleKind.Replacement, CompatibilitySeverity.Error, "Remove ServerSyncedBoatMapExploreRadius; SailRange is its maintained replacement."); Register(compatibility, "jg224.SleepSkipBossBlocker", "garst.SleepGuard", CompatibilityRuleKind.Replacement, CompatibilitySeverity.Error, "Install only the consolidated SleepSkip adapter, not both implementations."); Register(compatibility, "jg224.worldstagedirector", "jg224.progressguard", CompatibilityRuleKind.Replacement, CompatibilitySeverity.Error, "Remove ProgressGuard; WorldStageDirector preserves its migration path."); Register(compatibility, "com.jg224.chestflow", "goldenrevolver.quick_stack_store", CompatibilityRuleKind.HardConflict, CompatibilitySeverity.Error, "ChestFlow and Quick Stack Store mutate the same storage actions; disable one."); Register(compatibility, "jg224.worldstagedirector", "ZenDragon.ZenBossStone", CompatibilityRuleKind.HardConflict, CompatibilitySeverity.Error, "Both mods own boss-stone progression; disable one."); Register(compatibility, "jg224.skaldhall", "nex.SpeedyPaths", CompatibilityRuleKind.OrderingConstraint, CompatibilitySeverity.Information, "SkaldHall must run after Speedy Paths for arena speed normalization.", "movement"); Register(compatibility, "garst.SleepGuard", "Azumatt.SleepSkip", CompatibilityRuleKind.SoftDependency, CompatibilitySeverity.Warning, "SleepGuard has no effect without a compatible SleepSkip installation.", "sleep-adapter"); string[] array = new string[9] { "Azumatt.AzuExtendedPlayerInventory", "randyknapp.mods.equipmentandquickslots", "aedenthorn.ExtendedPlayerInventory", "com.bruce.valheim.comfyquickslots", "shudnal.ExtraSlots", "shudnal.ExtraSlotsCustomSlots", "moreslots", "toombe.EquipMultipleUtilityItemsUpdate", "aedenthorn.EquipMultipleUtilityItems" }; for (int i = 0; i < array.Length; i++) { Register(compatibility, "com.jg224.gearslots", array[i], CompatibilityRuleKind.HardConflict, CompatibilitySeverity.Error, "Multiple equipment/quick-slot providers are loaded; disable one."); } } internal static ISet<string> LoadedPluginGuids() { return new HashSet<string>(Chainloader.PluginInfos.Keys, StringComparer.Ordinal); } private static void Register(ICompatibilityRegistry compatibility, string owner, string target, CompatibilityRuleKind kind, CompatibilitySeverity severity, string message, string feature = "") { compatibility.Register(new CompatibilityRule(owner, target, kind, severity, message, feature)); } } internal sealed class LifecycleBus : ILifecycleBus { private sealed class Subscription { internal ModuleId Owner; internal LifecycleEventKind Kind; internal Action<LifecycleEvent> Handler; internal int Priority; internal long Order; } private readonly object _gate = new object(); private readonly List<Subscription> _subscriptions = new List<Subscription>(); private readonly IFeatureCircuitBreaker _breakers; private readonly ILogSink _log; private long _nextOrder; internal LifecycleBus(IFeatureCircuitBreaker breakers, ILogSink log) { _breakers = breakers; _log = log; } public IDisposable Subscribe(ModuleId owner, LifecycleEventKind kind, Action<LifecycleEvent> handler, int priority = 0) { if (handler == null) { throw new ArgumentNullException("handler"); } Subscription subscription = new Subscription { Owner = owner, Kind = kind, Handler = handler, Priority = priority, Order = Interlocked.Increment(ref _nextOrder) }; lock (_gate) { _subscriptions.Add(subscription); } return new Registration(delegate { lock (_gate) { _subscriptions.Remove(subscription); } }); } public void Publish(LifecycleEvent lifecycleEvent) { if (lifecycleEvent == null) { throw new ArgumentNullException("lifecycleEvent"); } Subscription[] array; lock (_gate) { array = (from value in _subscriptions where value.Kind == lifecycleEvent.Kind orderby value.Priority descending, value.Owner, value.Order select value).ToArray(); } foreach (Subscription subscription in array) { string text = "lifecycle." + lifecycleEvent.Kind; if (!_breakers.Execute(subscription.Owner, text, delegate { subscription.Handler(lifecycleEvent); })) { _log.Warning("Lifecycle subscriber failed and was isolated: " + subscription.Owner.ToString() + "/" + text); } } } } internal sealed class ModuleRegistry : IModuleRegistry { private sealed class Entry { internal ModuleDescriptor Descriptor; internal ModuleRuntimeState State; internal string Detail; } private readonly object _gate = new object(); private readonly Dictionary<ModuleId, Entry> _modules = new Dictionary<ModuleId, Entry>(); public IDisposable Register(ModuleDescriptor descriptor) { if (descriptor == null) { throw new ArgumentNullException("descriptor"); } if (!descriptor.SupportsCoreApi(1)) { throw new InvalidOperationException(descriptor.DisplayName + " does not support Mod Core API " + 1 + "."); } Entry entry = new Entry { Descriptor = descriptor, State = ModuleRuntimeState.Registered, Detail = string.Empty }; lock (_gate) { if (_modules.ContainsKey(descriptor.Id)) { throw new InvalidOperationException("Duplicate module ID: " + descriptor.Id); } if (_modules.Values.Any((Entry value) => string.Equals(value.Descriptor.PluginGuid, descriptor.PluginGuid, StringComparison.Ordinal))) { throw new InvalidOperationException("Plugin GUID already registered: " + descriptor.PluginGuid); } _modules.Add(descriptor.Id, entry); } return new Registration(delegate { lock (_gate) { if (_modules.TryGetValue(descriptor.Id, out var value) && value == entry) { _modules.Remove(descriptor.Id); } } }); } public bool TryGet(ModuleId id, out ModuleSnapshot module) { lock (_gate) { if (_modules.TryGetValue(id, out var value)) { module = ToSnapshot(value); return true; } } module = null; return false; } public IReadOnlyList<ModuleSnapshot> Snapshot() { lock (_gate) { return Array.AsReadOnly(_modules.Values.OrderBy((Entry value) => value.Descriptor.Id).Select(ToSnapshot).ToArray()); } } public void SetState(ModuleId id, ModuleRuntimeState state, string detail = "") { if (detail != null && detail.Length > 1024) { throw new ArgumentException("Module detail is too long.", "detail"); } lock (_gate) { if (!_modules.TryGetValue(id, out var value)) { throw new KeyNotFoundException("Module is not registered: " + id); } value.State = state; value.Detail = detail ?? string.Empty; } } private static ModuleSnapshot ToSnapshot(Entry entry) { return new ModuleSnapshot(entry.Descriptor, entry.State, entry.Detail); } } internal static class ModuleVersionRules { internal static bool RequiresExact(string configured, ModuleId module, ModuleRequirement requirement) { if (string.IsNullOrWhiteSpace(configured)) { return false; } string[] array = configured.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text == "*" && requirement == ModuleRequirement.RequiredOnBoth) { return true; } if (string.Equals(text, module.Value, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } } internal sealed class NamespaceRegistry : INamespaceRegistry { private sealed class Entry { internal NamespaceSnapshot Snapshot; internal HashSet<string> AllNames; } private readonly object _gate = new object(); private readonly List<Entry> _entries = new List<Entry>(); public IDisposable Register(ModuleId owner, NamespaceKind kind, string prefix, int schemaVersion = 1, params string[] legacyAliases) { if (owner.IsEmpty) { throw new ArgumentException("owner"); } prefix = Validate(prefix, "prefix"); if (schemaVersion <= 0) { throw new ArgumentOutOfRangeException("schemaVersion"); } string[] array = (legacyAliases ?? Array.Empty<string>()).Select((string value) => Validate(value, "legacyAliases")).Distinct<string>(StringComparer.Ordinal).ToArray(); HashSet<string> hashSet = new HashSet<string>(array, StringComparer.Ordinal) { prefix }; NamespaceSnapshot snapshot = new NamespaceSnapshot(owner, kind, prefix, schemaVersion, Array.AsReadOnly(array)); Entry entry = new Entry { Snapshot = snapshot, AllNames = hashSet }; lock (_gate) { foreach (Entry item in _entries.Where((Entry value) => value.Snapshot.Kind == kind)) { if (item.AllNames.Overlaps(hashSet)) { string text = item.AllNames.First(hashSet.Contains); throw new InvalidOperationException("Namespace collision for " + kind.ToString() + " '" + text + "' between " + item.Snapshot.Owner.ToString() + " and " + owner.ToString() + "."); } } _entries.Add(entry); } return new Registration(delegate { lock (_gate) { _entries.Remove(entry); } }); } public IReadOnlyList<NamespaceSnapshot> Snapshot() { lock (_gate) { return Array.AsReadOnly((from value in _entries select value.Snapshot into value orderby value.Kind select value).ThenBy<NamespaceSnapshot, string>((NamespaceSnapshot value) => value.Prefix, StringComparer.Ordinal).ToArray()); } } private static string Validate(string value, string parameter) { value = Guard.Bounded(value, parameter, 128); if (value.IndexOfAny(new char[3] { '\r', '\n', '\0' }) >= 0) { throw new ArgumentException(parameter); } return value; } } internal sealed class NetworkRouter : INetworkRouter { private sealed class RemoteModule { internal ModuleId Id; internal string Version; internal int Protocol; internal ModuleSide Side; internal ModuleRequirement Requirement; internal ulong Capabilities; } private sealed class PeerState { internal ZNetPeer Peer; internal long StartedAt; internal bool HandshakeComplete; internal bool RequiredCompatible; internal string Detail = "waiting"; internal readonly Dictionary<string, ulong> Capabilities = new Dictionary<string, ulong>(StringComparer.Ordinal); internal readonly Dictionary<string, long> IncomingSequences = new Dictionary<string, long>(StringComparer.Ordinal); internal long OutgoingSequence; internal long RateWindowStarted; internal int RateWindowCount; internal object Session; internal IDisposable PendingDisconnect; } private sealed class HandlerEntry { internal NetworkMessageDescriptor Descriptor; internal Action<NetworkMessageContext, ZPackage> Handler; } private const string HelloRpc = "com.jg224.modcore.Hello"; private const string AckRpc = "com.jg224.modcore.Ack"; private const string EnvelopeRpc = "com.jg224.modcore.Envelope"; private static readonly ModuleId CoreModuleId = new ModuleId("modcore"); private readonly object _gate = new object(); private readonly Dictionary<ZRpc, PeerState> _peers = new Dictionary<ZRpc, PeerState>(); private readonly Dictionary<string, HandlerEntry> _handlers = new Dictionary<string, HandlerEntry>(StringComparer.Ordinal); private readonly IModuleRegistry _modules; private readonly PlayerIdentityService _identity; private readonly ILifecycleBus _lifecycle; private readonly ICoreScheduler _scheduler; private readonly IMetricRegistry _metrics; private readonly IFeatureCircuitBreaker _breakers; private readonly ILogSink _log; private readonly Func<int> _maximumPacketBytes; private readonly Func<int> _maximumRpcPerSecond; private readonly Func<double> _handshakeTimeoutSeconds; private readonly Func<bool> _enforceRequired; private readonly Func<string> _exactVersionModules; private readonly Func<bool> _isServer; private readonly Func<object> _session; private readonly Func<long> _timestamp; private readonly Action<ZNetPeer> _disconnect; private readonly Action<ZRpc, string, ZPackage> _invoke; private ZNetPeer _serverPeer; private int _nextCorrelation; internal NetworkRouter(IModuleRegistry modules, PlayerIdentityService identity, ILifecycleBus lifecycle, ICoreScheduler scheduler, IMetricRegistry metrics, IFeatureCircuitBreaker breakers, ILogSink log, Func<int> maximumPacketBytes, Func<int> maximumRpcPerSecond, Func<double> handshakeTimeoutSeconds, Func<bool> enforceRequired, Func<string> exactVersionModules = null, Func<bool> isServer = null, Func<object> session = null, Func<long> timestamp = null, Action<ZNetPeer> disconnect = null, Action<ZRpc, string, ZPackage> invoke = null) { _modules = modules; _identity = identity; _lifecycle = lifecycle; _scheduler = scheduler; _metrics = metrics; _breakers = breakers; _log = log; _maximumPacketBytes = maximumPacketBytes; _maximumRpcPerSecond = maximumRpcPerSecond; _handshakeTimeoutSeconds = handshakeTimeoutSeconds; _enforceRequired = enforceRequired; _exactVersionModules = exactVersionModules ?? ((Func<string>)(() => string.Empty)); _isServer = isServer ?? ((Func<bool>)(() => (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())); _session = session ?? ((Func<object>)(() => ZNet.instance)); _timestamp = timestamp ?? new Func<long>(Stopwatch.GetTimestamp); _disconnect = disconnect ?? ((Action<ZNetPeer>)delegate(ZNetPeer peer) { ZNet.instance.Disconnect(peer); }); _invoke = invoke ?? ((Action<ZRpc, string, ZPackage>)delegate(ZRpc rpc, string name, ZPackage package) { rpc.Invoke(name, new object[1] { package }); }); } public IDisposable Register(NetworkMessageDescriptor descriptor, Action<NetworkMessageContext, ZPackage> handler) { if (descriptor == null) { throw new ArgumentNullException("descriptor"); } if (handler == null) { throw new ArgumentNullException("handler"); } if (!_modules.TryGet(descriptor.Owner, out var module)) { throw new InvalidOperationException("Network message owner is not a registered module: " + descriptor.Owner); } if (module.Descriptor.ProtocolVersion != descriptor.ModuleProtocol) { throw new InvalidOperationException("Network descriptor protocol does not match the registered module protocol."); } string key = HandlerKey(descriptor.Owner, descriptor.MessageType); HandlerEntry entry = new HandlerEntry { Descriptor = descriptor, Handler = handler }; lock (_gate) { if (_handlers.ContainsKey(key)) { throw new InvalidOperationException("Network message already registered: " + key); } _handlers.Add(key, entry); } return new Registration(delegate { lock (_gate) { if (_handlers.TryGetValue(key, out var value) && value == entry) { _handlers.Remove(key); } } }); } public bool SendToServer(NetworkMessageDescriptor descriptor, ZPackage payload, int correlationId = 0, NetworkMessageFlags flags = NetworkMessageFlags.None) { ZNetPeer serverPeer; lock (_gate) { serverPeer = _serverPeer; } if (serverPeer != null) { return Send(serverPeer, descriptor, payload, correlationId, flags); } return false; } public bool SendToPeer(long peerId, NetworkMessageDescriptor descriptor, ZPackage payload, int correlationId = 0, NetworkMessageFlags flags = NetworkMessageFlags.None) { PeerState peerState; lock (_gate) { peerState = _peers.Values.FirstOrDefault((PeerState value) => value.Peer != null && value.Peer.m_uid == peerId); } if (peerState != null) { return Send(peerState.Peer, descriptor, payload, correlationId, flags); } return false; } public int Broadcast(NetworkMessageDescriptor descriptor, ZPackage payload, int correlationId = 0, NetworkMessageFlags flags = NetworkMessageFlags.None) { ZNetPeer[] array; lock (_gate) { array = (from value in _peers.Values select value.Peer into value where value != null select value).ToArray(); } int num = 0; for (int num2 = 0; num2 < array.Length; num2++) { if (Send(array[num2], descriptor, payload, correlationId, flags)) { num++; } } return num; } public int NextCorrelationId() { int num = Interlocked.Increment(ref _nextCorrelation); if (num != 0) { return num; } return Interlocked.Increment(ref _nextCorrelation); } public IReadOnlyList<NetworkPeerSnapshot> PeerSnapshot() { lock (_gate) { return Array.AsReadOnly((from value in _peers.Values orderby value.Peer?.m_uid ?? 0 select new NetworkPeerSnapshot(value.Peer?.m_uid ?? 0, value.HandshakeComplete, value.RequiredCompatible, value.Detail, new Dictionary<string, ulong>(value.Capabilities, StringComparer.Ordinal))).ToArray()); } } internal void OnNewConnection(ZNet znet, ZNetPeer peer) { if (!((Object)(object)znet == (Object)null) && peer?.m_rpc != null) { peer.m_rpc.Register<ZPackage>("com.jg224.modcore.Hello", (Action<ZRpc, ZPackage>)ReceiveHello); peer.m_rpc.Register<ZPackage>("com.jg224.modcore.Ack", (Action<ZRpc, ZPackage>)ReceiveAck); peer.m_rpc.Register<ZPackage>("com.jg224.modcore.Envelope", (Action<ZRpc, ZPackage>)ReceiveEnvelope); TrackConnection(peer); if (!znet.IsServer()) { _invoke(peer.m_rpc, "com.jg224.modcore.Hello", WriteHandshake()); } } } internal void TrackConnection(ZNetPeer peer) { PeerState value = new PeerState { Peer = peer, StartedAt = _timestamp(), RateWindowStarted = _timestamp(), Session = _session() }; lock (_gate) { if (_peers.TryGetValue(peer.m_rpc, out var value2)) { value2.PendingDisconnect?.Dispose(); } _peers[peer.m_rpc] = value; if (!_isServer()) { _serverPeer = peer; } } _identity.Connected(peer); _lifecycle.Publish(new LifecycleEvent(LifecycleEventKind.PeerConnected, peer, peer.m_uid)); } internal void OnDisconnect(ZNetPeer peer) { if (peer?.m_rpc == null) { return; } lock (_gate) { if (_peers.TryGetValue(peer.m_rpc, out var value)) { value.PendingDisconnect?.Dispose(); } _peers.Remove(peer.m_rpc); if (_serverPeer == peer) { _serverPeer = null; } } _identity.Disconnected(peer); _lifecycle.Publish(new LifecycleEvent(LifecycleEventKind.PeerDisconnected, peer, peer.m_uid)); } internal void OnShutdown() { lock (_gate) { foreach (PeerState value in _peers.Values) { value.PendingDisconnect?.Dispose(); } _peers.Clear(); _serverPeer = null; } _identity.Reset(); } internal void Tick() { long now = _timestamp(); double limit = _handshakeTimeoutSeconds() * (double)Stopwatch.Frequency; List<PeerState> list; lock (_gate) { list = _peers.Values.Where((PeerState value) => !value.HandshakeComplete && (double)(now - value.StartedAt) > limit).ToList(); for (int num = 0; num < list.Count; num++) { list[num].HandshakeComplete = true; list[num].RequiredCompatible = false; list[num].Detail = "ModCore handshake timed out"; } } for (int num2 = 0; num2 < list.Count; num2++) { _log.Warning("Mod Core handshake timed out for peer " + (list[num2].Peer?.m_uid ?? 0) + "."); EnforceFailure(list[num2], HasRequiredModules()); } } internal void ReceiveHello(ZRpc rpc, ZPackage package) { _metrics.Increment(CoreModuleId, "rpc.received", 1L); if (_session() != null && _isServer() && TryBeginHandshake(rpc, fromServer: false, out var state)) { if (!TryReadHandshake(package, out var modules, out var error)) { CompleteHandshake(state, compatible: false, "invalid handshake", null); EnforceFailure(state, HasRequiredModules()); SendAck(rpc, compatible: false, "Invalid Mod Core handshake: " + error); } else { string detail; Dictionary<string, ulong> capabilities; bool flag = EvaluateRequired(modules, out detail, out capabilities); CompleteHandshake(state, flag, detail, capabilities); EnforceFailure(state, !flag); SendAck(rpc, flag, detail); } } } internal void ReceiveAck(ZRpc rpc, ZPackage package) { _metrics.Increment(CoreModuleId, "rpc.received", 1L); if (_session() == null || _isServer() || !TryBeginHandshake(rpc, fromServer: true, out var state)) { return; } try { if (package == null || package.Size() <= 0 || package.Size() > _maximumPacketBytes()) { throw new InvalidOperationException("Ack size is invalid."); } if (package.ReadInt() != 1) { throw new InvalidOperationException("Core protocol differs."); } bool num = package.ReadBool(); string text = ReadBoundedString(package, 1024); List<RemoteModule> list = ReadModules(package); if (package.GetPos() != package.Size()) { throw new InvalidOperationException("Ack contains trailing data."); } string detail; Dictionary<string, ulong> capabilities; bool flag = EvaluateRequired(list, out detail, out capabilities); bool flag2 = num && flag; CompleteHandshake(state, flag2, flag2 ? "compatible" : (text + "; " + detail), capabilities); EnforceFailure(state, !flag2 && (HasRequiredModules() || list.Any((RemoteModule module) => module.Requirement == ModuleRequirement.RequiredOnBoth))); if (!flag2) { _log.Warning("Server Mod Core compatibility failed: " + text + "; " + detail); } } catch (Exception exception) { CompleteHandshake(state, compatible: false, "invalid acknowledgement", null); EnforceFailure(state, HasRequiredModules()); _log.Error("Rejected invalid Mod Core acknowledgement.", exception); } } private bool TryBeginHandshake(ZRpc rpc, bool fromServer, out PeerState state) { lock (_gate) { if (!_peers.TryGetValue(rpc, out state) || (fromServer && state.Peer != _serverPeer)) { return false; } if (!AllowRateLocked(state)) { _metrics.Increment(CoreModuleId, "rpc.rate_limited", 1L); return false; } return !state.HandshakeComplete; } } private void CompleteHandshake(PeerState state, bool compatible, string detail, Dictionary<string, ulong> capabilities) { lock (_gate) { state.HandshakeComplete = true; state.RequiredCompatible = compatible; state.Detail = detail; state.Capabilities.Clear(); if (capabilities == null) { return; } foreach (KeyValuePair<string, ulong> capability in capabilities) { state.Capabilities[capability.Key] = capability.Value; } } } private bool HasRequiredModules() { return _modules.Snapshot().Any((ModuleSnapshot value) => value.Descriptor.Requirement == ModuleRequirement.RequiredOnBoth); } private void EnforceFailure(PeerState state, bool requiredFailure) { if (!requiredFailure || !_enforceRequired()) { return; } lock (_gate) { if (state.RequiredCompatible || state.PendingDisconnect != null) { return; } state.PendingDisconnect = _scheduler.Schedule(CoreModuleId, TimeSpan.FromSeconds(1.0), delegate { lock (_gate) { if (!_peers.TryGetValue(state.Peer.m_rpc, out var value) || value != state || _session() != state.Session || state.RequiredCompatible || !_enforceRequired()) { return; } } _disconnect(state.Peer); }); } } private void ReceiveEnvelope(ZRpc rpc, ZPackage package) { //IL_02c1: Unknown result type (might be due to invalid IL or missing references) //IL_02cb: Expected O, but got Unknown _metrics.Increment(CoreModuleId, "rpc.received", 1L); PeerState value; ModuleId moduleId; ushort messageType; int correlationId; long num2; NetworkMessageFlags networkMessageFlags; HandlerEntry entry; byte[] array; try { lock (_gate) { if (!_peers.TryGetValue(rpc, out value) || !value.HandshakeComplete || !value.RequiredCompatible) { return; } if (!AllowRateLocked(value)) { _metrics.Increment(CoreModuleId, "rpc.rate_limited", 1L); return; } } if (package == null || package.Size() <= 0 || package.Size() > _maximumPacketBytes()) { throw new InvalidOperationException("Envelope size is invalid."); } if (package.ReadInt() != 1) { throw new InvalidOperationException("Core protocol differs."); } if (!ModuleId.TryParse(ReadBoundedString(package, 64), out moduleId)) { throw new InvalidOperationException("Module ID is invalid."); } int num = package.ReadInt(); messageType = package.ReadUShort(); correlationId = package.ReadInt(); num2 = package.ReadLong(); networkMessageFlags = (NetworkMessageFlags)package.ReadByte(); lock (_gate) { if (!_handlers.TryGetValue(HandlerKey(moduleId, messageType), out entry)) { return; } if (num != entry.Descriptor.ModuleProtocol) { throw new InvalidOperationException("Module protocol or payload bound is invalid."); } array = ReadEnvelopePayload(package, entry.Descriptor.MaximumPayloadBytes); if (!DirectionAllowed(entry.Descriptor.Direction)) { throw new InvalidOperationException("Message direction is not allowed."); } if (entry.Descriptor.RequiredCapability != 0L && (!value.Capabilities.TryGetValue(moduleId.Value, out var value2) || (value2 & entry.Descriptor.RequiredCapability) != entry.Descriptor.RequiredCapability)) { throw new InvalidOperationException("Peer did not negotiate the required capability."); } if ((networkMessageFlags & NetworkMessageFlags.Ordered) != NetworkMessageFlags.None) { string key = HandlerKey(moduleId, messageType); if (value.IncomingSequences.TryGetValue(key, out var value3) && num2 <= value3) { _metrics.Increment(CoreModuleId, "rpc.stale_rejected", 1L); return; } value.IncomingSequences[key] = num2; } } } catch (Exception ex) { _metrics.Increment(CoreModuleId, "rpc.malformed", 1L); _log.Warning("Rejected Mod Core envelope: " + ex.Message); return; } _identity.TryGetByConnection(rpc, out var identity); NetworkMessageContext context = new NetworkMessageContext(value.Peer?.m_uid ?? 0, identity, correlationId, num2, networkMessageFlags); ZPackage inner = new ZPackage(array); string feature = "network.message." + moduleId.ToString() + "." + messageType; if (_breakers.Execute(moduleId, feature, delegate { entry.Handler(context, inner); }) && inner.GetPos() != inner.Size()) { _metrics.Increment(CoreModuleId, "rpc.trailing_payload", 1L); _log.Warning("Network handler did not consume its full payload: " + moduleId.ToString() + "/" + messageType + "."); } } internal static byte[] ReadEnvelopePayload(ZPackage package, int maximumBytes) { if (package == null || maximumBytes < 0 || package.Size() - package.GetPos() < 4) { throw new InvalidOperationException("Envelope payload length is missing."); } int num = package.ReadInt(); if (num < 0 || num > maximumBytes || num != package.Size() - package.GetPos()) { throw new InvalidOperationException("Envelope payload length or bound is invalid."); } byte[] array = package.ReadByteArray(num); if (array.Length != num || package.GetPos() != package.Size()) { throw new InvalidOperationException("Envelope payload is incomplete."); } return array; } private bool Send(ZNetPeer peer, NetworkMessageDescriptor descriptor, ZPackage payload, int correlation, NetworkMessageFlags flags) { //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Expected O, but got Unknown if (peer?.m_rpc == null || descriptor == null || payload == null || !peer.m_rpc.IsConnected()) { return false; } byte[] array = payload.GetArray(); if (array.Length > descriptor.MaximumPayloadBytes) { throw new InvalidOperationException("Payload exceeds message bound."); } long num; lock (_gate) { if (!_handlers.TryGetValue(HandlerKey(descriptor.Owner, descriptor.Message