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 _registrations = new List(); private Harmony _harmony; private CoreServices _services; private CoreRuntime _runtime; private ConfigFile _configuration; private ConfigEntry _debugLogging; private ConfigEntry _writeCompatibilityReport; private ConfigEntry _enforceRequiredModules; private ConfigEntry _maximumPacketBytes; private ConfigEntry _maximumRpcPerSecond; private ConfigEntry _handshakeTimeoutSeconds; private ConfigEntry _maximumDispatcherQueue; private ConfigEntry _exactVersionModules; private ConfigEntry _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("Diagnostics", "DebugLogging", false, "Enable verbose Mod Core diagnostics. Normal gameplay should leave this disabled."); _writeCompatibilityReport = _configuration.Bind("Diagnostics", "WriteCompatibilityReport", true, "Write a redacted compatibility report when a world starts."); _enforceRequiredModules = _configuration.Bind("Networking", "EnforceRequiredModules", true, "Disconnect a peer when a migrated module marked RequiredOnBoth is missing or protocol-incompatible."); _exactVersionModules = _configuration.Bind("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("Display", "StatusShortcut", new KeyboardShortcut((KeyCode)291, Array.Empty()), "Open the read-only, redacted compatibility status panel. Press again to close."); _maximumPacketBytes = _configuration.Bind("Networking", "MaximumPacketBytes", 1048576, new ConfigDescription("Maximum accepted Mod Core envelope size.", (AcceptableValueBase)(object)new AcceptableValueRange(4096, 1048576), Array.Empty())); _maximumRpcPerSecond = _configuration.Bind("Networking", "MaximumRpcPerPeerPerSecond", 120, new ConfigDescription("Per-peer Mod Core message rate limit.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 2000), Array.Empty())); _handshakeTimeoutSeconds = _configuration.Bind("Networking", "HandshakeTimeoutSeconds", 5f, new ConfigDescription("Time allowed for Mod Core capability negotiation.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 30f), Array.Empty())); _maximumDispatcherQueue = _configuration.Bind("Runtime", "MaximumMainThreadQueue", 4096, new ConfigDescription("Maximum queued cross-thread game actions before new work is rejected.", (AcceptableValueBase)(object)new AcceptableValueRange(64, 100000), Array.Empty())); } private void RegisterConfigurationMetadata() { _registrations.Add(RegisterConfig(_debugLogging, ConfigScope.Developer)); _registrations.Add(RegisterConfig(_writeCompatibilityReport, ConfigScope.LocalPreference)); _registrations.Add(RegisterConfig(_enforceRequiredModules, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig(_maximumPacketBytes, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig(_maximumRpcPerSecond, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig(_handshakeTimeoutSeconds, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig(_maximumDispatcherQueue, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig(_exactVersionModules, ConfigScope.ServerPolicy)); _registrations.Add(RegisterConfig(_statusShortcut, ConfigScope.LocalPreference)); } private IDisposable RegisterConfig(ConfigEntry 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 _entries = new Dictionary(); 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 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 Handler; } private readonly object _gate = new object(); private readonly Dictionary _states = new Dictionary(StringComparer.Ordinal); private readonly List _subscribers = new List(); 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 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(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 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 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 Handler; } private readonly object _gate = new object(); private readonly Dictionary _commands = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _lastRuns = new Dictionary(StringComparer.Ordinal); private readonly ILogSink _log; internal CommandRegistry(ILogSink log) { _log = log ?? NullLogSink.Instance; } public IDisposable Register(CommandDescriptor descriptor, Func handler) { if (descriptor == null) { throw new ArgumentNullException("descriptor"); } if (handler == null) { throw new ArgumentNullException("handler"); } List names = new List { descriptor.Name }; names.AddRange(descriptor.Aliases); if (names.Any(string.IsNullOrWhiteSpace) || names.Any((string value) => value.Length > 64) || names.Distinct(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 Snapshot() { lock (_gate) { return Array.AsReadOnly((from value in _commands.Values.Distinct() select value.Descriptor).OrderBy((CommandDescriptor value) => value.Name, StringComparer.OrdinalIgnoreCase).ToArray()); } } private static bool TryTokenize(string input, out List tokens, out string error) { tokens = new List(); 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 _rules = new List(); public IReadOnlyList 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 Evaluate(ISet loadedPluginGuids) { if (loadedPluginGuids == null) { throw new ArgumentNullException("loadedPluginGuids"); } CompatibilityRule[] array; lock (_gate) { array = _rules.ToArray(); } List list = new List(); 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 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 item in Chainloader.PluginInfos.OrderBy, string>((KeyValuePair 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 set = KnownModCatalog.LoadedPluginGuids(); HashSet hashSet = new HashSet(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 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 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 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 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 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 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 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 _settings = new Dictionary(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 Snapshot(bool includeSecrets = false) { ConfigSettingDescriptor[] array; lock (_gate) { array = _settings.Values.OrderBy((ConfigSettingDescriptor configSettingDescriptor2) => configSettingDescriptor2.Owner).ThenBy((ConfigSettingDescriptor configSettingDescriptor2) => configSettingDescriptor2.Section, StringComparer.Ordinal).ThenBy((ConfigSettingDescriptor configSettingDescriptor2) => configSettingDescriptor2.Key, StringComparer.Ordinal) .ToArray(); } List list = new List(array.Length); foreach (ConfigSettingDescriptor configSettingDescriptor in array) { bool flag = configSettingDescriptor.Scope == ConfigScope.Secret && !includeSecrets; string value; if (flag) { value = ""; } else { try { value = configSettingDescriptor.ValueProvider() ?? string.Empty; } catch (Exception ex) { value = ""; } } 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 _writeReport; private readonly List _registrations = new List(); private int _ticking; private bool _disposed; internal CoreServices Services => _services; internal CoreRuntime(CoreServices services, ILogSink log, Func 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 maximumPacketBytes, Func maximumRpcPerSecond, Func handshakeTimeoutSeconds, Func enforceRequired, Func 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 _values = new ConcurrentDictionary(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 Snapshot() { return Array.AsReadOnly(_values.OrderBy, string>((KeyValuePair pair) => pair.Key, StringComparer.Ordinal).Select(delegate(KeyValuePair 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 _states = new ConcurrentDictionary(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(ModuleId owner, string feature, Func 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 Snapshot() { List list = new List(); foreach (KeyValuePair item in _states.OrderBy, string>((KeyValuePair 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 _subscriptions = new List(); private readonly IFeatureCircuitBreaker _breakers; private long _nextOrder; internal GameEventBus(IFeatureCircuitBreaker breakers) { _breakers = breakers; } public IDisposable Subscribe(ModuleId owner, Action 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(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)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 _providers = new List(); private readonly List _policies = new List(); private readonly Dictionary _pending = new Dictionary(StringComparer.Ordinal); private readonly HashSet _completed = new HashSet(StringComparer.Ordinal); private readonly Queue _completedOrder = new Queue(); 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 value) => value.Provider.ProviderId, StringComparer.Ordinal).ToArray(); array2 = _policies.OrderByDescending((PolicyEntry value) => value.Policy.Priority).ThenBy((PolicyEntry value) => value.Policy.PolicyId, StringComparer.Ordinal).ToArray(); } List> list = new List>(); for (int num = 0; num < array.Length; num++) { IReadOnlyList readOnlyList; try { readOnlyList = array[num].Provider.FindCandidates(request) ?? Array.Empty(); } 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 left, Tuple 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 list2 = new List(); 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(), valid: false, error); } private static IInventoryProvider FindProvider(IEnumerable 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 reservations, IEnumerable 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 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 LoadedPluginGuids() { return new HashSet(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 Handler; internal int Priority; internal long Order; } private readonly object _gate = new object(); private readonly List _subscriptions = new List(); 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 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 _modules = new Dictionary(); 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 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 AllNames; } private readonly object _gate = new object(); private readonly List _entries = new List(); 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()).Select((string value) => Validate(value, "legacyAliases")).Distinct(StringComparer.Ordinal).ToArray(); HashSet hashSet = new HashSet(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 Snapshot() { lock (_gate) { return Array.AsReadOnly((from value in _entries select value.Snapshot into value orderby value.Kind select value).ThenBy((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 Capabilities = new Dictionary(StringComparer.Ordinal); internal readonly Dictionary IncomingSequences = new Dictionary(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 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 _peers = new Dictionary(); private readonly Dictionary _handlers = new Dictionary(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 _maximumPacketBytes; private readonly Func _maximumRpcPerSecond; private readonly Func _handshakeTimeoutSeconds; private readonly Func _enforceRequired; private readonly Func _exactVersionModules; private readonly Func _isServer; private readonly Func _session; private readonly Func _timestamp; private readonly Action _disconnect; private readonly Action _invoke; private ZNetPeer _serverPeer; private int _nextCorrelation; internal NetworkRouter(IModuleRegistry modules, PlayerIdentityService identity, ILifecycleBus lifecycle, ICoreScheduler scheduler, IMetricRegistry metrics, IFeatureCircuitBreaker breakers, ILogSink log, Func maximumPacketBytes, Func maximumRpcPerSecond, Func handshakeTimeoutSeconds, Func enforceRequired, Func exactVersionModules = null, Func isServer = null, Func session = null, Func timestamp = null, Action disconnect = null, Action 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.Empty)); _isServer = isServer ?? ((Func)(() => (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())); _session = session ?? ((Func)(() => ZNet.instance)); _timestamp = timestamp ?? new Func(Stopwatch.GetTimestamp); _disconnect = disconnect ?? ((Action)delegate(ZNetPeer peer) { ZNet.instance.Disconnect(peer); }); _invoke = invoke ?? ((Action)delegate(ZRpc rpc, string name, ZPackage package) { rpc.Invoke(name, new object[1] { package }); }); } public IDisposable Register(NetworkMessageDescriptor descriptor, Action 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 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(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("com.jg224.modcore.Hello", (Action)ReceiveHello); peer.m_rpc.Register("com.jg224.modcore.Ack", (Action)ReceiveAck); peer.m_rpc.Register("com.jg224.modcore.Envelope", (Action)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 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 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 list = ReadModules(package); if (package.GetPos() != package.Size()) { throw new InvalidOperationException("Ack contains trailing data."); } string detail; Dictionary 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 capabilities) { lock (_gate) { state.HandshakeComplete = true; state.RequiredCompatible = compatible; state.Detail = detail; state.Capabilities.Clear(); if (capabilities == null) { return; } foreach (KeyValuePair 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.MessageType), out var value)) { throw new InvalidOperationException("Message is not registered: " + descriptor.Owner.ToString() + "/" + descriptor.MessageType); } if (!DescriptorMatches(descriptor, value.Descriptor)) { throw new InvalidOperationException("Send descriptor differs from its registered message contract."); } if (!OutgoingDirectionAllowed(descriptor.Direction)) { throw new InvalidOperationException("Message direction is not valid for this sender."); } if (!_peers.TryGetValue(peer.m_rpc, out var value2) || !value2.HandshakeComplete || !value2.RequiredCompatible) { return false; } if (descriptor.RequiredCapability != 0L && (!value2.Capabilities.TryGetValue(descriptor.Owner.Value, out var value3) || (value3 & descriptor.RequiredCapability) != descriptor.RequiredCapability)) { return false; } num = ++value2.OutgoingSequence; } ZPackage val = new ZPackage(); val.Write(1); val.Write(descriptor.Owner.Value); val.Write(descriptor.ModuleProtocol); val.Write(descriptor.MessageType); val.Write(correlation); val.Write(num); val.Write((byte)flags); val.Write(array); if (val.Size() > _maximumPacketBytes()) { throw new InvalidOperationException("Envelope exceeds core packet bound."); } peer.m_rpc.Invoke("com.jg224.modcore.Envelope", new object[1] { val }); _metrics.Increment(CoreModuleId, "rpc.sent", 1L); return true; } private ZPackage WriteHandshake() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write("0.5.0"); WriteModules(val); return val; } private bool TryReadHandshake(ZPackage package, out List modules, out string error) { modules = null; try { if (package == null || package.Size() <= 0 || package.Size() > _maximumPacketBytes()) { throw new InvalidOperationException("Handshake size is invalid."); } if (package.ReadInt() != 1) { throw new InvalidOperationException("Core protocol differs."); } ReadBoundedString(package, 64); modules = ReadModules(package); if (package.GetPos() != package.Size()) { throw new InvalidOperationException("Handshake contains trailing data."); } error = string.Empty; return true; } catch (Exception ex) { error = ex.Message; return false; } } private void SendAck(ZRpc rpc, bool compatible, string detail) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(compatible); val.Write(Truncate(detail ?? string.Empty, 1024)); WriteModules(val); _invoke(rpc, "com.jg224.modcore.Ack", val); _metrics.Increment(CoreModuleId, "rpc.sent", 1L); } private void WriteModules(ZPackage package) { IReadOnlyList readOnlyList = _modules.Snapshot(); if (readOnlyList.Count > 256) { throw new InvalidOperationException("Too many modules are registered for the handshake."); } package.Write(readOnlyList.Count); for (int i = 0; i < readOnlyList.Count; i++) { ModuleDescriptor descriptor = readOnlyList[i].Descriptor; package.Write(descriptor.Id.Value); package.Write(descriptor.Version.ToString()); package.Write(descriptor.ProtocolVersion); package.Write((byte)descriptor.Side); package.Write((byte)descriptor.Requirement); package.Write(descriptor.Capabilities); } } private static List ReadModules(ZPackage package) { int num = package.ReadInt(); if (num < 0 || num > 256) { throw new InvalidOperationException("Module count is outside bounds."); } List list = new List(num); HashSet hashSet = new HashSet(); for (int i = 0; i < num; i++) { if (!ModuleId.TryParse(ReadBoundedString(package, 64), out var moduleId) || !hashSet.Add(moduleId)) { throw new InvalidOperationException("Module ID is invalid or duplicated."); } string text = ReadBoundedString(package, 64); if (!SemanticVersion.TryParse(text, out var _)) { throw new InvalidOperationException("Module version is invalid."); } int num2 = package.ReadInt(); ModuleSide moduleSide = (ModuleSide)package.ReadByte(); ModuleRequirement moduleRequirement = (ModuleRequirement)package.ReadByte(); ulong capabilities = package.ReadULong(); if (num2 < 0 || !Enum.IsDefined(typeof(ModuleSide), moduleSide) || !Enum.IsDefined(typeof(ModuleRequirement), moduleRequirement)) { throw new InvalidOperationException("Module metadata is invalid."); } list.Add(new RemoteModule { Id = moduleId, Version = text, Protocol = num2, Side = moduleSide, Requirement = moduleRequirement, Capabilities = capabilities }); } return list; } private bool EvaluateRequired(IReadOnlyList remote, out string detail, out Dictionary capabilities) { IReadOnlyList readOnlyList = _modules.Snapshot(); Dictionary dictionary = remote.ToDictionary((RemoteModule remoteModule2) => remoteModule2.Id, (RemoteModule result) => result); Dictionary dictionary2 = readOnlyList.ToDictionary((ModuleSnapshot moduleSnapshot) => moduleSnapshot.Descriptor.Id, (ModuleSnapshot moduleSnapshot) => moduleSnapshot.Descriptor); List list = new List(); capabilities = new Dictionary(StringComparer.Ordinal); for (int num = 0; num < readOnlyList.Count; num++) { ModuleDescriptor descriptor = readOnlyList[num].Descriptor; if (dictionary.TryGetValue(descriptor.Id, out var value)) { if (ModuleVersionRules.RequiresExact(_exactVersionModules(), descriptor.Id, descriptor.Requirement) && !string.Equals(descriptor.Version.ToString(), value.Version, StringComparison.Ordinal)) { list.Add(descriptor.Id.ToString() + " version " + descriptor.Version.ToString() + " != " + value.Version); } if (value.Protocol == descriptor.ProtocolVersion) { capabilities[descriptor.Id.Value] = value.Capabilities & descriptor.Capabilities; } else if (descriptor.Requirement == ModuleRequirement.RequiredOnBoth) { list.Add(descriptor.Id.ToString() + " protocol " + descriptor.ProtocolVersion + " != " + value.Protocol); } } else if (descriptor.Requirement == ModuleRequirement.RequiredOnBoth) { list.Add("missing remote module " + descriptor.Id); } } for (int num2 = 0; num2 < remote.Count; num2++) { RemoteModule remoteModule = remote[num2]; if (remoteModule.Requirement == ModuleRequirement.RequiredOnBoth) { if (!dictionary2.TryGetValue(remoteModule.Id, out var value2)) { list.Add("missing local module " + remoteModule.Id); } else if (value2.ProtocolVersion != remoteModule.Protocol) { list.Add(remoteModule.Id.ToString() + " protocol " + value2.ProtocolVersion + " != " + remoteModule.Protocol); } } } detail = ((list.Count == 0) ? "compatible" : string.Join("; ", list.ToArray())); return list.Count == 0; } private bool AllowRateLocked(PeerState state) { long num = _timestamp(); if (num - state.RateWindowStarted >= Stopwatch.Frequency) { state.RateWindowStarted = num; state.RateWindowCount = 0; } if (state.RateWindowCount >= _maximumRpcPerSecond()) { return false; } state.RateWindowCount++; return true; } private static bool DirectionAllowed(NetworkDirection direction) { if (direction == NetworkDirection.Bidirectional) { return true; } if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer()) { return direction == NetworkDirection.ServerToClient; } return direction == NetworkDirection.ClientToServer; } private static bool OutgoingDirectionAllowed(NetworkDirection direction) { if (direction == NetworkDirection.Bidirectional) { return true; } if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer()) { return direction == NetworkDirection.ClientToServer; } return direction == NetworkDirection.ServerToClient; } private static bool DescriptorMatches(NetworkMessageDescriptor left, NetworkMessageDescriptor right) { if (left.Owner == right.Owner && left.MessageType == right.MessageType && left.ModuleProtocol == right.ModuleProtocol && left.Direction == right.Direction && left.MaximumPayloadBytes == right.MaximumPayloadBytes) { return left.RequiredCapability == right.RequiredCapability; } return false; } private static string ReadBoundedString(ZPackage package, int maximum) { string text = package.ReadString(); if (text == null || text.Length > maximum) { throw new InvalidOperationException("String exceeds its bound."); } return text; } private static string HandlerKey(ModuleId owner, ushort messageType) { return owner.Value + ":" + messageType; } private static string Truncate(string value, int maximum) { if (value.Length > maximum) { return value.Substring(0, maximum); } return value; } } internal sealed class PlayerIdentityService : IPlayerIdentityService { private readonly object _gate = new object(); private readonly Dictionary _peers = new Dictionary(); internal void Connected(ZNetPeer peer) { if (peer?.m_rpc == null) { return; } lock (_gate) { _peers[peer.m_rpc] = peer; } } internal void Disconnected(ZNetPeer peer) { if (peer?.m_rpc == null) { return; } lock (_gate) { _peers.Remove(peer.m_rpc); } } internal void Reset() { lock (_gate) { _peers.Clear(); } } public bool TryGetPeer(long peerId, out PlayerIdentity identity) { ZNetPeer peer; lock (_gate) { peer = ((IEnumerable)_peers.Values).FirstOrDefault((Func)((ZNetPeer value) => value != null && value.m_uid == peerId)); } identity = Create(peer); if (identity != null) { return identity.IsResolved; } return false; } public bool TryGetByConnection(object connection, out PlayerIdentity identity) { ZNetPeer value = null; ZRpc rpc = (ZRpc)((connection is ZRpc) ? connection : null); if (rpc != null) { lock (_gate) { _peers.TryGetValue(rpc, out value); } if (value == null && (Object)(object)ZNet.instance != (Object)null) { value = ((IEnumerable)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func)((ZNetPeer val2) => val2?.m_rpc == rpc)); } } else { ZNetPeer val = (ZNetPeer)((connection is ZNetPeer) ? connection : null); if (val != null) { value = val; } } identity = Create(value); if (identity != null) { return identity.IsResolved; } return false; } public IReadOnlyList Snapshot() { ZNetPeer[] source; lock (_gate) { source = _peers.Values.Where((ZNetPeer value) => value != null).Distinct().ToArray(); } return Array.AsReadOnly((from value in source.Select(Create) where value != null orderby value.PeerId select value).ToArray()); } private static PlayerIdentity Create(ZNetPeer peer) { //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) if (peer == null) { return null; } string text = string.Empty; try { ISocket socket = peer.m_socket; object obj = ((socket != null) ? socket.GetHostName() : null); if (obj == null) { ZRpc rpc = peer.m_rpc; if (rpc == null) { obj = null; } else { ISocket socket2 = rpc.GetSocket(); obj = ((socket2 != null) ? socket2.GetHostName() : null); } if (obj == null) { obj = string.Empty; } } text = (string)obj; } catch { } bool isAdmin = false; try { isAdmin = (Object)(object)ZNet.instance != (Object)null && !string.IsNullOrEmpty(text) && ZNet.instance.IsAdmin(text); } catch { } string persistentId = string.Empty; string text2 = peer.m_playerName ?? string.Empty; try { if (peer.m_characterID != ZDOID.None && (Object)(object)ZNetScene.instance != (Object)null) { GameObject obj4 = ZNetScene.instance.FindInstance(peer.m_characterID); Player val = ((obj4 != null) ? obj4.GetComponent() : null); if ((Object)(object)val != (Object)null) { persistentId = val.GetPlayerID().ToString(CultureInfo.InvariantCulture); text2 = val.GetPlayerName() ?? text2; } } } catch { } bool resolved = peer.m_uid != 0L && peer.m_rpc != null; return new PlayerIdentity(peer.m_uid, persistentId, text2, text, isAdmin, peer.m_server, resolved); } } internal sealed class UiRegistry : IUiRegistry { private readonly object _gate = new object(); private readonly List _reservations = new List(); public IDisposable Reserve(UiReservation reservation) { if (reservation == null) { throw new ArgumentNullException("reservation"); } lock (_gate) { UiReservation uiReservation = _reservations.FirstOrDefault((UiReservation value) => value.Surface == reservation.Surface && string.Equals(value.Slot, reservation.Slot, StringComparison.Ordinal) && (!value.Stackable || !reservation.Stackable)); if (uiReservation != null) { throw new InvalidOperationException("UI slot " + reservation.Surface.ToString() + "/" + reservation.Slot + " is exclusively reserved by " + uiReservation.Owner.ToString() + "."); } _reservations.Add(reservation); } return new Registration(delegate { lock (_gate) { _reservations.Remove(reservation); } }); } public IReadOnlyList Snapshot() { lock (_gate) { return Array.AsReadOnly(_reservations.OrderBy((UiReservation value) => value.Surface).ThenBy((UiReservation value) => value.Slot, StringComparer.Ordinal).ThenByDescending((UiReservation value) => value.Priority) .ThenBy((UiReservation value) => value.Owner) .ToArray()); } } } internal sealed class InputRegistry : IInputRegistry { private readonly object _gate = new object(); private readonly List _actions = new List(); public bool GameOwnsTextInput { get { try { if (TextInput.IsVisible() || Console.IsVisible() || Menu.IsVisible() || StoreGui.IsVisible()) { return true; } if ((Object)(object)Chat.instance != (Object)null && Chat.instance.HasFocus()) { return true; } return (Object)(object)Minimap.instance != (Object)null && Minimap.InTextInput(); } catch { return true; } } } public IDisposable Register(InputActionDescriptor action) { if (action == null) { throw new ArgumentNullException("action"); } lock (_gate) { if (_actions.Any((InputActionDescriptor value) => value.Owner == action.Owner && string.Equals(value.ActionId, action.ActionId, StringComparison.Ordinal))) { throw new InvalidOperationException("Input action already registered: " + action.Owner.ToString() + "/" + action.ActionId); } _actions.Add(action); } return new Registration(delegate { lock (_gate) { _actions.Remove(action); } }); } public IReadOnlyList Snapshot() { lock (_gate) { return Array.AsReadOnly(_actions.OrderBy((InputActionDescriptor value) => value.Context, StringComparer.OrdinalIgnoreCase).ThenBy((InputActionDescriptor value) => value.Binding, StringComparer.OrdinalIgnoreCase).ThenBy((InputActionDescriptor value) => value.Owner) .ToArray()); } } public IReadOnlyList Collisions() { InputActionDescriptor[] array; lock (_gate) { array = _actions.ToArray(); } List list = new List(); for (int i = 0; i < array.Length; i++) { for (int j = i + 1; j < array.Length; j++) { if (string.Equals(array[i].Binding, array[j].Binding, StringComparison.OrdinalIgnoreCase) && string.Equals(array[i].Context, array[j].Context, StringComparison.OrdinalIgnoreCase)) { list.Add(new InputCollision(array[i], array[j])); } } } return list.AsReadOnly(); } } internal sealed class LocalizationRegistry : ILocalizationRegistry { private sealed class Entry { internal ModuleId Owner; internal string Value; } private readonly object _gate = new object(); private readonly Dictionary _entries = new Dictionary(StringComparer.Ordinal); public IDisposable Register(ModuleId owner, string key, string fallbackEnglish) { key = Guard.Bounded(key, "key", 128); fallbackEnglish = Guard.Bounded(fallbackEnglish, "fallbackEnglish", 2048); Entry entry = new Entry { Owner = owner, Value = fallbackEnglish }; lock (_gate) { if (_entries.ContainsKey(key)) { throw new InvalidOperationException("Localization key already registered: " + key); } _entries.Add(key, entry); } return new Registration(delegate { lock (_gate) { if (_entries.TryGetValue(key, out var value) && value == entry) { _entries.Remove(key); } } }); } public string Resolve(string key, params object[] arguments) { string text; lock (_gate) { text = (_entries.TryGetValue(key, out var value) ? value.Value : key); } if (arguments == null || arguments.Length == 0) { return text; } try { return string.Format(CultureInfo.CurrentCulture, text, arguments); } catch (FormatException) { return text; } } } internal sealed class NotificationService : INotificationService { private readonly object _gate = new object(); private readonly Dictionary _lastSent = new Dictionary(StringComparer.Ordinal); private readonly ILogSink _log; internal NotificationService(ILogSink log) { _log = log ?? NullLogSink.Instance; } public bool Publish(Notification notification) { if (notification == null) { throw new ArgumentNullException("notification"); } string key = notification.Owner.ToString() + "\n" + notification.Channel.ToString() + "\n" + notification.PeerId + "\n" + notification.DeduplicationKey; long timestamp = Stopwatch.GetTimestamp(); long num = (long)(notification.Cooldown.TotalSeconds * (double)Stopwatch.Frequency); lock (_gate) { if (num > 0 && _lastSent.TryGetValue(key, out var value) && timestamp - value < num) { return false; } _lastSent[key] = timestamp; if (_lastSent.Count > 4096) { long cutoff = timestamp - Stopwatch.Frequency * 86400; string[] array = (from pair in _lastSent where pair.Value < cutoff select pair.Key).ToArray(); foreach (string key2 in array) { _lastSent.Remove(key2); } } } try { switch (notification.Channel) { case NotificationChannel.LocalCenter: case NotificationChannel.ArenaOrArea: { MessageHud instance2 = MessageHud.instance; if (instance2 != null) { instance2.ShowMessage((MessageType)2, notification.Message, 0, (Sprite)null, false, true); } return (Object)(object)MessageHud.instance != (Object)null; } case NotificationChannel.LocalStatus: { MessageHud instance = MessageHud.instance; if (instance != null) { instance.ShowMessage((MessageType)1, notification.Message, 0, (Sprite)null, false, true); } return (Object)(object)MessageHud.instance != (Object)null; } case NotificationChannel.PrivatePeer: if (ZRoutedRpc.instance == null || notification.PeerId == 0L) { return false; } ZRoutedRpc.instance.InvokeRoutedRPC(notification.PeerId, "ShowMessage", new object[2] { 2, notification.Message }); return true; case NotificationChannel.ServerWide: return Broadcast(notification.Message); case NotificationChannel.Diagnostics: _log.Info("[notification] " + notification.Message); return true; default: return false; } } catch (Exception exception) { _log.Error("Notification delivery failed for " + notification.Owner.ToString() + ".", exception); return false; } } private static bool Broadcast(string message) { if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { return false; } bool result = false; List connectedPeers = ZNet.instance.GetConnectedPeers(); for (int i = 0; i < connectedPeers.Count; i++) { ZNetPeer val = connectedPeers[i]; if (val != null) { ZRoutedRpc.instance.InvokeRoutedRPC(val.m_uid, "ShowMessage", new object[2] { 2, message }); result = true; } } if ((Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, message, 0, (Sprite)null, false, true); result = true; } return result; } } internal sealed class RoutedRpcIngressRegistry : IDisposable { private sealed class Entry { internal ModuleId Owner; internal string Name; } internal readonly struct Endpoint { internal bool IsServer { get; } internal long LocalPeerId { get; } internal long ServerPeerId { get; } internal Endpoint(bool isServer, long localPeerId, long serverPeerId) { IsServer = isServer; LocalPeerId = localPeerId; ServerPeerId = serverPeerId; } } private readonly object _gate = new object(); private readonly Dictionary _methods = new Dictionary(); private readonly IPlayerIdentityService _identity; private readonly Func _endpoint; private bool _disposed; internal RoutedRpcIngressRegistry(IPlayerIdentityService identity, Func endpoint) { _identity = identity ?? throw new ArgumentNullException("identity"); _endpoint = endpoint ?? throw new ArgumentNullException("endpoint"); } internal IDisposable Register(ModuleId owner, params string[] methodNames) { if (owner.IsEmpty) { throw new ArgumentException("An owner is required.", "owner"); } if (methodNames == null || methodNames.Length == 0 || methodNames.Length > 256) { throw new ArgumentException("Register between one and 256 exact method names.", "methodNames"); } Dictionary additions = new Dictionary(); foreach (string text in methodNames) { if (string.IsNullOrWhiteSpace(text) || text.Length > 256 || text.IndexOfAny(new char[3] { '\r', '\n', '\0' }) >= 0) { throw new ArgumentException("An RPC name is invalid.", "methodNames"); } int stableHashCode = StringExtensionMethods.GetStableHashCode(text); if (additions.ContainsKey(stableHashCode)) { throw new InvalidOperationException("Duplicate or colliding routed RPC name: " + text); } additions.Add(stableHashCode, new Entry { Owner = owner, Name = text }); } lock (_gate) { if (_disposed) { throw new ObjectDisposedException("RoutedRpcIngressRegistry"); } foreach (KeyValuePair item in additions) { if (_methods.TryGetValue(item.Key, out var value)) { throw new InvalidOperationException("Routed RPC '" + item.Value.Name + "' collides with '" + value.Name + "' owned by " + value.Owner.ToString() + "."); } } foreach (KeyValuePair item2 in additions) { _methods.Add(item2.Key, item2.Value); } } return new Registration(delegate { lock (_gate) { foreach (KeyValuePair item3 in additions) { if (_methods.TryGetValue(item3.Key, out var value2) && value2 == item3.Value) { _methods.Remove(item3.Key); } } } }); } internal bool Allows(object connection, ZPackage package) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) if (package == null) { return true; } int pos = package.GetPos(); if (package.Size() - pos < 40) { return true; } bool flag = false; try { package.ReadLong(); long num = package.ReadLong(); long num2 = package.ReadLong(); package.ReadZDOID(); int key = package.ReadInt(); lock (_gate) { if (!_methods.ContainsKey(key)) { return true; } } flag = true; if (package.Size() - package.GetPos() < 4) { return false; } int num3 = package.ReadInt(); if (num3 < 0 || num3 != package.Size() - package.GetPos()) { return false; } if (connection == null || !_identity.TryGetByConnection(connection, out var identity) || identity == null || !identity.IsResolved || identity.PeerId == 0L || num == 0L) { return false; } Endpoint endpoint = _endpoint(); if (endpoint.LocalPeerId == 0L) { return false; } if (endpoint.IsServer) { return num == identity.PeerId; } return endpoint.ServerPeerId != 0L && identity.IsServer && identity.PeerId == endpoint.ServerPeerId && (num2 == endpoint.LocalPeerId || num2 == 0); } catch (Exception) when (flag) { return false; } finally { package.SetPos(pos); } } public void Dispose() { lock (_gate) { _disposed = true; _methods.Clear(); } } } internal sealed class RulePipelineRegistry : IRulePipelineRegistry { private sealed class Entry { internal Type ContextType; internal Type DecisionType; internal object Pipeline; } private readonly object _gate = new object(); private readonly Dictionary _pipelines = new Dictionary(StringComparer.Ordinal); private readonly IFeatureCircuitBreaker _breakers; private readonly ILogSink _log; public IReadOnlyList PipelineIds { get { lock (_gate) { return Array.AsReadOnly(_pipelines.Keys.OrderBy((string value) => value, StringComparer.Ordinal).ToArray()); } } } internal RulePipelineRegistry(IFeatureCircuitBreaker breakers, ILogSink log) { _breakers = breakers; _log = log; } public RulePipeline GetOrCreate(string pipelineId) { if (string.IsNullOrWhiteSpace(pipelineId) || pipelineId.Length > 128) { throw new ArgumentException("pipelineId"); } lock (_gate) { if (_pipelines.TryGetValue(pipelineId, out var value)) { if (value.ContextType != typeof(TContext) || value.DecisionType != typeof(TDecision)) { throw new InvalidOperationException("Pipeline '" + pipelineId + "' was requested with different types."); } return (RulePipeline)value.Pipeline; } RulePipeline rulePipeline = new RulePipeline(pipelineId, _breakers, _log); _pipelines.Add(pipelineId, new Entry { ContextType = typeof(TContext), DecisionType = typeof(TDecision), Pipeline = rulePipeline }); return rulePipeline; } } } internal sealed class StatusOverlay { private bool _visible; private string _report = string.Empty; private Vector2 _scroll; private GUIStyle _style; internal void Toggle(CoreServices services) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) if (_visible) { Close(); return; } _report = CompatibilityReport.Build(services); _scroll = Vector2.zero; _visible = true; } internal void Close() { _visible = false; _report = string.Empty; } internal void Draw(CoreServices services) { //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Expected O, but got Unknown //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown if (!_visible || services == null || !Object.op_Implicit((Object)(object)Player.m_localPlayer)) { if (!Object.op_Implicit((Object)(object)Player.m_localPlayer)) { Close(); } return; } if (_style == null) { _style = new GUIStyle(GUI.skin.label) { wordWrap = true, richText = false, fontSize = 14 }; } float num = Mathf.Min(740f, (float)Screen.width - 32f); float num2 = Mathf.Min(700f, (float)Screen.height - 32f); Rect val = default(Rect); ((Rect)(ref val))..ctor(((float)Screen.width - num) * 0.5f, ((float)Screen.height - num2) * 0.5f, num, num2); GUI.Box(val, "ModCore — press the status shortcut again to close"); Rect val2 = default(Rect); ((Rect)(ref val2))..ctor(((Rect)(ref val)).x + 12f, ((Rect)(ref val)).y + 30f, num - 24f, num2 - 42f); float num3 = _style.CalcHeight(new GUIContent(_report), ((Rect)(ref val2)).width - 22f); _scroll = GUI.BeginScrollView(val2, _scroll, new Rect(0f, 0f, ((Rect)(ref val2)).width - 22f, num3)); GUI.Label(new Rect(0f, 0f, ((Rect)(ref val2)).width - 22f, num3), _report, _style); GUI.EndScrollView(); } } internal sealed class MainThreadDispatcher : IMainThreadDispatcher { private sealed class WorkItem { internal ModuleId Owner; internal int Generation; internal Action Action; } private readonly int _threadId; private readonly int _maximumPending; private readonly ConcurrentQueue _queue = new ConcurrentQueue(); private readonly ConcurrentDictionary _generations = new ConcurrentDictionary(); private readonly ILogSink _log; private int _pending; public bool IsMainThread => Thread.CurrentThread.ManagedThreadId == _threadId; public int PendingCount => Volatile.Read(in _pending); internal MainThreadDispatcher(ILogSink log, int maximumPending) { if (maximumPending < 64 || maximumPending > 100000) { throw new ArgumentOutOfRangeException("maximumPending"); } _threadId = Thread.CurrentThread.ManagedThreadId; _maximumPending = maximumPending; _log = log ?? NullLogSink.Instance; } public bool Post(ModuleId owner, Action action) { if (owner.IsEmpty) { throw new ArgumentException("owner"); } if (action == null) { throw new ArgumentNullException("action"); } if (Interlocked.Increment(ref _pending) > _maximumPending) { Interlocked.Decrement(ref _pending); _log.Warning("Main-thread queue is full; rejected work from " + owner.ToString() + "."); return false; } int orAdd = _generations.GetOrAdd(owner, 0); _queue.Enqueue(new WorkItem { Owner = owner, Generation = orAdd, Action = action }); return true; } public int Drain(int maximumActions = 256) { if (!IsMainThread) { throw new InvalidOperationException("Main-thread work can only be drained on the main thread."); } if (maximumActions <= 0) { throw new ArgumentOutOfRangeException("maximumActions"); } int num = 0; WorkItem result; while (num < maximumActions && _queue.TryDequeue(out result)) { Interlocked.Decrement(ref _pending); num++; if (_generations.GetOrAdd(result.Owner, 0) == result.Generation) { try { result.Action(); } catch (Exception exception) { _log.Error("Main-thread action failed for " + result.Owner.ToString() + ".", exception); } } } return num; } public void CancelOwner(ModuleId owner) { _generations.AddOrUpdate(owner, 1, (ModuleId _, int current) => current + 1); } } internal sealed class CoreScheduler : ICoreScheduler, IDisposable { private sealed class ScheduledItem : IDisposable { internal ModuleId Owner; internal Action Action; internal SchedulerTarget Target; internal long DueTicks; internal long RepeatTicks; internal CancellationTokenSource Cancellation; internal Timer Timer; internal CoreScheduler Scheduler; internal int Disposed; public void Dispose() { if (Interlocked.Exchange(ref Disposed, 1) == 0) { Cancellation.Cancel(); Timer?.Dispose(); Scheduler?.Remove(this); Cancellation.Dispose(); } } } private readonly object _gate = new object(); private readonly List _items = new List(); private readonly IMainThreadDispatcher _dispatcher; private readonly ILogSink _log; private bool _disposed; internal CoreScheduler(IMainThreadDispatcher dispatcher, ILogSink log) { _dispatcher = dispatcher; _log = log ?? NullLogSink.Instance; } public IDisposable Schedule(ModuleId owner, TimeSpan delay, Action action, SchedulerTarget target = SchedulerTarget.MainThread, TimeSpan? repeat = null) { if (owner.IsEmpty) { throw new ArgumentException("owner"); } if (action == null) { throw new ArgumentNullException("action"); } if (delay < TimeSpan.Zero || delay > TimeSpan.FromDays(365.0)) { throw new ArgumentOutOfRangeException("delay"); } TimeSpan timeSpan = repeat ?? TimeSpan.Zero; if (timeSpan < TimeSpan.Zero || timeSpan > TimeSpan.FromDays(365.0)) { throw new ArgumentOutOfRangeException("repeat"); } ScheduledItem item = new ScheduledItem { Owner = owner, Action = action, Target = target, DueTicks = Stopwatch.GetTimestamp() + ToStopwatchTicks(delay), RepeatTicks = ToStopwatchTicks(timeSpan), Cancellation = new CancellationTokenSource(), Scheduler = this }; lock (_gate) { if (_disposed) { throw new ObjectDisposedException("CoreScheduler"); } _items.Add(item); } if (target == SchedulerTarget.Background) { item.Timer = new Timer(delegate { RunBackground(item); }, null, delay, (timeSpan > TimeSpan.Zero) ? timeSpan : Timeout.InfiniteTimeSpan); } return item; } public void CancelOwner(ModuleId owner) { ScheduledItem[] array; lock (_gate) { array = _items.Where((ScheduledItem value) => value.Owner == owner).ToArray(); } for (int num = 0; num < array.Length; num++) { array[num].Dispose(); } _dispatcher.CancelOwner(owner); } public void Tick() { long now = Stopwatch.GetTimestamp(); ScheduledItem[] array; lock (_gate) { if (_disposed) { return; } array = _items.Where((ScheduledItem value) => value.Target == SchedulerTarget.MainThread && Volatile.Read(in value.Disposed) == 0 && now >= value.DueTicks).ToArray(); for (int num = 0; num < array.Length; num++) { if (array[num].RepeatTicks > 0) { array[num].DueTicks = now + array[num].RepeatTicks; } } } foreach (ScheduledItem scheduledItem in array) { try { scheduledItem.Action(scheduledItem.Cancellation.Token); } catch (Exception exception) { _log.Error("Scheduled main-thread action failed for " + scheduledItem.Owner.ToString() + ".", exception); } if (scheduledItem.RepeatTicks <= 0) { scheduledItem.Dispose(); } } } public void Dispose() { ScheduledItem[] array; lock (_gate) { if (_disposed) { return; } _disposed = true; array = _items.ToArray(); } for (int i = 0; i < array.Length; i++) { array[i].Dispose(); } } private void RunBackground(ScheduledItem item) { if (Volatile.Read(in item.Disposed) == 0 && !item.Cancellation.IsCancellationRequested) { try { item.Action(item.Cancellation.Token); } catch (Exception exception) { _log.Error("Scheduled background action failed for " + item.Owner.ToString() + ".", exception); } if (item.RepeatTicks <= 0) { item.Dispose(); } } } private void Remove(ScheduledItem item) { lock (_gate) { _items.Remove(item); } } private static long ToStopwatchTicks(TimeSpan value) { if (value <= TimeSpan.Zero) { return 0L; } return checked((long)(value.TotalSeconds * (double)Stopwatch.Frequency)); } } } namespace JG224.ModCore.Patches { [HarmonyPatch(typeof(Game), "Awake")] internal static class GameAwakePatch { [HarmonyPostfix] private static void Postfix(Game __instance) { Plugin.Runtime?.OnGameAwake(__instance); } } [HarmonyPatch(typeof(Game), "OnDestroy")] internal static class GameDestroyPatch { [HarmonyPrefix] private static void Prefix(Game __instance) { Plugin.Runtime?.OnGameDestroyed(__instance); } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class NetworkAwakePatch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.NetworkStarting, __instance, 0L); } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] internal static class NetworkConnectionPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { Plugin.Runtime?.Services.NetworkRuntime.OnNewConnection(__instance, peer); } } [HarmonyPatch(typeof(ZNet), "Disconnect")] internal static class NetworkDisconnectPatch { [HarmonyPrefix] private static void Prefix(ZNetPeer peer) { Plugin.Runtime?.Services.NetworkRuntime.OnDisconnect(peer); } } [HarmonyPatch(typeof(ZNet), "Shutdown")] internal static class NetworkShutdownPatch { [HarmonyPrefix] private static void Prefix(ZNet __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.NetworkStopping, __instance, 0L); Plugin.Runtime?.Services.NetworkRuntime.OnShutdown(); } } [HarmonyPatch(typeof(ZNet), "Update")] internal static class DedicatedServerTickPatch { [HarmonyPostfix] private static void Postfix() { Plugin.Runtime?.Tick(); } } [HarmonyPatch(typeof(Player), "SetLocalPlayer")] internal static class LocalPlayerPatch { [HarmonyPostfix] private static void Postfix(Player __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.LocalPlayerReady, __instance, 0L); } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class PlayerSpawnedPatch { [HarmonyPostfix] private static void Postfix(Player __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.LocalPlayerRespawned, __instance, 0L); } } [HarmonyPatch(typeof(Player), "OnDestroy")] internal static class PlayerDestroyedPatch { [HarmonyPrefix] private static void Prefix(Player __instance) { if (Player.m_localPlayer == __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.LocalPlayerDestroyed, __instance, 0L); } } } [HarmonyPatch(typeof(Hud), "Awake")] internal static class HudReadyPatch { [HarmonyPostfix] private static void Postfix(Hud __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.HudReady, __instance, 0L); } } [HarmonyPatch(typeof(Hud), "OnDestroy")] internal static class HudDestroyedPatch { [HarmonyPrefix] private static void Prefix(Hud __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.HudDestroyed, __instance, 0L); } } [HarmonyPatch(typeof(InventoryGui), "Awake")] internal static class InventoryReadyPatch { [HarmonyPostfix] private static void Postfix(InventoryGui __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.InventoryReady, __instance, 0L); } } [HarmonyPatch(typeof(InventoryGui), "OnDestroy")] internal static class InventoryDestroyedPatch { [HarmonyPrefix] private static void Prefix(InventoryGui __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.InventoryDestroyed, __instance, 0L); } } [HarmonyPatch(typeof(ObjectDB), "Awake")] internal static class ObjectDbReadyPatch { [HarmonyPostfix] private static void Postfix(ObjectDB __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.ObjectDbReady, __instance, 0L); } } [HarmonyPatch(typeof(ZNetScene), "Awake")] internal static class NetworkSceneReadyPatch { [HarmonyPostfix] private static void Postfix(ZNetScene __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.NetworkSceneReady, __instance, 0L); } } [HarmonyPatch(typeof(ZNetScene), "OnDestroy")] internal static class NetworkSceneDestroyedPatch { [HarmonyPrefix] private static void Prefix(ZNetScene __instance) { Plugin.Runtime?.Publish(LifecycleEventKind.NetworkSceneDestroyed, __instance, 0L); } } [HarmonyPatch(typeof(ZRoutedRpc), "RPC_RoutedRPC")] internal static class RoutedRpcIngressPatch { internal static bool IsInstalled { get { MethodInfo methodInfo = AccessTools.Method(typeof(ZRoutedRpc), "RPC_RoutedRPC", (Type[])null, (Type[])null); MethodInfo prefix = AccessTools.Method(typeof(RoutedRpcIngressPatch), "Prefix", (Type[])null, (Type[])null); if (methodInfo != null) { return Harmony.GetPatchInfo((MethodBase)methodInfo)?.Prefixes.Any((Patch patch) => patch.owner == "com.jg224.modcore" && patch.PatchMethod == prefix) ?? false; } return false; } } [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(ZRpc rpc, ZPackage pkg) { return Plugin.Runtime?.Services.RoutedIngress.Allows(rpc, pkg) ?? true; } } } namespace JG224.ModCore.API { public sealed class AuthoritativePolicyChannel : IDisposable { private readonly ICoreServices _core; private readonly PolicyDescriptor _policy; private readonly NetworkMessageDescriptor _request; private readonly NetworkMessageDescriptor _snapshot; private readonly Func _readLocal; private readonly Action _apply; private readonly Action _report; private readonly List _leases = new List(); private readonly Dictionary _lastRequest = new Dictionary(); private readonly List _expiredRequests = new List(); private IDisposable _policyLease; private byte[] _current; private byte[] _rejected; private byte[] _pending; private long _revision; private long _acceptedRevision; private long _pendingRevision; private double _now; private double _nextRequest; private double _nextApplyAttempt; private bool _connected; private bool _server; private bool _disposed; public bool HasPolicy { get { if (_current != null) { return _connected; } return false; } } public AuthoritativePolicyChannel(ICoreServices core, PolicyDescriptor policy, int moduleProtocol, ushort requestType, ushort snapshotType, Func readLocal, Action apply, Action report = null) { _core = core ?? throw new ArgumentNullException("core"); _policy = policy ?? throw new ArgumentNullException("policy"); _readLocal = readLocal ?? throw new ArgumentNullException("readLocal"); _apply = apply ?? throw new ArgumentNullException("apply"); _report = report ?? ((Action)delegate { }); if (requestType == snapshotType) { throw new ArgumentException("Message types must differ."); } _request = new NetworkMessageDescriptor(policy.Owner, requestType, moduleProtocol, NetworkDirection.ClientToServer, 1, 0uL); _snapshot = new NetworkMessageDescriptor(policy.Owner, snapshotType, moduleProtocol, NetworkDirection.ServerToClient, checked(policy.MaximumBytes + 16), 0uL); try { _policyLease = core.Policies.Register(policy); _leases.Add(core.Network.Register(_request, OnRequest)); _leases.Add(core.Network.Register(_snapshot, OnSnapshot)); _leases.Add(core.Lifecycle.Subscribe(policy.Owner, LifecycleEventKind.NetworkStopping, delegate { Reset(); })); _leases.Add(core.Lifecycle.Subscribe(policy.Owner, LifecycleEventKind.WorldUnloading, delegate { Reset(); })); _leases.Add(core.Lifecycle.Subscribe(policy.Owner, LifecycleEventKind.PeerDisconnected, delegate(LifecycleEvent disconnected) { _lastRequest.Remove(disconnected.PeerId); })); } catch { Dispose(); throw; } } public void Tick(double now, bool connected, bool server) { //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_018f: Expected O, but got Unknown if (_disposed) { return; } if (!connected) { if (_connected) { Reset(); } return; } if (_connected && _server != server) { Reset(); } _connected = true; _server = server; _now = now; if (server) { byte[] array = _readLocal(); if (_pending != null) { if (Equal(array, _pending)) { if (now >= _nextApplyAttempt && ApplyPending()) { _core.Network.Broadcast(_snapshot, Package(), 0, NetworkMessageFlags.Ordered); } return; } _pending = null; _pendingRevision = 0L; _nextApplyAttempt = 0.0; } if ((!Equal(array, _current) || _revision != _acceptedRevision) && !Equal(array, _rejected)) { long revision = checked(_acceptedRevision + 1); if (Accept(revision, array)) { _core.Network.Broadcast(_snapshot, Package(), 0, NetworkMessageFlags.Ordered); } } } else { if (_pending != null && now >= _nextApplyAttempt) { ApplyPending(); } if (now >= _nextRequest) { _nextRequest = now + (HasPolicy ? 30.0 : 3.0); _core.Network.SendToServer(_request, new ZPackage(), _core.Network.NextCorrelationId(), NetworkMessageFlags.Request); } } } public void Reset() { if (!_disposed) { _connected = false; _current = (_rejected = (_pending = null)); _revision = (_acceptedRevision = (_pendingRevision = 0L)); _nextRequest = (_nextApplyAttempt = 0.0); _lastRequest.Clear(); _expiredRequests.Clear(); _policyLease?.Dispose(); _policyLease = _core.Policies.Register(_policy); } } private void OnRequest(NetworkMessageContext context, ZPackage payload) { if (!_connected || !_server || !HasPolicy || payload.Size() != 0 || (_lastRequest.TryGetValue(context.PeerId, out var value) && _now - value < 1.0)) { return; } if (_lastRequest.Count >= 128 && !_lastRequest.ContainsKey(context.PeerId)) { _expiredRequests.Clear(); foreach (KeyValuePair item in _lastRequest) { if (_now - item.Value >= 1.0) { _expiredRequests.Add(item.Key); } } for (int i = 0; i < _expiredRequests.Count; i++) { _lastRequest.Remove(_expiredRequests[i]); } if (_lastRequest.Count >= 128) { return; } } _lastRequest[context.PeerId] = _now; _core.Network.SendToPeer(context.PeerId, _snapshot, Package(), context.CorrelationId, NetworkMessageFlags.Reply); } private void OnSnapshot(NetworkMessageContext context, ZPackage payload) { if (!_connected || _server) { return; } try { long num = payload.ReadLong(); int num2 = payload.ReadInt(); if (num2 < 0 || num2 > _policy.MaximumBytes || num2 != payload.Size() - payload.GetPos()) { _report("Invalid policy payload length rejected."); return; } payload.SetPos(8); byte[] bytes = payload.ReadByteArray(); if (payload.GetPos() != payload.Size()) { _report("Trailing policy data rejected."); } else if (num > _revision && (_pending == null || num > _pendingRevision)) { Accept(num, bytes); } } catch (Exception ex) { _report("Malformed policy snapshot rejected: " + ex.Message); } } private bool Accept(long revision, byte[] bytes) { if (!_core.Policies.TryApply(_policy.Owner, revision, bytes, authoritative: true, out var snapshot, out var error) && (snapshot == null || snapshot.Revision != revision || !Equal(snapshot.Payload, bytes))) { if (snapshot != null) { _acceptedRevision = Math.Max(_acceptedRevision, snapshot.Revision); } _report("Policy rejected: " + error); _rejected = ((bytes == null || bytes.Length > _policy.MaximumBytes) ? null : ((byte[])bytes.Clone())); return false; } _acceptedRevision = Math.Max(_acceptedRevision, revision); _pending = (byte[])bytes.Clone(); _pendingRevision = revision; _rejected = null; return ApplyPending(); } private bool ApplyPending() { byte[] pending = _pending; if (pending == null) { return false; } long pendingRevision = _pendingRevision; try { _apply((byte[])pending.Clone()); } catch (Exception ex) { _nextApplyAttempt = _now + 3.0; _report("Policy application failed; retry pending: " + ex.Message); return false; } if (_disposed || !_connected || _pending != pending) { return false; } _current = pending; _revision = pendingRevision; _pending = null; _pendingRevision = 0L; _nextApplyAttempt = 0.0; if (!_server) { _nextRequest = _now + 30.0; } return true; } private ZPackage Package() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(_revision); val.Write(_current); return val; } private static bool Equal(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } for (int i = 0; i < left.Length; i++) { if (left[i] != right[i]) { return false; } } return true; } public void Dispose() { if (!_disposed) { _disposed = true; _connected = false; _current = (_rejected = (_pending = null)); _revision = (_acceptedRevision = (_pendingRevision = 0L)); _lastRequest.Clear(); _expiredRequests.Clear(); for (int num = _leases.Count - 1; num >= 0; num--) { _leases[num].Dispose(); } _leases.Clear(); _policyLease?.Dispose(); _policyLease = null; } } } public static class CombatPipelineIds { public const string Damage = "combat.damage"; public const string Block = "combat.block"; public const string Death = "combat.death"; public const string Durability = "combat.durability"; public const string SkillGain = "combat.skill-gain"; } public static class CombatRuleStages { public const int Validate = -1000; public const int Context = -500; public const int Gameplay = 0; public const int Integration = 500; public new const int Finalize = 1000; } public enum DamageRulePhase : byte { Incoming = 1, BeforeBlock, BeforeApply, AfterApply } public enum BlockRulePhase : byte { BeforeResolution = 1, AfterResolution } public enum CombatPolicyOverride : byte { Default, Allow, Suppress } public enum DurabilityUseKind : byte { Unknown, Attack, Block, Projectile, Tool, ArenaLoadout, Scripted } public sealed class CombatActor { public static readonly CombatActor None = new CombatActor(string.Empty, null, string.Empty, string.Empty, isPlayer: false, isBoss: false, allowEmpty: true); public string ActorId { get; } public Character NativeCharacter { get; } public string PrefabName { get; } public string DisplayName { get; } public bool IsPlayer { get; } public bool IsBoss { get; } public bool IsNone => ActorId.Length == 0; public CombatActor(string actorId, Character nativeCharacter = null, string prefabName = "", string displayName = "", bool isPlayer = false, bool isBoss = false) : this(actorId, nativeCharacter, prefabName, displayName, isPlayer, isBoss, allowEmpty: false) { } private CombatActor(string actorId, Character nativeCharacter, string prefabName, string displayName, bool isPlayer, bool isBoss, bool allowEmpty) { ActorId = Guard.Bounded(actorId ?? string.Empty, "actorId", 128, allowEmpty); PrefabName = Guard.Bounded(prefabName ?? string.Empty, "prefabName", 128, allowEmpty: true); DisplayName = Guard.Bounded(displayName ?? string.Empty, "displayName", 128, allowEmpty: true); NativeCharacter = nativeCharacter; IsPlayer = isPlayer; IsBoss = isBoss; } } public sealed class CombatItem { public static readonly CombatItem None = new CombatItem(string.Empty, string.Empty, null, allowEmpty: true); public string ItemId { get; } public string PrefabName { get; } public ItemData NativeItem { get; } public bool IsNone => ItemId.Length == 0; public CombatItem(string itemId, string prefabName, ItemData nativeItem = null) : this(itemId, prefabName, nativeItem, allowEmpty: false) { } private CombatItem(string itemId, string prefabName, ItemData nativeItem, bool allowEmpty) { ItemId = Guard.Bounded(itemId ?? string.Empty, "itemId", 256, allowEmpty); PrefabName = Guard.Bounded(prefabName ?? string.Empty, "prefabName", 128, allowEmpty); NativeItem = nativeItem; } } public sealed class CombatActorSnapshot { public static readonly CombatActorSnapshot None = new CombatActorSnapshot(string.Empty, string.Empty, string.Empty, isPlayer: false, isBoss: false, allowEmpty: true); public string ActorId { get; } public string PrefabName { get; } public string DisplayName { get; } public bool IsPlayer { get; } public bool IsBoss { get; } public bool IsNone => ActorId.Length == 0; public CombatActorSnapshot(string actorId, string prefabName = "", string displayName = "", bool isPlayer = false, bool isBoss = false) : this(actorId, prefabName, displayName, isPlayer, isBoss, allowEmpty: false) { } private CombatActorSnapshot(string actorId, string prefabName, string displayName, bool isPlayer, bool isBoss, bool allowEmpty) { ActorId = Guard.Bounded(actorId ?? string.Empty, "actorId", 128, allowEmpty); PrefabName = Guard.Bounded(prefabName ?? string.Empty, "prefabName", 128, allowEmpty: true); DisplayName = Guard.Bounded(displayName ?? string.Empty, "displayName", 128, allowEmpty: true); IsPlayer = isPlayer; IsBoss = isBoss; } public static CombatActorSnapshot From(CombatActor actor) { if (actor == null || actor.IsNone) { return None; } return new CombatActorSnapshot(actor.ActorId, actor.PrefabName, actor.DisplayName, actor.IsPlayer, actor.IsBoss); } } public sealed class CombatItemSnapshot { public static readonly CombatItemSnapshot None = new CombatItemSnapshot(string.Empty, string.Empty, 0, 0f, allowEmpty: true); public string ItemId { get; } public string PrefabName { get; } public int Quality { get; } public float Durability { get; } public bool IsNone => ItemId.Length == 0; public CombatItemSnapshot(string itemId, string prefabName, int quality, float durability) : this(itemId, prefabName, quality, durability, allowEmpty: false) { } private CombatItemSnapshot(string itemId, string prefabName, int quality, float durability, bool allowEmpty) { ItemId = Guard.Bounded(itemId ?? string.Empty, "itemId", 256, allowEmpty); PrefabName = Guard.Bounded(prefabName ?? string.Empty, "prefabName", 128, allowEmpty); if (quality < 0) { throw new ArgumentOutOfRangeException("quality"); } Quality = quality; Durability = CombatContractGuard.NonNegative(durability, "durability"); } public static CombatItemSnapshot From(CombatItem item) { if (item == null || item.IsNone) { return None; } int quality = item.NativeItem?.m_quality ?? 0; float durability = item.NativeItem?.m_durability ?? 0f; return new CombatItemSnapshot(item.ItemId, item.PrefabName, quality, durability); } } public sealed class CombatHitSnapshot { public float Damage { get; } public float Blunt { get; } public float Slash { get; } public float Pierce { get; } public float Chop { get; } public float Pickaxe { get; } public float Fire { get; } public float Frost { get; } public float Lightning { get; } public float Poison { get; } public float Spirit { get; } public float PushForce { get; } public float BackstabBonus { get; } public float StaggerMultiplier { get; } public bool Blockable { get; } public bool Dodgeable { get; } public bool Ranged { get; } public SkillType Skill { get; } public HitType HitType { get; } public int StatusEffectHash { get; } public float TotalDamage => Damage + Blunt + Slash + Pierce + Chop + Pickaxe + Fire + Frost + Lightning + Poison + Spirit; public CombatHitSnapshot(HitData hit) { //IL_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01b1: Unknown result type (might be due to invalid IL or missing references) //IL_01b6: Unknown result type (might be due to invalid IL or missing references) if (hit == null) { throw new ArgumentNullException("hit"); } Damage = CombatContractGuard.NonNegative(hit.m_damage.m_damage, "hit"); Blunt = CombatContractGuard.NonNegative(hit.m_damage.m_blunt, "hit"); Slash = CombatContractGuard.NonNegative(hit.m_damage.m_slash, "hit"); Pierce = CombatContractGuard.NonNegative(hit.m_damage.m_pierce, "hit"); Chop = CombatContractGuard.NonNegative(hit.m_damage.m_chop, "hit"); Pickaxe = CombatContractGuard.NonNegative(hit.m_damage.m_pickaxe, "hit"); Fire = CombatContractGuard.NonNegative(hit.m_damage.m_fire, "hit"); Frost = CombatContractGuard.NonNegative(hit.m_damage.m_frost, "hit"); Lightning = CombatContractGuard.NonNegative(hit.m_damage.m_lightning, "hit"); Poison = CombatContractGuard.NonNegative(hit.m_damage.m_poison, "hit"); Spirit = CombatContractGuard.NonNegative(hit.m_damage.m_spirit, "hit"); PushForce = CombatContractGuard.NonNegative(hit.m_pushForce, "hit"); BackstabBonus = CombatContractGuard.NonNegative(hit.m_backstabBonus, "hit"); StaggerMultiplier = CombatContractGuard.NonNegative(hit.m_staggerMultiplier, "hit"); Blockable = hit.m_blockable; Dodgeable = hit.m_dodgeable; Ranged = hit.m_ranged; Skill = hit.m_skill; HitType = hit.m_hitType; StatusEffectHash = hit.m_statusEffectHash; } } public abstract class CombatRuleContext { public string ActionId { get; } public CombatContextFlags Context { get; } public double MonotonicSeconds { get; } public bool IsAuthoritative { get; } protected CombatRuleContext(string actionId, CombatContextFlags context, double monotonicSeconds, bool authoritative) { ActionId = Guard.Bounded(actionId, "actionId", 128); MonotonicSeconds = CombatContractGuard.NonNegative(monotonicSeconds, "monotonicSeconds"); Context = context; IsAuthoritative = authoritative; } } public sealed class DamageRuleContext : CombatRuleContext { public CombatActor Attacker { get; } public CombatActor Defender { get; } public HitData OriginalHit { get; } public DamageRulePhase Phase { get; } public DamageRuleContext(string actionId, CombatActor attacker, CombatActor defender, HitData originalHit, DamageRulePhase phase, CombatContextFlags context, double monotonicSeconds, bool authoritative) : base(actionId, context, monotonicSeconds, authoritative) { Attacker = attacker ?? CombatActor.None; Defender = defender ?? throw new ArgumentNullException("defender"); OriginalHit = originalHit ?? throw new ArgumentNullException("originalHit"); if (!Enum.IsDefined(typeof(DamageRulePhase), phase)) { throw new ArgumentOutOfRangeException("phase"); } Phase = phase; } } public sealed class DamageRuleDecision { public HitData EffectiveHit { get; } public bool ApplyDamage { get; } public bool CountAsCombat { get; } public bool CountForProgression { get; } public string Reason { get; } public DamageRuleDecision(HitData effectiveHit, bool applyDamage = true, bool countAsCombat = true, bool countForProgression = true, string reason = "") { EffectiveHit = effectiveHit ?? throw new ArgumentNullException("effectiveHit"); ApplyDamage = applyDamage; CountAsCombat = countAsCombat; CountForProgression = countForProgression; Reason = Guard.Bounded(reason ?? string.Empty, "reason", 512, allowEmpty: true); } public static DamageRuleDecision From(HitData hit) { return new DamageRuleDecision((hit ?? throw new ArgumentNullException("hit")).Clone()); } } public sealed class BlockRuleContext : CombatRuleContext { public CombatActor Attacker { get; } public CombatActor Defender { get; } public CombatItem BlockingItem { get; } public HitData IncomingHit { get; } public BlockRulePhase Phase { get; } public bool NativePerfectBlock { get; } public float BlockTimer { get; } public float NativeBlockPower { get; } public BlockRuleContext(string actionId, CombatActor attacker, CombatActor defender, CombatItem blockingItem, HitData incomingHit, BlockRulePhase phase, bool nativePerfectBlock, float blockTimer, float nativeBlockPower, CombatContextFlags context, double monotonicSeconds, bool authoritative) : base(actionId, context, monotonicSeconds, authoritative) { Attacker = attacker ?? CombatActor.None; Defender = defender ?? throw new ArgumentNullException("defender"); BlockingItem = blockingItem ?? CombatItem.None; IncomingHit = incomingHit ?? throw new ArgumentNullException("incomingHit"); if (!Enum.IsDefined(typeof(BlockRulePhase), phase)) { throw new ArgumentOutOfRangeException("phase"); } Phase = phase; NativePerfectBlock = nativePerfectBlock; BlockTimer = CombatContractGuard.NonNegative(blockTimer, "blockTimer"); NativeBlockPower = CombatContractGuard.NonNegative(nativeBlockPower, "nativeBlockPower"); } } public sealed class BlockRuleDecision { public bool AllowBlock { get; } public CombatPolicyOverride PerfectBlock { get; } public float BlockPowerMultiplier { get; } public float StaminaCostMultiplier { get; } public float GuardDamageMultiplier { get; } public bool AllowAttackerStagger { get; } public string Reason { get; } public BlockRuleDecision(bool allowBlock = true, CombatPolicyOverride perfectBlock = CombatPolicyOverride.Default, float blockPowerMultiplier = 1f, float staminaCostMultiplier = 1f, float guardDamageMultiplier = 1f, bool allowAttackerStagger = true, string reason = "") { if (!Enum.IsDefined(typeof(CombatPolicyOverride), perfectBlock)) { throw new ArgumentOutOfRangeException("perfectBlock"); } AllowBlock = allowBlock; PerfectBlock = perfectBlock; BlockPowerMultiplier = CombatContractGuard.Multiplier(blockPowerMultiplier, "blockPowerMultiplier"); StaminaCostMultiplier = CombatContractGuard.Multiplier(staminaCostMultiplier, "staminaCostMultiplier"); GuardDamageMultiplier = CombatContractGuard.Multiplier(guardDamageMultiplier, "guardDamageMultiplier"); AllowAttackerStagger = allowAttackerStagger; Reason = Guard.Bounded(reason ?? string.Empty, "reason", 512, allowEmpty: true); } } public sealed class DeathRuleContext : CombatRuleContext { public CombatActor Victim { get; } public CombatActor Killer { get; } public HitData KillingHit { get; } public DeathRuleContext(string actionId, CombatActor victim, CombatActor killer, HitData killingHit, CombatContextFlags context, double monotonicSeconds, bool authoritative) : base(actionId, context, monotonicSeconds, authoritative) { Victim = victim ?? throw new ArgumentNullException("victim"); Killer = killer ?? CombatActor.None; KillingHit = killingHit; } } public sealed class DeathRuleDecision { public bool AllowDeath { get; } public CombatPolicyOverride Drops { get; } public CombatPolicyOverride Rewards { get; } public CombatPolicyOverride ProgressionCredit { get; } public string Reason { get; } public DeathRuleDecision(bool allowDeath = true, CombatPolicyOverride drops = CombatPolicyOverride.Default, CombatPolicyOverride rewards = CombatPolicyOverride.Default, CombatPolicyOverride progressionCredit = CombatPolicyOverride.Default, string reason = "") { CombatContractGuard.Policy(drops, "drops"); CombatContractGuard.Policy(rewards, "rewards"); CombatContractGuard.Policy(progressionCredit, "progressionCredit"); AllowDeath = allowDeath; Drops = drops; Rewards = rewards; ProgressionCredit = progressionCredit; Reason = Guard.Bounded(reason ?? string.Empty, "reason", 512, allowEmpty: true); } } public sealed class DurabilityRuleContext : CombatRuleContext { public CombatActor Actor { get; } public CombatItem Item { get; } public DurabilityUseKind UseKind { get; } public float CurrentDurability { get; } public float RequestedLoss { get; } public DurabilityRuleContext(string actionId, CombatActor actor, CombatItem item, DurabilityUseKind useKind, float currentDurability, float requestedLoss, CombatContextFlags context, double monotonicSeconds, bool authoritative) : base(actionId, context, monotonicSeconds, authoritative) { Actor = actor ?? throw new ArgumentNullException("actor"); Item = item ?? throw new ArgumentNullException("item"); if (!Enum.IsDefined(typeof(DurabilityUseKind), useKind)) { throw new ArgumentOutOfRangeException("useKind"); } UseKind = useKind; CurrentDurability = CombatContractGuard.NonNegative(currentDurability, "currentDurability"); RequestedLoss = CombatContractGuard.NonNegative(requestedLoss, "requestedLoss"); } } public sealed class DurabilityRuleDecision { public float DurabilityLoss { get; } public bool Apply { get; } public string Reason { get; } public DurabilityRuleDecision(float durabilityLoss, bool apply = true, string reason = "") { DurabilityLoss = CombatContractGuard.NonNegative(durabilityLoss, "durabilityLoss"); Apply = apply; Reason = Guard.Bounded(reason ?? string.Empty, "reason", 512, allowEmpty: true); } } public sealed class SkillGainRuleContext : CombatRuleContext { public CombatActor Actor { get; } public SkillType Skill { get; } public float RequestedAmount { get; } public SkillGainRuleContext(string actionId, CombatActor actor, SkillType skill, float requestedAmount, CombatContextFlags context, double monotonicSeconds, bool authoritative) : base(actionId, context, monotonicSeconds, authoritative) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) Actor = actor ?? throw new ArgumentNullException("actor"); Skill = skill; RequestedAmount = CombatContractGuard.NonNegative(requestedAmount, "requestedAmount"); } } public sealed class SkillGainRuleDecision { public float Amount { get; } public bool Apply { get; } public string Reason { get; } public SkillGainRuleDecision(float amount, bool apply = true, string reason = "") { Amount = CombatContractGuard.NonNegative(amount, "amount"); Apply = apply; Reason = Guard.Bounded(reason ?? string.Empty, "reason", 512, allowEmpty: true); } } public sealed class CombatHitResolvedEvent { public string ActionId { get; } public CombatActorSnapshot Attacker { get; } public CombatActorSnapshot Defender { get; } public CombatHitSnapshot EffectiveHit { get; } public CombatContextFlags Context { get; } public float HealthBefore { get; } public float HealthAfter { get; } public bool Blocked { get; } public bool Dodged { get; } public bool Staggered { get; } public double MonotonicSeconds { get; } public bool IsAuthoritative { get; } public CombatHitResolvedEvent(string actionId, CombatActorSnapshot attacker, CombatActorSnapshot defender, CombatHitSnapshot effectiveHit, CombatContextFlags context, float healthBefore, float healthAfter, bool blocked, bool dodged, bool staggered, double monotonicSeconds, bool authoritative) { ActionId = Guard.Bounded(actionId, "actionId", 128); Attacker = attacker ?? CombatActorSnapshot.None; Defender = defender ?? throw new ArgumentNullException("defender"); EffectiveHit = effectiveHit ?? throw new ArgumentNullException("effectiveHit"); Context = context; HealthBefore = CombatContractGuard.NonNegative(healthBefore, "healthBefore"); HealthAfter = CombatContractGuard.NonNegative(healthAfter, "healthAfter"); Blocked = blocked; Dodged = dodged; Staggered = staggered; MonotonicSeconds = CombatContractGuard.NonNegative(monotonicSeconds, "monotonicSeconds"); IsAuthoritative = authoritative; } } public sealed class CombatBlockResolvedEvent { public string ActionId { get; } public CombatActorSnapshot Attacker { get; } public CombatActorSnapshot Defender { get; } public CombatItemSnapshot BlockingItem { get; } public bool Perfect { get; } public float IncomingDamage { get; } public float BlockedDamage { get; } public float StaminaUsed { get; } public CombatContextFlags Context { get; } public double MonotonicSeconds { get; } public bool IsAuthoritative { get; } public CombatBlockResolvedEvent(string actionId, CombatActorSnapshot attacker, CombatActorSnapshot defender, CombatItemSnapshot blockingItem, bool perfect, float incomingDamage, float blockedDamage, float staminaUsed, CombatContextFlags context, double monotonicSeconds, bool authoritative) { ActionId = Guard.Bounded(actionId, "actionId", 128); Attacker = attacker ?? CombatActorSnapshot.None; Defender = defender ?? throw new ArgumentNullException("defender"); BlockingItem = blockingItem ?? CombatItemSnapshot.None; Perfect = perfect; IncomingDamage = CombatContractGuard.NonNegative(incomingDamage, "incomingDamage"); BlockedDamage = CombatContractGuard.NonNegative(blockedDamage, "blockedDamage"); StaminaUsed = CombatContractGuard.NonNegative(staminaUsed, "staminaUsed"); Context = context; MonotonicSeconds = CombatContractGuard.NonNegative(monotonicSeconds, "monotonicSeconds"); IsAuthoritative = authoritative; } } public sealed class CombatDodgeResolvedEvent { public string ActionId { get; } public CombatActorSnapshot Actor { get; } public CombatActorSnapshot Attacker { get; } public bool Perfect { get; } public float StaminaUsed { get; } public float StaminaRefunded { get; } public CombatContextFlags Context { get; } public double MonotonicSeconds { get; } public bool IsAuthoritative { get; } public CombatDodgeResolvedEvent(string actionId, CombatActorSnapshot actor, CombatActorSnapshot attacker, bool perfect, float staminaUsed, float staminaRefunded, CombatContextFlags context, double monotonicSeconds, bool authoritative) { ActionId = Guard.Bounded(actionId, "actionId", 128); Actor = actor ?? throw new ArgumentNullException("actor"); Attacker = attacker ?? CombatActorSnapshot.None; Perfect = perfect; StaminaUsed = CombatContractGuard.NonNegative(staminaUsed, "staminaUsed"); StaminaRefunded = CombatContractGuard.NonNegative(staminaRefunded, "staminaRefunded"); Context = context; MonotonicSeconds = CombatContractGuard.NonNegative(monotonicSeconds, "monotonicSeconds"); IsAuthoritative = authoritative; } } public sealed class CombatStaggerChangedEvent { public CombatActorSnapshot Target { get; } public float PreviousNormalized { get; } public float CurrentNormalized { get; } public bool Broken { get; } public ulong Sequence { get; } public CombatContextFlags Context { get; } public double MonotonicSeconds { get; } public bool IsAuthoritative { get; } public CombatStaggerChangedEvent(CombatActorSnapshot target, float previousNormalized, float currentNormalized, bool broken, ulong sequence, CombatContextFlags context, double monotonicSeconds, bool authoritative) { Target = target ?? throw new ArgumentNullException("target"); PreviousNormalized = CombatContractGuard.Normalized(previousNormalized, "previousNormalized"); CurrentNormalized = CombatContractGuard.Normalized(currentNormalized, "currentNormalized"); Broken = broken; Sequence = sequence; Context = context; MonotonicSeconds = CombatContractGuard.NonNegative(monotonicSeconds, "monotonicSeconds"); IsAuthoritative = authoritative; } } public sealed class CombatDeathResolvedEvent { public string ActionId { get; } public CombatActorSnapshot Victim { get; } public CombatActorSnapshot Killer { get; } public CombatContextFlags Context { get; } public bool DropsAllowed { get; } public bool RewardsAllowed { get; } public bool ProgressionCredited { get; } public double MonotonicSeconds { get; } public bool IsAuthoritative { get; } public CombatDeathResolvedEvent(string actionId, CombatActorSnapshot victim, CombatActorSnapshot killer, CombatContextFlags context, bool dropsAllowed, bool rewardsAllowed, bool progressionCredited, double monotonicSeconds, bool authoritative) { ActionId = Guard.Bounded(actionId, "actionId", 128); Victim = victim ?? throw new ArgumentNullException("victim"); Killer = killer ?? CombatActorSnapshot.None; Context = context; DropsAllowed = dropsAllowed; RewardsAllowed = rewardsAllowed; ProgressionCredited = progressionCredited; MonotonicSeconds = CombatContractGuard.NonNegative(monotonicSeconds, "monotonicSeconds"); IsAuthoritative = authoritative; } } public static class CombatRulePipelineExtensions { public static RulePipeline GetDamagePipeline(this IRulePipelineRegistry registry) { return Require(registry).GetOrCreate("combat.damage"); } public static RulePipeline GetBlockPipeline(this IRulePipelineRegistry registry) { return Require(registry).GetOrCreate("combat.block"); } public static RulePipeline GetDeathPipeline(this IRulePipelineRegistry registry) { return Require(registry).GetOrCreate("combat.death"); } public static RulePipeline GetDurabilityPipeline(this IRulePipelineRegistry registry) { return Require(registry).GetOrCreate("combat.durability"); } public static RulePipeline GetSkillGainPipeline(this IRulePipelineRegistry registry) { return Require(registry).GetOrCreate("combat.skill-gain"); } private static IRulePipelineRegistry Require(IRulePipelineRegistry registry) { return registry ?? throw new ArgumentNullException("registry"); } } internal static class CombatContractGuard { internal static float NonNegative(float value, string parameter) { if (float.IsNaN(value) || float.IsInfinity(value) || value < 0f) { throw new ArgumentOutOfRangeException(parameter); } return value; } internal static double NonNegative(double value, string parameter) { if (double.IsNaN(value) || double.IsInfinity(value) || value < 0.0) { throw new ArgumentOutOfRangeException(parameter); } return value; } internal static float Multiplier(float value, string parameter) { value = NonNegative(value, parameter); if (value > 1000f) { throw new ArgumentOutOfRangeException(parameter); } return value; } internal static float Normalized(float value, string parameter) { if (float.IsNaN(value) || float.IsInfinity(value) || value < 0f || value > 1f) { throw new ArgumentOutOfRangeException(parameter); } return value; } internal static void Policy(CombatPolicyOverride value, string parameter) { if (!Enum.IsDefined(typeof(CombatPolicyOverride), value)) { throw new ArgumentOutOfRangeException(parameter); } } } public static class ModCoreConstants { public const int ApiVersion = 1; public const int NetworkProtocol = 1; public const int MaximumModulesPerPeer = 256; public const int MaximumModuleIdLength = 64; public const int MaximumVersionLength = 64; } public enum ModuleSide : byte { Client = 1, Server, Both } public enum ModuleRequirement : byte { LocalOnly = 1, ServerOnly, OptionalNegotiated, RequiredOnBoth, Developer } public enum ModuleRuntimeState : byte { Registered = 1, Compatible, Degraded, FeatureDisabled, Faulted, Inactive } public enum CompatibilityRuleKind : byte { HardConflict = 1, FeatureConflict, SoftDependency, OrderingConstraint, Replacement, ExternalAdapter } public enum CompatibilitySeverity : byte { Information = 1, Warning, Error } public enum ConfigScope : byte { LocalPreference = 1, ServerPolicy, WorldState, Secret, Developer } public enum LifecycleEventKind : byte { CoreReady = 1, NetworkStarting, NetworkStopping, PeerConnected, PeerDisconnected, WorldLoading, WorldReady, WorldSaving, WorldUnloading, LocalPlayerReady, LocalPlayerDestroyed, LocalPlayerRespawned, HudReady, HudDestroyed, InventoryReady, InventoryDestroyed, ObjectDbReady, NetworkSceneReady, NetworkSceneDestroyed } [Flags] public enum CombatContextFlags : uint { None = 0u, Normal = 1u, Boss = 2u, PlayerVersusPlayer = 4u, Arena = 8u, Training = 0x10u, Spectator = 0x20u, ScriptedOrNpc = 0x40u, Dead = 0x80u, Transitioning = 0x100u } public enum RuleHandlerKind : byte { Observe = 1, Transform, Veto, Supply, ExclusiveOwner } public enum NamespaceKind : byte { Zdo = 1, ItemCustomData, PlayerSave, WorldStore, RoutedRpc, RawRpc, Command, Ui, Metric } public enum SchedulerTarget : byte { MainThread = 1, Background } public enum UiSurface : byte { Hud = 1, EnemyHud, BossHud, Inventory, MainMenu, Overlay } public enum NotificationChannel : byte { LocalCenter = 1, LocalStatus, PrivatePeer, ServerWide, ArenaOrArea, Diagnostics } public enum NetworkDirection : byte { ClientToServer = 1, ServerToClient, Bidirectional } [Flags] public enum NetworkMessageFlags : byte { None = 0, Ordered = 1, Request = 2, Reply = 4, IdempotentMutation = 8 } public readonly struct ModuleId : IEquatable, IComparable { private readonly string _value; public string Value => _value ?? string.Empty; public bool IsEmpty => string.IsNullOrEmpty(_value); public ModuleId(string value) { if (!IsValid(value)) { throw new ArgumentException("Module IDs must be 1-64 lowercase ASCII letters, digits, dots, underscores, or hyphens.", "value"); } _value = value; } public static bool TryParse(string value, out ModuleId moduleId) { if (IsValid(value)) { moduleId = new ModuleId(value); return true; } moduleId = default(ModuleId); return false; } public static bool IsValid(string value) { if (string.IsNullOrEmpty(value) || value.Length > 64) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '_' && c != '-') { return false; } } return true; } public bool Equals(ModuleId other) { return string.Equals(Value, other.Value, StringComparison.Ordinal); } public override bool Equals(object obj) { if (obj is ModuleId other) { return Equals(other); } return false; } public override int GetHashCode() { return StringComparer.Ordinal.GetHashCode(Value); } public int CompareTo(ModuleId other) { return string.Compare(Value, other.Value, StringComparison.Ordinal); } public override string ToString() { return Value; } public static bool operator ==(ModuleId left, ModuleId right) { return left.Equals(right); } public static bool operator !=(ModuleId left, ModuleId right) { return !left.Equals(right); } } public readonly struct SemanticVersion : IEquatable, IComparable { public int Major { get; } public int Minor { get; } public int Patch { get; } public string Prerelease { get; } public SemanticVersion(int major, int minor, int patch, string prerelease = "") { if (major < 0 || minor < 0 || patch < 0) { throw new ArgumentOutOfRangeException("major", "Version components cannot be negative."); } if (prerelease != null && prerelease.Length > 32) { throw new ArgumentException("Prerelease label is too long.", "prerelease"); } Major = major; Minor = minor; Patch = patch; Prerelease = prerelease ?? string.Empty; } public static bool TryParse(string value, out SemanticVersion version) { version = default(SemanticVersion); if (string.IsNullOrWhiteSpace(value) || value.Length > 64) { return false; } string[] array = value.Split(new char[1] { '+' }, 2)[0].Split(new char[1] { '-' }, 2); string[] array2 = array[0].Split(new char[1] { '.' }); if (array2.Length != 3 || !int.TryParse(array2[0], NumberStyles.None, CultureInfo.InvariantCulture, out var result) || !int.TryParse(array2[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result2) || !int.TryParse(array2[2], NumberStyles.None, CultureInfo.InvariantCulture, out var result3) || result < 0 || result2 < 0 || result3 < 0) { return false; } string text = ((array.Length == 2) ? array[1] : string.Empty); if (text.Length > 32 || !IsValidLabel(text)) { return false; } version = new SemanticVersion(result, result2, result3, text); return true; } private static bool IsValidLabel(string value) { foreach (char c in value) { if (!char.IsLetterOrDigit(c) && c != '.' && c != '-') { return false; } } return true; } public int CompareTo(SemanticVersion other) { int num = Major.CompareTo(other.Major); if (num != 0) { return num; } num = Minor.CompareTo(other.Minor); if (num != 0) { return num; } num = Patch.CompareTo(other.Patch); if (num != 0) { return num; } if (Prerelease.Length == 0 && other.Prerelease.Length != 0) { return 1; } if (Prerelease.Length != 0 && other.Prerelease.Length == 0) { return -1; } return string.Compare(Prerelease, other.Prerelease, StringComparison.Ordinal); } public bool Equals(SemanticVersion other) { return CompareTo(other) == 0; } public override bool Equals(object obj) { if (obj is SemanticVersion other) { return Equals(other); } return false; } public override int GetHashCode() { return (((((Major * 397) ^ Minor) * 397) ^ Patch) * 397) ^ StringComparer.Ordinal.GetHashCode(Prerelease); } public override string ToString() { return Major + "." + Minor + "." + Patch + ((Prerelease.Length == 0) ? string.Empty : ("-" + Prerelease)); } public static bool operator <(SemanticVersion left, SemanticVersion right) { return left.CompareTo(right) < 0; } public static bool operator >(SemanticVersion left, SemanticVersion right) { return left.CompareTo(right) > 0; } public static bool operator <=(SemanticVersion left, SemanticVersion right) { return left.CompareTo(right) <= 0; } public static bool operator >=(SemanticVersion left, SemanticVersion right) { return left.CompareTo(right) >= 0; } } public sealed class ModuleDescriptor { public ModuleId Id { get; } public string PluginGuid { get; } public string DisplayName { get; } public SemanticVersion Version { get; } public int ProtocolVersion { get; } public ModuleSide Side { get; } public ModuleRequirement Requirement { get; } public ulong Capabilities { get; } public int MinimumCoreApi { get; } public int MaximumCoreApi { get; } public ModuleDescriptor(ModuleId id, string pluginGuid, string displayName, SemanticVersion version, int protocolVersion, ModuleSide side, ModuleRequirement requirement, ulong capabilities = 0uL, int minimumCoreApi = 1, int maximumCoreApi = 1) { if (id.IsEmpty) { throw new ArgumentException("Module ID is required.", "id"); } if (string.IsNullOrWhiteSpace(pluginGuid) || pluginGuid.Length > 128) { throw new ArgumentException("A bounded plugin GUID is required.", "pluginGuid"); } if (string.IsNullOrWhiteSpace(displayName) || displayName.Length > 128) { throw new ArgumentException("A bounded display name is required.", "displayName"); } if (protocolVersion < 0) { throw new ArgumentOutOfRangeException("protocolVersion"); } if (minimumCoreApi <= 0 || maximumCoreApi < minimumCoreApi) { throw new ArgumentOutOfRangeException("minimumCoreApi"); } Id = id; PluginGuid = pluginGuid; DisplayName = displayName; Version = version; ProtocolVersion = protocolVersion; Side = side; Requirement = requirement; Capabilities = capabilities; MinimumCoreApi = minimumCoreApi; MaximumCoreApi = maximumCoreApi; } public bool SupportsCoreApi(int apiVersion) { if (apiVersion >= MinimumCoreApi) { return apiVersion <= MaximumCoreApi; } return false; } } public sealed class ModuleSnapshot { public ModuleDescriptor Descriptor { get; } public ModuleRuntimeState State { get; } public string Detail { get; } public ModuleSnapshot(ModuleDescriptor descriptor, ModuleRuntimeState state, string detail) { Descriptor = descriptor ?? throw new ArgumentNullException("descriptor"); State = state; Detail = detail ?? string.Empty; } } public sealed class LifecycleEvent { public LifecycleEventKind Kind { get; } public object Subject { get; } public long PeerId { get; } public string Detail { get; } public DateTime UtcTimestamp { get; } public LifecycleEvent(LifecycleEventKind kind, object subject = null, long peerId = 0L, string detail = "") { Kind = kind; Subject = subject; PeerId = peerId; Detail = detail ?? string.Empty; UtcTimestamp = DateTime.UtcNow; } } public sealed class CompatibilityRule { public string OwnerPluginGuid { get; } public string TargetPluginGuid { get; } public CompatibilityRuleKind Kind { get; } public CompatibilitySeverity Severity { get; } public string Message { get; } public string Feature { get; } public CompatibilityRule(string ownerPluginGuid, string targetPluginGuid, CompatibilityRuleKind kind, CompatibilitySeverity severity, string message, string feature = "") { if (string.IsNullOrWhiteSpace(ownerPluginGuid)) { throw new ArgumentException("ownerPluginGuid"); } if (string.IsNullOrWhiteSpace(targetPluginGuid)) { throw new ArgumentException("targetPluginGuid"); } OwnerPluginGuid = ownerPluginGuid; TargetPluginGuid = targetPluginGuid; Kind = kind; Severity = severity; Message = message ?? string.Empty; Feature = feature ?? string.Empty; } } public sealed class CompatibilityIssue { public CompatibilityRule Rule { get; } public bool OwnerLoaded { get; } public bool TargetLoaded { get; } public CompatibilityIssue(CompatibilityRule rule, bool ownerLoaded, bool targetLoaded) { Rule = rule; OwnerLoaded = ownerLoaded; TargetLoaded = targetLoaded; } } public sealed class MetricSnapshot { public string Owner { get; } public string Name { get; } public long Value { get; } public bool IsGauge { get; } public MetricSnapshot(string owner, string name, long value, bool gauge) { Owner = owner; Name = name; Value = value; IsGauge = gauge; } } public sealed class Registration : IDisposable { private Action _dispose; public Registration(Action dispose) { _dispose = dispose ?? throw new ArgumentNullException("dispose"); } public void Dispose() { Interlocked.Exchange(ref _dispose, null)?.Invoke(); } } internal static class Guard { internal static string Bounded(string value, string parameter, int maximum, bool allowEmpty = false) { if (value == null || (!allowEmpty && string.IsNullOrWhiteSpace(value)) || value.Length > maximum) { throw new ArgumentException(parameter + " must be " + (allowEmpty ? "at most " : "between 1 and ") + maximum + " characters.", parameter); } return value; } } [Flags] public enum ResourceContextFlags : uint { None = 0u, Crafting = 1u, Building = 2u, CombatAmmo = 4u, Arena = 8u, Training = 0x10u, FreeCost = 0x20u, TemporaryLoadout = 0x40u, RestoreOnExit = 0x80u } [Flags] public enum InventoryCandidateFlags : uint { None = 0u, Equipped = 1u, Protected = 2u, Temporary = 4u, NonConsumable = 8u, Ammo = 0x10u, WeaponInstance = 0x20u } public sealed class ResourceRequest { public string TransactionId { get; } public string PrefabName { get; } public int Amount { get; } public ResourceContextFlags Context { get; } public string PlayerId { get; } public ResourceRequest(string transactionId, string prefabName, int amount, ResourceContextFlags context, string playerId = "") { TransactionId = Guard.Bounded(transactionId, "transactionId", 128); PrefabName = Guard.Bounded(prefabName, "prefabName", 128); if (amount <= 0 || amount > 1000000) { throw new ArgumentOutOfRangeException("amount"); } Amount = amount; Context = context; PlayerId = Guard.Bounded(playerId ?? string.Empty, "playerId", 128, allowEmpty: true); } } public sealed class InventoryCandidate { public string ProviderId { get; } public string ItemId { get; } public string PrefabName { get; } public int Available { get; } public int ProviderPriority { get; } public InventoryCandidateFlags Flags { get; } public object NativeItem { get; } public InventoryCandidate(string providerId, string itemId, string prefabName, int available, int providerPriority, InventoryCandidateFlags flags = InventoryCandidateFlags.None, object nativeItem = null) { ProviderId = Guard.Bounded(providerId, "providerId", 128); ItemId = Guard.Bounded(itemId, "itemId", 256); PrefabName = Guard.Bounded(prefabName, "prefabName", 128); if (available < 0) { throw new ArgumentOutOfRangeException("available"); } Available = available; ProviderPriority = providerPriority; Flags = flags; NativeItem = nativeItem; } } public sealed class InventoryReservation { public string ProviderId { get; } public string ItemId { get; } public int Amount { get; } public object Token { get; } public InventoryReservation(string providerId, string itemId, int amount, object token) { ProviderId = providerId; ItemId = itemId; Amount = amount; Token = token; } } public interface IInventoryProvider { string ProviderId { get; } int Priority { get; } IReadOnlyList FindCandidates(ResourceRequest request); bool TryReserve(ResourceRequest request, InventoryCandidate candidate, int amount, out InventoryReservation reservation, out string error); void Commit(InventoryReservation reservation); void Rollback(InventoryReservation reservation); } public interface IItemProtectionPolicy { string PolicyId { get; } int Priority { get; } bool IsProtected(ResourceRequest request, InventoryCandidate candidate, out string reason); } public sealed class InventoryTransactionPlan { public ResourceRequest Request { get; } public IReadOnlyList Reservations { get; } public bool IsValid { get; } public string Error { get; } internal InventoryTransactionPlan(ResourceRequest request, IReadOnlyList reservations, bool valid, string error) { Request = request; Reservations = reservations ?? Array.Empty(); IsValid = valid; Error = error ?? string.Empty; } } public sealed class InventoryTransactionResult { public bool Succeeded { get; } public bool RolledBack { get; } public string Error { get; } public InventoryTransactionResult(bool succeeded, bool rolledBack, string error) { Succeeded = succeeded; RolledBack = rolledBack; Error = error ?? string.Empty; } } public interface IInventoryBroker { IDisposable RegisterProvider(ModuleId owner, IInventoryProvider provider); IDisposable RegisterProtectionPolicy(ModuleId owner, IItemProtectionPolicy policy); InventoryTransactionPlan Plan(ResourceRequest request); InventoryTransactionResult Commit(InventoryTransactionPlan plan); void Cancel(InventoryTransactionPlan plan); } public interface ILogSink { void Debug(string message); void Info(string message); void Warning(string message); void Error(string message); void Error(string message, Exception exception); } internal sealed class BepInExLogSink : ILogSink { private readonly ManualLogSource _log; private readonly Func _debugEnabled; internal BepInExLogSink(ManualLogSource log, Func debugEnabled) { _log = log ?? throw new ArgumentNullException("log"); _debugEnabled = debugEnabled ?? ((Func)(() => false)); } public void Debug(string message) { if (_debugEnabled()) { _log.LogDebug((object)message); } } public void Info(string message) { _log.LogInfo((object)message); } public void Warning(string message) { _log.LogWarning((object)message); } public void Error(string message) { _log.LogError((object)message); } public void Error(string message, Exception exception) { _log.LogError((object)(message + Environment.NewLine + exception)); } } public sealed class NullLogSink : ILogSink { public static readonly NullLogSink Instance = new NullLogSink(); private NullLogSink() { } public void Debug(string message) { } public void Info(string message) { } public void Warning(string message) { } public void Error(string message) { } public void Error(string message, Exception exception) { } } public static class PluginConfigFiles { private const string ConfigBackingFieldName = "k__BackingField"; public static ConfigFile Attach(BaseUnityPlugin plugin, ConfigFile configFile) { if (plugin == null) { throw new ArgumentNullException("plugin"); } if (configFile == null) { throw new ArgumentNullException("configFile"); } FieldInfo field = typeof(BaseUnityPlugin).GetField("k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); if (field == null || field.FieldType != typeof(ConfigFile)) { throw new MissingFieldException(typeof(BaseUnityPlugin).FullName, "k__BackingField"); } field.SetValue(plugin, configFile); if (plugin.Config != configFile) { throw new InvalidOperationException("The custom configuration file was not attached to the plugin."); } return configFile; } } public static class RoutedRpcIngress { public static IDisposable Register(ModuleId owner, params string[] methodNames) { CoreRuntime runtime = Plugin.Runtime; if (runtime == null || !ModCoreApi.IsAvailable) { throw new InvalidOperationException("ModCore is not ready to authenticate routed RPCs."); } if (!RoutedRpcIngressPatch.IsInstalled) { throw new InvalidOperationException("ModCore routed RPC authentication patch is not installed."); } if (!runtime.Services.Modules.TryGet(owner, out var _)) { throw new InvalidOperationException("Register the owning module before its routed RPC ingress."); } return runtime.Services.RoutedIngress.Register(owner, methodNames); } } public sealed class RuleDescriptor { public ModuleId Owner { get; } public string RuleId { get; } public int Stage { get; } public int Priority { get; } public RuleHandlerKind Kind { get; } public RuleDescriptor(ModuleId owner, string ruleId, int stage, int priority, RuleHandlerKind kind) { Owner = owner; RuleId = Guard.Bounded(ruleId, "ruleId", 128); Stage = stage; Priority = priority; Kind = kind; } } public readonly struct RuleResult { public TDecision Decision { get; } public bool Stop { get; } public bool Vetoed { get; } public string Reason { get; } private RuleResult(TDecision decision, bool stop, bool vetoed, string reason) { Decision = decision; Stop = stop; Vetoed = vetoed; Reason = reason ?? string.Empty; } public static RuleResult Continue(TDecision decision) { return new RuleResult(decision, stop: false, vetoed: false, string.Empty); } public static RuleResult StopWith(TDecision decision, string reason = "") { return new RuleResult(decision, stop: true, vetoed: false, reason); } public static RuleResult Veto(TDecision decision, string reason) { return new RuleResult(decision, stop: true, vetoed: true, reason); } } public sealed class PipelineEvaluation { public TDecision Decision { get; } public bool Vetoed { get; } public string Reason { get; } public IReadOnlyList ExecutedRules { get; } public PipelineEvaluation(TDecision decision, bool vetoed, string reason, IReadOnlyList executedRules) { Decision = decision; Vetoed = vetoed; Reason = reason ?? string.Empty; ExecutedRules = executedRules ?? Array.Empty(); } } public delegate RuleResult RuleHandler(TContext context, TDecision current); public sealed class RulePipeline { private sealed class Entry { internal RuleDescriptor Descriptor; internal RuleHandler Handler; internal long RegistrationOrder; } private readonly object _gate = new object(); private readonly string _id; private readonly IFeatureCircuitBreaker _breakers; private readonly ILogSink _log; private readonly List _entries = new List(); private long _nextOrder; public string Id => _id; internal RulePipeline(string id, IFeatureCircuitBreaker breakers, ILogSink log) { _id = Guard.Bounded(id, "id", 128); _breakers = breakers ?? throw new ArgumentNullException("breakers"); _log = log ?? NullLogSink.Instance; } public IDisposable Register(RuleDescriptor descriptor, RuleHandler handler) { if (descriptor == null) { throw new ArgumentNullException("descriptor"); } if (handler == null) { throw new ArgumentNullException("handler"); } Entry entry = new Entry { Descriptor = descriptor, Handler = handler, RegistrationOrder = Interlocked.Increment(ref _nextOrder) }; lock (_gate) { if (_entries.Any((Entry e) => e.Descriptor.Owner == descriptor.Owner && string.Equals(e.Descriptor.RuleId, descriptor.RuleId, StringComparison.Ordinal))) { throw new InvalidOperationException("Duplicate rule registration: " + descriptor.Owner.ToString() + "/" + descriptor.RuleId); } if (descriptor.Kind == RuleHandlerKind.ExclusiveOwner && _entries.Any((Entry e) => e.Descriptor.Kind == RuleHandlerKind.ExclusiveOwner && e.Descriptor.Stage == descriptor.Stage)) { throw new InvalidOperationException("Pipeline '" + _id + "' already has an exclusive owner at stage " + descriptor.Stage + "."); } _entries.Add(entry); SortLocked(); } return new Registration(delegate { lock (_gate) { _entries.Remove(entry); } }); } public PipelineEvaluation Evaluate(TContext context, TDecision seed) { Entry[] array; lock (_gate) { array = _entries.ToArray(); } TDecision current = seed; bool vetoed = false; string reason = string.Empty; List list = new List(array.Length); foreach (Entry entry in array) { string feature = "pipeline." + _id + "." + entry.Descriptor.RuleId; if (_breakers.IsOpen(entry.Descriptor.Owner, feature)) { continue; } RuleResult result = default(RuleResult); if (!_breakers.Execute(entry.Descriptor.Owner, feature, delegate { result = entry.Handler(context, current); })) { _log.Warning("Rule disabled after failure: " + entry.Descriptor.Owner.ToString() + "/" + entry.Descriptor.RuleId); continue; } list.Add(entry.Descriptor.Owner.ToString() + "/" + entry.Descriptor.RuleId); if (entry.Descriptor.Kind != RuleHandlerKind.Observe) { current = result.Decision; } if (result.Vetoed) { vetoed = true; reason = result.Reason; } if (result.Stop) { break; } } return new PipelineEvaluation(current, vetoed, reason, list.AsReadOnly()); } public IReadOnlyList Snapshot() { lock (_gate) { return Array.AsReadOnly(_entries.Select((Entry e) => e.Descriptor).ToArray()); } } private void SortLocked() { _entries.Sort(delegate(Entry left, Entry right) { int num = left.Descriptor.Stage.CompareTo(right.Descriptor.Stage); if (num != 0) { return num; } num = right.Descriptor.Priority.CompareTo(left.Descriptor.Priority); if (num != 0) { return num; } num = left.Descriptor.Owner.CompareTo(right.Descriptor.Owner); if (num != 0) { return num; } num = string.Compare(left.Descriptor.RuleId, right.Descriptor.RuleId, StringComparison.Ordinal); return (num == 0) ? left.RegistrationOrder.CompareTo(right.RegistrationOrder) : num; }); } } public interface IModuleRegistry { IDisposable Register(ModuleDescriptor descriptor); bool TryGet(ModuleId id, out ModuleSnapshot module); IReadOnlyList Snapshot(); void SetState(ModuleId id, ModuleRuntimeState state, string detail = ""); } public interface ICompatibilityRegistry { IReadOnlyList Rules { get; } IDisposable Register(CompatibilityRule rule); IReadOnlyList Evaluate(ISet loadedPluginGuids); } public interface ILifecycleBus { IDisposable Subscribe(ModuleId owner, LifecycleEventKind kind, Action handler, int priority = 0); void Publish(LifecycleEvent lifecycleEvent); } public interface IGameEventBus { IDisposable Subscribe(ModuleId owner, Action observer, int priority = 0); void Publish(ModuleId publisher, TEvent value); } public interface IMetricRegistry { void Increment(ModuleId owner, string name, long amount = 1L); void SetGauge(ModuleId owner, string name, long value); IReadOnlyList Snapshot(); void RemoveOwner(ModuleId owner); } public interface IFeatureCircuitBreaker { bool IsOpen(ModuleId owner, string feature); bool Execute(ModuleId owner, string feature, Action action); T Execute(ModuleId owner, string feature, Func action, T fallback); void Reset(ModuleId owner, string feature); IReadOnlyList Snapshot(); } public sealed class CircuitBreakerSnapshot { public ModuleId Owner { get; } public string Feature { get; } public bool IsOpen { get; } public int ConsecutiveFailures { get; } public string Reason { get; } public CircuitBreakerSnapshot(ModuleId owner, string feature, bool isOpen, int consecutiveFailures, string reason) { Owner = owner; Feature = feature; IsOpen = isOpen; ConsecutiveFailures = consecutiveFailures; Reason = reason ?? string.Empty; } } public interface IMainThreadDispatcher { bool IsMainThread { get; } int PendingCount { get; } bool Post(ModuleId owner, Action action); int Drain(int maximumActions = 256); void CancelOwner(ModuleId owner); } public interface ICoreScheduler { IDisposable Schedule(ModuleId owner, TimeSpan delay, Action action, SchedulerTarget target = SchedulerTarget.MainThread, TimeSpan? repeat = null); void CancelOwner(ModuleId owner); void Tick(); } public interface IConfigurationRegistry { IDisposable Register(ConfigSettingDescriptor descriptor); IReadOnlyList Snapshot(bool includeSecrets = false); } public interface IAuthoritativePolicyRegistry { IDisposable Register(PolicyDescriptor descriptor); bool TryApply(ModuleId owner, long revision, byte[] payload, bool authoritative, out PolicySnapshot snapshot, out string error); bool TryGet(ModuleId owner, out PolicySnapshot snapshot); IReadOnlyList Snapshot(); } public sealed class PolicyDescriptor { public ModuleId Owner { get; } public int ProtocolVersion { get; } public int MaximumBytes { get; } public Func Validator { get; } public PolicyDescriptor(ModuleId owner, int protocolVersion, int maximumBytes, Func validator) { if (owner.IsEmpty) { throw new ArgumentException("owner"); } if (protocolVersion < 0) { throw new ArgumentOutOfRangeException("protocolVersion"); } if (maximumBytes <= 0 || maximumBytes > 16777216) { throw new ArgumentOutOfRangeException("maximumBytes"); } Owner = owner; ProtocolVersion = protocolVersion; MaximumBytes = maximumBytes; Validator = validator ?? throw new ArgumentNullException("validator"); } } public sealed class PolicySnapshot { public ModuleId Owner { get; } public int ProtocolVersion { get; } public long Revision { get; } public string Sha256 { get; } public byte[] Payload { get; } public bool IsAuthoritative { get; } public PolicySnapshot(ModuleId owner, int protocolVersion, long revision, string sha256, byte[] payload, bool authoritative) { Owner = owner; ProtocolVersion = protocolVersion; Revision = revision; Sha256 = sha256 ?? string.Empty; Payload = ((payload == null) ? Array.Empty() : ((byte[])payload.Clone())); IsAuthoritative = authoritative; } } public sealed class ConfigSettingDescriptor { public ModuleId Owner { get; } public string Section { get; } public string Key { get; } public ConfigScope Scope { get; } public Func ValueProvider { get; } public int SchemaVersion { get; } public ConfigSettingDescriptor(ModuleId owner, string section, string key, ConfigScope scope, Func valueProvider, int schemaVersion = 1) { Owner = owner; Section = Guard.Bounded(section, "section", 128); Key = Guard.Bounded(key, "key", 128); Scope = scope; ValueProvider = valueProvider ?? throw new ArgumentNullException("valueProvider"); if (schemaVersion <= 0) { throw new ArgumentOutOfRangeException("schemaVersion"); } SchemaVersion = schemaVersion; } } public sealed class ConfigValueSnapshot { public ConfigSettingDescriptor Descriptor { get; } public string Value { get; } public bool IsRedacted { get; } public ConfigValueSnapshot(ConfigSettingDescriptor descriptor, string value, bool redacted) { Descriptor = descriptor; Value = value ?? string.Empty; IsRedacted = redacted; } } public interface INamespaceRegistry { IDisposable Register(ModuleId owner, NamespaceKind kind, string prefix, int schemaVersion = 1, params string[] legacyAliases); IReadOnlyList Snapshot(); } public sealed class NamespaceSnapshot { public ModuleId Owner { get; } public NamespaceKind Kind { get; } public string Prefix { get; } public int SchemaVersion { get; } public IReadOnlyList LegacyAliases { get; } public NamespaceSnapshot(ModuleId owner, NamespaceKind kind, string prefix, int schemaVersion, IReadOnlyList legacyAliases) { Owner = owner; Kind = kind; Prefix = prefix; SchemaVersion = schemaVersion; LegacyAliases = legacyAliases; } } public interface IAtomicStoreFactory { IAtomicStore Open(ModuleId owner, string rootDirectory, string storeName, int maximumBytes); } public interface IAtomicStore { string Path { get; } bool TryLoad(out AtomicStoreRecord record, out string error); void Save(int schemaVersion, long worldOrPlayerId, byte[] payload); } public sealed class AtomicStoreRecord { public int SchemaVersion { get; } public long OwnerId { get; } public byte[] Payload { get; } public bool RecoveredFromBackup { get; } public AtomicStoreRecord(int schemaVersion, long ownerId, byte[] payload, bool recoveredFromBackup) { SchemaVersion = schemaVersion; OwnerId = ownerId; Payload = payload ?? Array.Empty(); RecoveredFromBackup = recoveredFromBackup; } } public interface ICombatStateService { CombatSnapshot Get(string playerId); void Observe(CombatObservation observation); void Clear(string playerId, string reason = ""); void Tick(double monotonicSeconds); IDisposable Subscribe(ModuleId owner, Action handler); } public sealed class CombatObservation { public string PlayerId { get; } public double MonotonicSeconds { get; } public CombatContextFlags Context { get; } public string OpponentId { get; } public string BossId { get; } public double ClearDelaySeconds { get; } public bool IsAuthoritative { get; } public CombatObservation(string playerId, double monotonicSeconds, CombatContextFlags context, string opponentId = "", string bossId = "", double clearDelaySeconds = 8.0, bool authoritative = false) { PlayerId = Guard.Bounded(playerId, "playerId", 128); if (double.IsNaN(monotonicSeconds) || double.IsInfinity(monotonicSeconds) || monotonicSeconds < 0.0) { throw new ArgumentOutOfRangeException("monotonicSeconds"); } if (double.IsNaN(clearDelaySeconds) || double.IsInfinity(clearDelaySeconds) || clearDelaySeconds < 0.0 || clearDelaySeconds > 3600.0) { throw new ArgumentOutOfRangeException("clearDelaySeconds"); } MonotonicSeconds = monotonicSeconds; Context = context; OpponentId = Guard.Bounded(opponentId ?? string.Empty, "opponentId", 128, allowEmpty: true); BossId = Guard.Bounded(bossId ?? string.Empty, "bossId", 128, allowEmpty: true); ClearDelaySeconds = clearDelaySeconds; IsAuthoritative = authoritative; } } public sealed class CombatSnapshot { public static readonly CombatSnapshot Empty = new CombatSnapshot(string.Empty, active: false, CombatContextFlags.None, 0.0, 0.0, string.Empty, string.Empty, authoritative: false); public string PlayerId { get; } public bool IsActive { get; } public CombatContextFlags Context { get; } public double EnteredAt { get; } public double LastHostileAt { get; } public string OpponentId { get; } public string BossId { get; } public bool IsAuthoritative { get; } public CombatSnapshot(string playerId, bool active, CombatContextFlags context, double enteredAt, double lastHostileAt, string opponentId, string bossId, bool authoritative) { PlayerId = playerId ?? string.Empty; IsActive = active; Context = context; EnteredAt = enteredAt; LastHostileAt = lastHostileAt; OpponentId = opponentId ?? string.Empty; BossId = bossId ?? string.Empty; IsAuthoritative = authoritative; } } public sealed class CombatTransition { public CombatSnapshot Previous { get; } public CombatSnapshot Current { get; } public string Reason { get; } public CombatTransition(CombatSnapshot previous, CombatSnapshot current, string reason) { Previous = previous ?? CombatSnapshot.Empty; Current = current ?? CombatSnapshot.Empty; Reason = reason ?? string.Empty; } } public interface IUiRegistry { IDisposable Reserve(UiReservation reservation); IReadOnlyList Snapshot(); } public sealed class UiReservation { public ModuleId Owner { get; } public UiSurface Surface { get; } public string Slot { get; } public int Priority { get; } public bool Stackable { get; } public UiReservation(ModuleId owner, UiSurface surface, string slot, int priority = 0, bool stackable = true) { Owner = owner; Surface = surface; Slot = Guard.Bounded(slot, "slot", 64); Priority = priority; Stackable = stackable; } } public interface IInputRegistry { bool GameOwnsTextInput { get; } IDisposable Register(InputActionDescriptor action); IReadOnlyList Snapshot(); IReadOnlyList Collisions(); } public sealed class InputActionDescriptor { public ModuleId Owner { get; } public string ActionId { get; } public string Binding { get; } public string Context { get; } public int Priority { get; } public InputActionDescriptor(ModuleId owner, string actionId, string binding, string context = "gameplay", int priority = 0) { Owner = owner; ActionId = Guard.Bounded(actionId, "actionId", 64); Binding = Guard.Bounded(binding, "binding", 64); Context = Guard.Bounded(context, "context", 64); Priority = priority; } } public sealed class InputCollision { public InputActionDescriptor First { get; } public InputActionDescriptor Second { get; } public InputCollision(InputActionDescriptor first, InputActionDescriptor second) { First = first; Second = second; } } public interface ILocalizationRegistry { IDisposable Register(ModuleId owner, string key, string fallbackEnglish); string Resolve(string key, params object[] arguments); } public interface INotificationService { bool Publish(Notification notification); } public sealed class Notification { public ModuleId Owner { get; } public NotificationChannel Channel { get; } public string DeduplicationKey { get; } public string Message { get; } public int Priority { get; } public TimeSpan Cooldown { get; } public long PeerId { get; } public Notification(ModuleId owner, NotificationChannel channel, string deduplicationKey, string message, int priority = 0, TimeSpan? cooldown = null, long peerId = 0L) { Owner = owner; Channel = channel; DeduplicationKey = Guard.Bounded(deduplicationKey, "deduplicationKey", 128); Message = Guard.Bounded(message, "message", 1024); Priority = priority; Cooldown = cooldown ?? TimeSpan.Zero; if (Cooldown < TimeSpan.Zero || Cooldown > TimeSpan.FromHours(24.0)) { throw new ArgumentOutOfRangeException("cooldown"); } PeerId = peerId; } } public interface ICommandRegistry { IDisposable Register(CommandDescriptor descriptor, Func handler); CommandResult Execute(CommandContext context, string input); IReadOnlyList Snapshot(); } public sealed class CommandDescriptor { public ModuleId Owner { get; } public string Name { get; } public string Help { get; } public bool RequiresAdmin { get; } public TimeSpan MinimumInterval { get; } public IReadOnlyList Aliases { get; } public CommandDescriptor(ModuleId owner, string name, string help, bool requiresAdmin = false, TimeSpan? minimumInterval = null, params string[] aliases) { Owner = owner; Name = Guard.Bounded(name, "name", 64); Help = Guard.Bounded(help ?? string.Empty, "help", 512, allowEmpty: true); RequiresAdmin = requiresAdmin; MinimumInterval = minimumInterval ?? TimeSpan.Zero; if (MinimumInterval < TimeSpan.Zero || MinimumInterval > TimeSpan.FromHours(1.0)) { throw new ArgumentOutOfRangeException("minimumInterval"); } Aliases = Array.AsReadOnly((aliases ?? Array.Empty()).Clone() as string[]); } } public sealed class CommandContext { public string SenderId { get; } public string SenderName { get; } public bool IsAdmin { get; } public bool IsServer { get; } public IReadOnlyList Arguments { get; } public CommandContext(string senderId, string senderName, bool isAdmin, bool isServer, IReadOnlyList arguments = null) { SenderId = senderId ?? string.Empty; SenderName = senderName ?? string.Empty; IsAdmin = isAdmin; IsServer = isServer; Arguments = arguments ?? Array.Empty(); } } public sealed class CommandResult { public bool Succeeded { get; } public string Message { get; } public bool IsNotHandled { get; } public static CommandResult Success(string message = "") { return new CommandResult(succeeded: true, message, notHandled: false); } public static CommandResult Failure(string message) { return new CommandResult(succeeded: false, message, notHandled: false); } public static CommandResult NotHandled() { return new CommandResult(succeeded: false, string.Empty, notHandled: true); } private CommandResult(bool succeeded, string message, bool notHandled) { Succeeded = succeeded; Message = message ?? string.Empty; IsNotHandled = notHandled; } } public interface IPlayerIdentityService { bool TryGetPeer(long peerId, out PlayerIdentity identity); bool TryGetByConnection(object connection, out PlayerIdentity identity); IReadOnlyList Snapshot(); } public sealed class PlayerIdentity { public long PeerId { get; } public string PersistentId { get; } public string DisplayName { get; } public string HostName { get; } public bool IsAdmin { get; } public bool IsServer { get; } public bool IsResolved { get; } public PlayerIdentity(long peerId, string persistentId, string displayName, string hostName, bool isAdmin, bool isServer, bool resolved) { PeerId = peerId; PersistentId = persistentId ?? string.Empty; DisplayName = displayName ?? string.Empty; HostName = hostName ?? string.Empty; IsAdmin = isAdmin; IsServer = isServer; IsResolved = resolved; } } public interface INetworkRouter { IDisposable Register(NetworkMessageDescriptor descriptor, Action handler); bool SendToServer(NetworkMessageDescriptor descriptor, ZPackage payload, int correlationId = 0, NetworkMessageFlags flags = NetworkMessageFlags.None); bool SendToPeer(long peerId, NetworkMessageDescriptor descriptor, ZPackage payload, int correlationId = 0, NetworkMessageFlags flags = NetworkMessageFlags.None); int Broadcast(NetworkMessageDescriptor descriptor, ZPackage payload, int correlationId = 0, NetworkMessageFlags flags = NetworkMessageFlags.None); int NextCorrelationId(); IReadOnlyList PeerSnapshot(); } public sealed class NetworkMessageDescriptor { public ModuleId Owner { get; } public ushort MessageType { get; } public int ModuleProtocol { get; } public NetworkDirection Direction { get; } public int MaximumPayloadBytes { get; } public ulong RequiredCapability { get; } public NetworkMessageDescriptor(ModuleId owner, ushort messageType, int moduleProtocol, NetworkDirection direction, int maximumPayloadBytes = 65536, ulong requiredCapability = 0uL) { if (messageType == 0) { throw new ArgumentOutOfRangeException("messageType"); } if (moduleProtocol < 0) { throw new ArgumentOutOfRangeException("moduleProtocol"); } if (maximumPayloadBytes <= 0 || maximumPayloadBytes > 1048576) { throw new ArgumentOutOfRangeException("maximumPayloadBytes"); } Owner = owner; MessageType = messageType; ModuleProtocol = moduleProtocol; Direction = direction; MaximumPayloadBytes = maximumPayloadBytes; RequiredCapability = requiredCapability; } } public sealed class NetworkMessageContext { public long PeerId { get; } public PlayerIdentity Sender { get; } public int CorrelationId { get; } public long Sequence { get; } public NetworkMessageFlags Flags { get; } public NetworkMessageContext(long peerId, PlayerIdentity sender, int correlationId, long sequence, NetworkMessageFlags flags) { PeerId = peerId; Sender = sender; CorrelationId = correlationId; Sequence = sequence; Flags = flags; } } public sealed class NetworkPeerSnapshot { public long PeerId { get; } public bool HandshakeComplete { get; } public bool RequiredModulesCompatible { get; } public string Detail { get; } public IReadOnlyDictionary Capabilities { get; } public NetworkPeerSnapshot(long peerId, bool handshakeComplete, bool requiredModulesCompatible, string detail, IReadOnlyDictionary capabilities) { PeerId = peerId; HandshakeComplete = handshakeComplete; RequiredModulesCompatible = requiredModulesCompatible; Detail = detail ?? string.Empty; Capabilities = capabilities; } } public interface IRulePipelineRegistry { IReadOnlyList PipelineIds { get; } RulePipeline GetOrCreate(string pipelineId); } public interface ICoreServices { IModuleRegistry Modules { get; } ICompatibilityRegistry Compatibility { get; } ILifecycleBus Lifecycle { get; } IGameEventBus Events { get; } IMetricRegistry Metrics { get; } IFeatureCircuitBreaker CircuitBreakers { get; } IMainThreadDispatcher MainThread { get; } ICoreScheduler Scheduler { get; } IConfigurationRegistry Configuration { get; } IAuthoritativePolicyRegistry Policies { get; } INamespaceRegistry Namespaces { get; } IAtomicStoreFactory Stores { get; } ICombatStateService CombatState { get; } IRulePipelineRegistry Rules { get; } IInventoryBroker Inventory { get; } IUiRegistry Ui { get; } IInputRegistry Input { get; } ILocalizationRegistry Localization { get; } INotificationService Notifications { get; } ICommandRegistry Commands { get; } IPlayerIdentityService Identity { get; } INetworkRouter Network { get; } } public static class ModCoreApi { private static ICoreServices _services; public static bool IsAvailable => _services != null; public static int ApiVersion => 1; public static ICoreServices Services => _services ?? throw new InvalidOperationException("ModCore is not initialized."); internal static void Initialize(ICoreServices services) { if (services == null) { throw new ArgumentNullException("services"); } if (Interlocked.CompareExchange(ref _services, services, null) != null) { throw new InvalidOperationException("ModCore was initialized twice."); } } internal static void Reset(ICoreServices expected) { Interlocked.CompareExchange(ref _services, null, expected); } } }