Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of NarcLab DevTools v0.1.4
BepInEx/plugins/NarcLabDevTools/NarcLabDevTools.dll
Decompiled a month agousing System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using UnityEngine; [assembly: AssemblyCompany("NarcLab")] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyTitle("NarcLab DevTools")] [assembly: AssemblyDescription("Private server-authorized Valheim administration tools")] [assembly: AssemblyProduct("NarcLab DevTools")] [assembly: AssemblyFileVersion("0.1.4.0")] [assembly: ComVisible(false)] [assembly: Guid("e320fba1-d90f-4b9c-8ec4-3a3bc074b78c")] [assembly: CompilationRelaxations(8)] [assembly: AssemblyVersion("0.1.4.0")] namespace NarcLab.Valheim.DevTools; internal enum CommandTarget { Unsupported, ClientLocal, ServerWorld, ServerSpawn } internal enum RequestError { None, Disabled, InvalidProtocol, InvalidRequestId, CommandTooLong, InvalidCommand, CommandNotRegistered, NotAuthorized, RateLimited, DuplicateRequest, SpawnSyntax, SpawnAmount, SpawnLevel, SpawnRadius, UnsupportedSpawnOption } internal enum AuthorizationMode { Invalid, DevToolsAdmins, ValheimAdmins, Either, Both } internal sealed class AuthorizationDecision { internal bool Allowed; internal string Source; internal string DenialMessage; } internal sealed class AdminListLoadResult { internal HashSet<string> Ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase); internal bool Created; internal int InvalidCount; internal string ErrorType; } internal sealed class CommandDescriptor { internal string Name; internal bool IsCheat; internal bool OnlyAdmin; internal bool OnlyServer; internal bool RemoteCommand; internal bool HideBehindDevCommands; } internal sealed class SpawnRequest { internal string Prefab; internal int Amount; internal int Level; internal float Radius; } internal static class DevToolsPolicy { internal const int ProtocolVersion = 1; internal const int MaxCommandLength = 512; internal const int MaxRequestIdLength = 48; internal const int MaximumSpawnAmountCeiling = 1000; internal static bool ShouldWrap(CommandDescriptor command) { if (command == null || string.IsNullOrWhiteSpace(command.Name)) { return false; } string text = command.Name.Trim().ToLowerInvariant(); if (text == "devcommands" || text == "confirmcheats") { return false; } if (!command.IsCheat && !command.OnlyAdmin) { return command.HideBehindDevCommands; } return true; } internal static CommandTarget Classify(CommandDescriptor command) { if (!ShouldWrap(command)) { return CommandTarget.Unsupported; } if (string.Equals(command.Name, "spawn", StringComparison.OrdinalIgnoreCase)) { return CommandTarget.ServerSpawn; } if (command.RemoteCommand) { return CommandTarget.ServerWorld; } return CommandTarget.ClientLocal; } internal static CommandTarget ClassifyRuntimeCommand(CommandDescriptor command, bool isQualifiedResourcesSpawn) { if (!isQualifiedResourcesSpawn) { return Classify(command); } return CommandTarget.ServerSpawn; } internal static bool IsRegisteredReplacement(object current, object replacement) { if (replacement != null) { return object.ReferenceEquals(current, replacement); } return false; } internal static RequestError ValidateRequest(int protocolVersion, string requestId, string command, string fullLine) { if (protocolVersion != 1) { return RequestError.InvalidProtocol; } if (!IsSafeRequestId(requestId)) { return RequestError.InvalidRequestId; } if (string.IsNullOrWhiteSpace(command) || !IsSafeCommandName(command)) { return RequestError.InvalidCommand; } if (string.IsNullOrWhiteSpace(fullLine) || fullLine.Length > 512 || HasControlCharacters(fullLine)) { return RequestError.CommandTooLong; } string a = FirstToken(fullLine); if (!string.Equals(a, command, StringComparison.OrdinalIgnoreCase)) { return RequestError.InvalidCommand; } return RequestError.None; } internal static RequestError ParseSpawn(string fullLine, int maximumAmount, out SpawnRequest request) { request = null; if (string.IsNullOrWhiteSpace(fullLine) || fullLine.Length > 512) { return RequestError.SpawnSyntax; } string[] array = fullLine.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (array.Length < 2 || array.Length > 5 || !string.Equals(array[0], "spawn", StringComparison.OrdinalIgnoreCase)) { return RequestError.SpawnSyntax; } string text = array[1].Trim(); if (!IsSafePrefabName(text) || text.EndsWith("*", StringComparison.Ordinal)) { return RequestError.SpawnSyntax; } int result = 1; int result2 = 1; float result3 = 0.5f; if (array.Length >= 3 && !int.TryParse(array[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) { return RequestError.SpawnAmount; } if (result < 1 || result > maximumAmount) { return RequestError.SpawnAmount; } if (array.Length >= 4 && !int.TryParse(array[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out result2)) { return RequestError.SpawnLevel; } if (result2 < 1 || result2 > 9) { return RequestError.SpawnLevel; } if (array.Length >= 5 && !float.TryParse(array[4], NumberStyles.Float, CultureInfo.InvariantCulture, out result3)) { return RequestError.SpawnRadius; } if (result3 < 0f || result3 > 20f) { return RequestError.SpawnRadius; } request = new SpawnRequest { Prefab = text, Amount = result, Level = result2, Radius = result3 }; return RequestError.None; } internal static bool IsPlayersQuery(string fullLine) { if (string.IsNullOrWhiteSpace(fullLine) || HasControlCharacters(fullLine)) { return false; } string[] array = fullLine.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 1) { return string.Equals(array[0], "players", StringComparison.OrdinalIgnoreCase); } return false; } internal static string FormatPlayerList(IList<string> playerNames) { int num = playerNames?.Count ?? 0; if (num == 0) { return "Players online: 0."; } List<string> list = new List<string>(); for (int i = 0; i < playerNames.Count; i++) { string text = SanitizeForLog(playerNames[i], 32); if (!string.IsNullOrWhiteSpace(text) && text != "-") { list.Add(text); } } string text2 = ((list.Count == 0) ? "names unavailable" : string.Join(", ", list.ToArray())); return SanitizeForLog("Players online: " + num + " - " + text2, 150); } internal static bool TryParseAuthorizationMode(string value, out AuthorizationMode mode) { mode = AuthorizationMode.Invalid; string a = ((value == null) ? string.Empty : value.Trim()); if (string.Equals(a, "DevToolsAdmins", StringComparison.OrdinalIgnoreCase)) { mode = AuthorizationMode.DevToolsAdmins; return true; } if (string.Equals(a, "ValheimAdmins", StringComparison.OrdinalIgnoreCase)) { mode = AuthorizationMode.ValheimAdmins; return true; } if (string.Equals(a, "Either", StringComparison.OrdinalIgnoreCase)) { mode = AuthorizationMode.Either; return true; } if (string.Equals(a, "Both", StringComparison.OrdinalIgnoreCase)) { mode = AuthorizationMode.Both; return true; } return false; } internal static bool ModeUsesValheimAdmins(AuthorizationMode mode) { if (mode != AuthorizationMode.ValheimAdmins && mode != AuthorizationMode.Either) { return mode == AuthorizationMode.Both; } return true; } internal static bool ModeUsesDevToolsAdmins(AuthorizationMode mode) { if (mode != AuthorizationMode.DevToolsAdmins && mode != AuthorizationMode.Either) { return mode == AuthorizationMode.Both; } return true; } internal static AuthorizationDecision EvaluateAuthorization(AuthorizationMode mode, bool nativeAdmin, bool devToolsAdmin) { switch (mode) { case AuthorizationMode.DevToolsAdmins: return Decision(devToolsAdmin, "devtools_adminlist", "You are not authorized in the DevTools admin list."); case AuthorizationMode.ValheimAdmins: return Decision(nativeAdmin, "valheim_admin", "Valheim server administrator access is required."); case AuthorizationMode.Either: { bool allowed = devToolsAdmin || nativeAdmin; string source = (devToolsAdmin ? "either_devtools" : "either_valheim"); return Decision(allowed, source, "DevTools or Valheim administrator access is required."); } case AuthorizationMode.Both: return Decision(devToolsAdmin && nativeAdmin, "both", "Both Valheim admin and DevTools authorization are required."); default: return Decision(allowed: false, "invalid_mode", "DevTools authorization is unavailable because the server configuration is invalid."); } } internal static bool TryNormalizePlatformId(string value, out string normalized) { normalized = ((value == null) ? string.Empty : value.Trim()); if (normalized.Length < 3 || normalized.Length > 128 || !char.IsLetter(normalized[0])) { normalized = string.Empty; return false; } bool flag = false; for (int i = 0; i < normalized.Length; i++) { char c = normalized[i]; if (c == '_' || c == ':' || c == '/') { flag = true; } else if (!char.IsLetterOrDigit(c) && c != '-') { normalized = string.Empty; return false; } } if (!flag) { normalized = string.Empty; return false; } return true; } internal static bool HasStrongPeerEvidence(bool peerFound, bool connected, bool ready, bool hasCharacter, bool rpcConnected, bool hasStablePlatformIdentity) { if (peerFound && connected && ready && hasCharacter && rpcConnected) { return hasStablePlatformIdentity; } return false; } internal static string ResolveCanonicalName(IList<string> availableNames, string requested) { if (availableNames == null || string.IsNullOrWhiteSpace(requested)) { return null; } for (int i = 0; i < availableNames.Count; i++) { if (string.Equals(availableNames[i], requested, StringComparison.OrdinalIgnoreCase)) { return availableNames[i]; } } return null; } internal static bool IsResourcesFallbackCandidate(bool resourcesLoaded, CommandDescriptor command, string description) { if (resourcesLoaded && command != null && string.Equals(command.Name, "spawn", StringComparison.OrdinalIgnoreCase) && !command.IsCheat && !command.OnlyAdmin && !command.RemoteCommand && !string.IsNullOrEmpty(description)) { return description.IndexOf("server-authoritative NarcLab resource drop", StringComparison.Ordinal) >= 0; } return false; } internal static string SanitizeForLog(string value, int maxLength) { if (string.IsNullOrEmpty(value)) { return "-"; } char[] array = value.ToCharArray(); for (int i = 0; i < array.Length; i++) { if (char.IsControl(array[i])) { array[i] = '?'; } } string text = new string(array); if (text.Length > maxLength) { return text.Substring(0, maxLength); } return text; } private static string FirstToken(string value) { string[] array = value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (array.Length != 0) { return array[0]; } return string.Empty; } private static bool IsSafeRequestId(string value) { if (string.IsNullOrWhiteSpace(value) || value.Length > 48) { return false; } foreach (char c in value) { if (!char.IsLetterOrDigit(c) && c != '-' && c != '_') { return false; } } return true; } private static bool IsSafeCommandName(string value) { if (value.Length > 64) { return false; } foreach (char c in value) { if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') { return false; } } return true; } private static bool IsSafePrefabName(string value) { if (string.IsNullOrWhiteSpace(value) || value.Length > 128) { return false; } foreach (char c in value) { if (!char.IsLetterOrDigit(c) && c != '_' && c != '-' && c != '$') { return false; } } return true; } private static AuthorizationDecision Decision(bool allowed, string source, string denialMessage) { AuthorizationDecision authorizationDecision = new AuthorizationDecision(); authorizationDecision.Allowed = allowed; authorizationDecision.Source = source; authorizationDecision.DenialMessage = denialMessage; return authorizationDecision; } private static bool HasControlCharacters(string value) { for (int i = 0; i < value.Length; i++) { if (char.IsControl(value[i])) { return true; } } return false; } } internal static class DevToolsAdminList { internal static AdminListLoadResult LoadOrCreate(string path) { AdminListLoadResult adminListLoadResult = new AdminListLoadResult(); try { string directoryName = Path.GetDirectoryName(path); if (string.IsNullOrWhiteSpace(directoryName)) { throw new ArgumentException("admin list directory is missing"); } Directory.CreateDirectory(directoryName); if (!File.Exists(path)) { File.WriteAllText(path, "# One stable Valheim platform identity per line.\r\n", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); adminListLoadResult.Created = true; } AdminListLoadResult adminListLoadResult2 = ParseLines(File.ReadAllLines(path)); adminListLoadResult2.Created = adminListLoadResult.Created; return adminListLoadResult2; } catch (Exception ex) { adminListLoadResult.ErrorType = ex.GetType().Name; return adminListLoadResult; } } internal static AdminListLoadResult ParseLines(IEnumerable<string> lines) { AdminListLoadResult adminListLoadResult = new AdminListLoadResult(); if (lines == null) { return adminListLoadResult; } foreach (string line in lines) { string text = ((line == null) ? string.Empty : line.Trim()); if (text.Length != 0 && !text.StartsWith("#", StringComparison.Ordinal)) { if (!DevToolsPolicy.TryNormalizePlatformId(text, out var normalized)) { adminListLoadResult.InvalidCount++; } else { adminListLoadResult.Ids.Add(normalized); } } } return adminListLoadResult; } } internal sealed class ReplayGuard { private readonly int maximumEntries; private readonly Queue<string> order = new Queue<string>(); private readonly HashSet<string> seen = new HashSet<string>(StringComparer.Ordinal); internal ReplayGuard(int maximumEntries) { this.maximumEntries = Math.Max(1, maximumEntries); } internal bool TryAccept(string requestId) { if (!seen.Add(requestId)) { return false; } order.Enqueue(requestId); while (order.Count > maximumEntries) { seen.Remove(order.Dequeue()); } return true; } } internal static class SpawnPlan { internal static bool TryCreate(int amount, int maxStack, out List<int> stacks) { stacks = null; if (amount < 1 || maxStack < 1) { return false; } List<int> list = new List<int>(); int num = amount; while (num > 0) { int num2 = Math.Min(num, maxStack); list.Add(num2); num -= num2; } stacks = list; return true; } } internal static class SpawnTransaction { internal static bool TryCreate<T>(IList<int> units, Func<int, int, T> create, Action<T> rollback, out List<T> created, out Exception error) { created = new List<T>(); error = null; try { for (int i = 0; i < units.Count; i++) { T val = create(units[i], i); if (object.Equals(val, default(T))) { throw new InvalidOperationException("spawn creation returned no object"); } created.Add(val); } return true; } catch (Exception ex) { error = ex; for (int num = created.Count - 1; num >= 0; num--) { try { rollback(created[num]); } catch { } } created.Clear(); return false; } } } [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInPlugin("ca.narclab.valheim.devtools", "NarcLab DevTools", "0.1.4")] public sealed class NarcLabDevToolsPlugin : BaseUnityPlugin { private sealed class WrappedCommand { internal string Name; internal ConsoleCommand Original; internal ConsoleCommand Replacement; internal CommandTarget Target; internal bool IsResourcesSpawn; } private sealed class PendingCommand { internal WrappedCommand Entry; internal string FullLine; internal Terminal Context; internal DateTime CreatedAt; } public const string PluginGuid = "ca.narclab.valheim.devtools"; public const string PluginName = "NarcLab DevTools"; public const string PluginVersion = "0.1.4"; public const string ResourcesGuid = "ca.narclab.valheim.resources"; private const string RequestRpc = "NarcLabDevTools_Request_v1"; private const string ResponseRpc = "NarcLabDevTools_Response_v1"; private const int PendingTimeoutSeconds = 12; private const int RateWindowSeconds = 5; private const int RateWindowMaximum = 12; private static readonly FieldInfo CommandsField = typeof(Terminal).GetField("commands", BindingFlags.Static | BindingFlags.NonPublic); private static readonly FieldInfo ActionField = typeof(ConsoleCommand).GetField("action", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly FieldInfo ActionFailableField = typeof(ConsoleCommand).GetField("actionFailable", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly MethodInfo ServerPeerIdMethod = typeof(ZRoutedRpc).GetMethod("GetServerPeerID", BindingFlags.Instance | BindingFlags.NonPublic); private readonly Dictionary<string, WrappedCommand> wrapped = new Dictionary<string, WrappedCommand>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, PendingCommand> pending = new Dictionary<string, PendingCommand>(StringComparer.Ordinal); private readonly Dictionary<long, ReplayGuard> replayByPeer = new Dictionary<long, ReplayGuard>(); private readonly Dictionary<long, Queue<DateTime>> rateByPeer = new Dictionary<long, Queue<DateTime>>(); private ConfigEntry<bool> featureEnabled; private ConfigEntry<string> authorizationMode; private ConfigEntry<bool> logCommands; private ConfigEntry<int> maximumSpawnAmount; private HashSet<string> devToolsAdminIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private ZRoutedRpc registeredRpc; private ZRoutedRpc failedRpc; private bool adminListLoaded; private bool invalidAuthorizationModeLogged; private bool registryLogged; private void Awake() { featureEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable NarcLab DevTools."); authorizationMode = ((BaseUnityPlugin)this).Config.Bind<string>("Authorization", "AuthorizationMode", "DevToolsAdmins", "Authorization source: DevToolsAdmins, ValheimAdmins, Either, or Both."); logCommands = ((BaseUnityPlugin)this).Config.Bind<bool>("Audit", "LogCommands", true, "Log accepted and rejected command requests."); maximumSpawnAmount = ((BaseUnityPlugin)this).Config.Bind<int>("Safety", "MaximumSpawnAmount", 1000, "Maximum number of entities or item units created by one spawn request (1-1000)."); ((BaseUnityPlugin)this).Logger.LogInfo((object)("NarcLab DevTools 0.1.4 initialized for Valheim " + Version.GetVersionString(false) + " network=" + 40u)); } private void Update() { EnsureRpcRegistration(); EnsureCommandRegistration(); ExpirePendingCommands(); } private void OnDestroy() { RestoreCommands(); } private void EnsureRpcRegistration() { ZRoutedRpc instance = ZRoutedRpc.instance; if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { EnsureDevToolsAdminListLoaded(); } if (instance == null || object.ReferenceEquals(instance, registeredRpc) || object.ReferenceEquals(instance, failedRpc)) { return; } try { instance.Register<int, string, string, string>("NarcLabDevTools_Request_v1", (Method<int, string, string, string>)HandleRequest); instance.Register<int, string, string, bool, string, string>("NarcLabDevTools_Response_v1", (Method<int, string, string, bool, string, string>)HandleResponse); registeredRpc = instance; ((BaseUnityPlugin)this).Logger.LogInfo((object)"NarcLab DevTools authenticated RPC contract registered"); } catch (Exception ex) { failedRpc = instance; ((BaseUnityPlugin)this).Logger.LogError((object)("RPC registration failed: " + ex.GetType().Name)); } } private void EnsureCommandRegistration() { if (wrapped.Count > 0 || !featureEnabled.Value) { return; } Dictionary<string, ConsoleCommand> commands = GetCommands(); if (commands == null || commands.Count == 0 || (IsResourcesLoaded() && commands.TryGetValue("spawn", out var value) && value.IsCheat)) { return; } List<KeyValuePair<string, ConsoleCommand>> list = new List<KeyValuePair<string, ConsoleCommand>>(commands); int num = 0; int num2 = 0; int num3 = 0; foreach (KeyValuePair<string, ConsoleCommand> item in list) { ConsoleCommand value2 = item.Value; CommandDescriptor command = Describe(value2); bool flag = IsResourcesSpawn(value2); CommandTarget commandTarget = DevToolsPolicy.ClassifyRuntimeCommand(command, flag); if (commandTarget == CommandTarget.Unsupported) { continue; } WrappedCommand wrappedCommand = new WrappedCommand(); wrappedCommand.Name = item.Key; wrappedCommand.Original = value2; wrappedCommand.Target = commandTarget; wrappedCommand.IsResourcesSpawn = flag; WrappedCommand wrappedCommand2 = wrappedCommand; wrappedCommand2.Replacement = CreateWrapper(wrappedCommand2); if (commands.TryGetValue(item.Key, out var value3) && DevToolsPolicy.IsRegisteredReplacement(value3, wrappedCommand2.Replacement)) { wrapped.Add(item.Key, wrappedCommand2); if (commandTarget == CommandTarget.ClientLocal) { num++; } if (commandTarget == CommandTarget.ServerWorld) { num2++; } if (commandTarget == CommandTarget.ServerSpawn) { num3++; } } } if (!registryLogged && wrapped.Count > 0) { registryLogged = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)("Current-build command registry wrapped: total=" + wrapped.Count + " client_local=" + num + " server_world=" + num2 + " server_spawn=" + num3)); } } private ConsoleCommand CreateWrapper(WrappedCommand entry) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Expected O, but got Unknown ConsoleOptionsFetcher val = (ConsoleOptionsFetcher)delegate { try { return entry.Original.GetTabOptions(); } catch { return (List<string>)null; } }; return new ConsoleCommand(entry.Name, entry.Original.Description, (ConsoleEvent)delegate(ConsoleEventArgs args) { SubmitCommand(entry, args); }, false, false, false, entry.Original.IsSecret, true, false, val, true, false, false); } private void SubmitCommand(WrappedCommand entry, ConsoleEventArgs args) { if (!featureEnabled.Value) { AddConsole(args?.Context, "[NarcLab DevTools] Disabled by server configuration."); return; } if ((Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { AddConsole(args?.Context, "[NarcLab DevTools] A connected player session is required."); return; } string text = args?.FullLine; string text2 = Guid.NewGuid().ToString("N"); if (DevToolsPolicy.ValidateRequest(1, text2, entry.Name, text) != RequestError.None) { AddConsole(args.Context, "[NarcLab DevTools] Invalid command request."); return; } if (!TryGetServerPeerId(out var peerId)) { AddConsole(args.Context, "[NarcLab DevTools] Server connection is not ready."); return; } pending[text2] = new PendingCommand { Entry = entry, FullLine = text, Context = args.Context, CreatedAt = DateTime.UtcNow }; try { ZRoutedRpc.instance.InvokeRoutedRPC(peerId, "NarcLabDevTools_Request_v1", new object[4] { 1, text2, entry.Name, text }); } catch (Exception ex) { pending.Remove(text2); ((BaseUnityPlugin)this).Logger.LogWarning((object)("Command request send failed: " + ex.GetType().Name)); AddConsole(args.Context, "[NarcLab DevTools] Command request failed."); } } private void HandleRequest(long sender, int protocol, string requestId, string command, string fullLine) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } RequestError requestError = DevToolsPolicy.ValidateRequest(protocol, requestId, command, fullLine); if (requestError != RequestError.None) { AuditReject(sender, command, requestError.ToString()); SendResponse(sender, requestId, command, approved: false, ErrorCode(requestError), "Command request rejected."); return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (!IsActivePeer(sender, peer)) { AuditReject(sender, command, "peer_not_active"); SendResponse(sender, requestId, command, approved: false, "peer_not_active", "Connected player session required."); return; } if (!AcceptRate(sender)) { AuditReject(sender, command, "rate_limited"); SendResponse(sender, requestId, command, approved: false, "rate_limited", "Too many command requests."); return; } if (!replayByPeer.TryGetValue(sender, out var value)) { value = new ReplayGuard(128); replayByPeer[sender] = value; } if (!value.TryAccept(requestId)) { AuditReject(sender, command, "duplicate_request"); SendResponse(sender, requestId, command, approved: false, "duplicate_request", "Duplicate command request."); return; } if (!wrapped.TryGetValue(command, out var value2)) { AuditReject(sender, command, "command_not_registered"); SendResponse(sender, requestId, command, approved: false, "command_not_registered", "Command is unavailable on this build."); return; } string text = SafePlatformId(peer); string command2 = (value2.Original.IsSecret ? (value2.Name + " <redacted>") : fullLine); EnsureDevToolsAdminListLoaded(); if (!DevToolsPolicy.TryParseAuthorizationMode(authorizationMode.Value, out var mode) && !invalidAuthorizationModeLogged) { invalidAuthorizationModeLogged = true; ((BaseUnityPlugin)this).Logger.LogError((object)"Invalid AuthorizationMode; DevTools requests will be denied."); } bool nativeAdmin = false; if (DevToolsPolicy.ModeUsesValheimAdmins(mode)) { try { nativeAdmin = !string.IsNullOrEmpty(text) && ZNet.instance.IsAdmin(text); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Native admin lookup failed: " + ex.GetType().Name)); } } bool devToolsAdmin = DevToolsPolicy.ModeUsesDevToolsAdmins(mode) && !string.IsNullOrEmpty(text) && devToolsAdminIds.Contains(text); AuthorizationDecision authorizationDecision = DevToolsPolicy.EvaluateAuthorization(mode, nativeAdmin, devToolsAdmin); if (!authorizationDecision.Allowed) { AuditReject(sender, command2, "not_authorized", text, authorizationDecision.Source); SendResponse(sender, requestId, command, approved: false, "not_authorized", authorizationDecision.DenialMessage); } else if (value2.Target == CommandTarget.ServerSpawn) { ExecuteSpawn(sender, peer, requestId, value2, fullLine, text, authorizationDecision.Source); } else if (string.Equals(value2.Name, "players", StringComparison.OrdinalIgnoreCase)) { ExecutePlayers(sender, peer, requestId, value2, fullLine, text, authorizationDecision.Source); } else if (value2.Target == CommandTarget.ServerWorld) { if (InvokeOriginal(value2.Original, fullLine, (Terminal)(object)Console.instance, out var failure)) { AuditAccept(peer, text, command2, value2.Target, "executed_server", authorizationDecision.Source); SendResponse(sender, requestId, command, approved: true, "executed_server", "Command executed by the server."); } else { AuditReject(sender, command2, "server_execution_failed_" + failure, text); SendResponse(sender, requestId, command, approved: false, "execution_failed", "Server command execution failed."); } } else if (value2.Target == CommandTarget.ClientLocal) { AuditAccept(peer, text, command2, value2.Target, "authorized_client_dispatch", authorizationDecision.Source); SendResponse(sender, requestId, command, approved: true, "execute_client", "Command authorized."); } else { AuditReject(sender, command2, "unsupported_command_context", text); SendResponse(sender, requestId, command, approved: false, "unsupported_command_context", "Command is unsupported in this context."); } } private void HandleResponse(long sender, int protocol, string requestId, string command, bool approved, string code, string message) { if (protocol != 1 || !TryGetServerPeerId(out var peerId) || sender != peerId) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Ignored DevTools response from non-server peer=" + sender)); return; } if (!pending.TryGetValue(requestId, out var value) || !string.Equals(value.Entry.Name, command, StringComparison.OrdinalIgnoreCase)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Ignored unmatched DevTools response request=" + DevToolsPolicy.SanitizeForLog(requestId, 48))); return; } pending.Remove(requestId); if (approved && string.Equals(code, "execute_client", StringComparison.Ordinal)) { if (!InvokeOriginal(value.Entry.Original, value.FullLine, value.Context, out var failure)) { AddConsole(value.Context, "[NarcLab DevTools] Local command failed: " + failure + "."); } } else if (approved) { AddConsole(value.Context, "[NarcLab DevTools] " + SafeUserMessage(message)); } else if (value.Entry.IsResourcesSpawn && string.Equals(code, "not_authorized", StringComparison.Ordinal)) { if (!InvokeOriginal(value.Entry.Original, value.FullLine, value.Context, out var _)) { AddConsole(value.Context, "[NarcLab] Resource command failed."); } } else { AddConsole(value.Context, "[NarcLab DevTools] Denied: " + SafeUserMessage(message)); } } private void ExecuteSpawn(long sender, ZNetPeer peer, string requestId, WrappedCommand entry, string fullLine, string platformId, string authorizationSource) { int num = Math.Max(1, Math.Min(1000, maximumSpawnAmount.Value)); SpawnRequest request; RequestError requestError = DevToolsPolicy.ParseSpawn(fullLine, num, out request); string error; Exception exception; if (requestError != RequestError.None) { AuditReject(sender, entry.Name, ErrorCode(requestError), platformId); SendResponse(sender, requestId, entry.Name, approved: false, ErrorCode(requestError), SpawnErrorMessage(requestError, num)); } else if (!TrySpawn(peer, request, out error, out exception)) { AuditReject(sender, entry.Name + " " + request.Prefab + " " + request.Amount, error, platformId); if (exception != null) { ((BaseUnityPlugin)this).Logger.LogError((object)("Spawn diagnostic exception=" + exception.GetType().Name)); } SendResponse(sender, requestId, entry.Name, approved: false, "spawn_failed", "Spawn failed: " + error + "."); } else { AuditAccept(peer, platformId, entry.Name + " " + request.Prefab + " " + request.Amount, entry.Target, "spawned_server_network_objects", authorizationSource); SendResponse(sender, requestId, entry.Name, approved: true, "executed_server", "Spawned " + request.Amount + " " + request.Prefab + "."); } } private void ExecutePlayers(long sender, ZNetPeer peer, string requestId, WrappedCommand entry, string fullLine, string platformId, string authorizationSource) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) if (!DevToolsPolicy.IsPlayersQuery(fullLine)) { AuditReject(sender, entry.Name, "invalid_players_syntax", platformId); SendResponse(sender, requestId, entry.Name, approved: false, "invalid_command", "Usage: players"); return; } try { List<PlayerInfo> playerList = ZNet.instance.GetPlayerList(); List<string> list = new List<string>(); if (playerList != null) { for (int i = 0; i < playerList.Count; i++) { list.Add(playerList[i].m_name); } } string message = DevToolsPolicy.FormatPlayerList(list); AuditAccept(peer, platformId, entry.Name, entry.Target, "reported_player_list", authorizationSource); SendResponse(sender, requestId, entry.Name, approved: true, "players_result", message); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Player list query failed: " + ex.GetType().Name)); AuditReject(sender, entry.Name, "player_list_unavailable", platformId); SendResponse(sender, requestId, entry.Name, approved: false, "execution_failed", "Player list is unavailable."); } } private bool TrySpawn(ZNetPeer peer, SpawnRequest request, out string error, out Exception exception) { //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) error = null; exception = null; if ((Object)(object)ZNetScene.instance == (Object)null) { error = "runtime_not_ready"; return false; } string text = ResolvePrefabName(request.Prefab); GameObject prefab = ((text == null) ? null : ZNetScene.instance.GetPrefab(text)); ZNetView val = (((Object)(object)prefab == (Object)null) ? null : prefab.GetComponent<ZNetView>()); if ((Object)(object)prefab == (Object)null || (Object)(object)val == (Object)null) { error = "network_prefab_not_found"; return false; } ItemDrop component = prefab.GetComponent<ItemDrop>(); List<int> units; if ((Object)(object)component != (Object)null && component.m_itemData != null && component.m_itemData.m_shared != null) { if (!SpawnPlan.TryCreate(request.Amount, component.m_itemData.m_shared.m_maxStackSize, out units)) { error = "invalid_stack_plan"; return false; } } else { units = new List<int>(); for (int i = 0; i < request.Amount; i++) { units.Add(1); } } Vector3 origin = peer.GetRefPos() + Vector3.up * 0.75f; List<GameObject> created; bool flag = SpawnTransaction.TryCreate(units, delegate(int stack, int index) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) float num = (float)index * 2.399963f; float num2 = ((request.Radius <= 0f) ? 0f : (request.Radius * Mathf.Sqrt(((float)index + 0.5f) / (float)units.Count))); Vector3 val2 = new Vector3(Mathf.Cos(num), 0f, Mathf.Sin(num)) * num2; return CreateNetworkObject(prefab, stack, request.Level, origin + val2); }, DestroyNetworkObject, out created, out exception); if (!flag) { error = "transaction_rolled_back"; } return flag; } private static GameObject CreateNetworkObject(GameObject prefab, int stack, int level, Vector3 position) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) GameObject val = null; try { val = Object.Instantiate<GameObject>(prefab, position, Quaternion.identity); if ((Object)(object)val == (Object)null) { throw new InvalidOperationException("instantiation returned null"); } ZNetView component = val.GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid() || !component.IsOwner() || component.GetZDO() == null) { throw new InvalidOperationException("spawned object is not a server-owned network object"); } component.GetZDO().Persistent = true; ItemDrop component2 = val.GetComponent<ItemDrop>(); if ((Object)(object)component2 != (Object)null && component2.m_itemData != null) { component2.m_itemData.m_stack = stack; ItemDrop.SaveToZDO(component2.m_itemData, component.GetZDO(), -1); } Character component3 = val.GetComponent<Character>(); if ((Object)(object)component3 != (Object)null && level > 1) { component3.SetLevel(level); } return val; } catch { DestroyNetworkObject(val); throw; } } private static void DestroyNetworkObject(GameObject value) { if (!((Object)(object)value == (Object)null)) { if ((Object)(object)ZNetScene.instance != (Object)null) { ZNetScene.instance.Destroy(value); } else { Object.Destroy((Object)(object)value); } } } private static string ResolvePrefabName(string requested) { GameObject prefab = ZNetScene.instance.GetPrefab(requested); if ((Object)(object)prefab != (Object)null) { return requested; } return DevToolsPolicy.ResolveCanonicalName(ZNetScene.instance.GetPrefabNames(), requested); } private static bool InvokeOriginal(ConsoleCommand command, string fullLine, Terminal context, out string failure) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown failure = null; if (command == null || string.IsNullOrWhiteSpace(fullLine)) { failure = "invalid_command"; return false; } try { ConsoleEventArgs e = new ConsoleEventArgs(fullLine, context, command); Delegate obj = ((ActionField == null) ? null : (ActionField.GetValue(command) as Delegate)); if ((object)obj != null) { obj.DynamicInvoke(e); return true; } Delegate obj2 = ((ActionFailableField == null) ? null : (ActionFailableField.GetValue(command) as Delegate)); if ((object)obj2 == null) { failure = "action_unavailable"; return false; } object obj3 = obj2.DynamicInvoke(e); if (obj3 is bool && !(bool)obj3) { failure = "parameters_or_context"; return false; } if (obj3 is string && !string.IsNullOrWhiteSpace((string)obj3)) { failure = DevToolsPolicy.SanitizeForLog((string)obj3, 80); return false; } return true; } catch (TargetInvocationException ex) { Exception ex2 = ex.InnerException ?? ex; failure = ex2.GetType().Name; return false; } catch (Exception ex3) { failure = ex3.GetType().Name; return false; } } private static bool IsActivePeer(long sender, ZNetPeer peer) { bool flag = peer != null; bool connected = flag && ZNet.instance.IsConnected(sender); bool ready = flag && peer.IsReady(); bool hasCharacter = flag && !((ZDOID)(ref peer.m_characterID)).IsNone(); bool rpcConnected = flag && peer.m_rpc != null && peer.m_rpc.IsConnected(); bool hasStablePlatformIdentity = flag && peer.m_socket != null && !string.IsNullOrWhiteSpace(SafePlatformId(peer)); return DevToolsPolicy.HasStrongPeerEvidence(flag, connected, ready, hasCharacter, rpcConnected, hasStablePlatformIdentity); } private bool AcceptRate(long sender) { if (!rateByPeer.TryGetValue(sender, out var value)) { value = new Queue<DateTime>(); rateByPeer[sender] = value; } DateTime utcNow = DateTime.UtcNow; while (value.Count > 0 && (utcNow - value.Peek()).TotalSeconds > 5.0) { value.Dequeue(); } if (value.Count >= 12) { return false; } value.Enqueue(utcNow); return true; } private void EnsureDevToolsAdminListLoaded() { if (adminListLoaded) { return; } adminListLoaded = true; string path = Path.Combine(Paths.ConfigPath, "NarcLabDevTools", "adminlist.txt"); AdminListLoadResult adminListLoadResult = DevToolsAdminList.LoadOrCreate(path); devToolsAdminIds = adminListLoadResult.Ids; if (!string.IsNullOrEmpty(adminListLoadResult.ErrorType)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DevTools admin list unavailable; loaded=0 error=" + DevToolsPolicy.SanitizeForLog(adminListLoadResult.ErrorType, 64))); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("DevTools admin list loaded: authorized_ids=" + devToolsAdminIds.Count + " created=" + adminListLoadResult.Created.ToString().ToLowerInvariant())); if (adminListLoadResult.InvalidCount > 0) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Ignored malformed DevTools admin list entries: count=" + adminListLoadResult.InvalidCount)); } } private static string SafePlatformId(ZNetPeer peer) { try { return (peer == null || peer.m_socket == null) ? string.Empty : peer.m_socket.GetHostName(); } catch { return string.Empty; } } private void AuditAccept(ZNetPeer peer, string platformId, string command, CommandTarget target, string result, string authorizationSource) { if (logCommands.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("ACCEPT actor=" + DevToolsPolicy.SanitizeForLog(platformId, 128) + " peer=" + ((peer == null) ? "-" : peer.m_uid.ToString()) + " authorization=" + DevToolsPolicy.SanitizeForLog(authorizationSource, 40) + " target=" + target.ToString() + " command=" + DevToolsPolicy.SanitizeForLog(command, 160) + " result=" + DevToolsPolicy.SanitizeForLog(result, 80))); } } private void AuditReject(long sender, string command, string reason, string platformId = null, string authorizationSource = null) { if (logCommands.Value) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("REJECT actor=" + DevToolsPolicy.SanitizeForLog(platformId, 128) + " peer=" + sender + " authorization=" + DevToolsPolicy.SanitizeForLog(authorizationSource, 40) + " command=" + DevToolsPolicy.SanitizeForLog(command, 80) + " reason=" + DevToolsPolicy.SanitizeForLog(reason, 80))); } } private void SendResponse(long peerId, string requestId, string command, bool approved, string code, string message) { try { ZRoutedRpc.instance.InvokeRoutedRPC(peerId, "NarcLabDevTools_Response_v1", new object[6] { 1, requestId, command, approved, code, message }); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Command response send failed: " + ex.GetType().Name)); } } private void ExpirePendingCommands() { if (pending.Count == 0) { return; } DateTime utcNow = DateTime.UtcNow; List<string> list = new List<string>(); foreach (KeyValuePair<string, PendingCommand> item in pending) { if ((utcNow - item.Value.CreatedAt).TotalSeconds > 12.0) { list.Add(item.Key); } } foreach (string item2 in list) { PendingCommand pendingCommand = pending[item2]; pending.Remove(item2); AddConsole(pendingCommand.Context, "[NarcLab DevTools] Server authorization timed out."); } } private void RestoreCommands() { Dictionary<string, ConsoleCommand> commands = GetCommands(); if (commands == null) { return; } foreach (WrappedCommand value2 in wrapped.Values) { if (commands.TryGetValue(value2.Name, out var value) && object.ReferenceEquals(value, value2.Replacement)) { commands[value2.Name] = value2.Original; } } wrapped.Clear(); } private static Dictionary<string, ConsoleCommand> GetCommands() { try { return (CommandsField == null) ? null : (CommandsField.GetValue(null) as Dictionary<string, ConsoleCommand>); } catch { return null; } } private static CommandDescriptor Describe(ConsoleCommand command) { CommandDescriptor commandDescriptor = new CommandDescriptor(); commandDescriptor.Name = command.Command; commandDescriptor.IsCheat = command.IsCheat; commandDescriptor.OnlyAdmin = command.OnlyAdmin; commandDescriptor.OnlyServer = command.OnlyServer; commandDescriptor.RemoteCommand = command.RemoteCommand; commandDescriptor.HideBehindDevCommands = command.HideBehindDevCommands; return commandDescriptor; } private static bool IsResourcesLoaded() { if (Chainloader.PluginInfos != null) { return Chainloader.PluginInfos.ContainsKey("ca.narclab.valheim.resources"); } return false; } private static bool IsResourcesSpawn(ConsoleCommand command) { if (command != null) { return DevToolsPolicy.IsResourcesFallbackCandidate(IsResourcesLoaded(), Describe(command), command.Description); } return false; } private static bool TryGetServerPeerId(out long peerId) { peerId = 0L; if (ZRoutedRpc.instance == null || ServerPeerIdMethod == null) { return false; } try { object obj = ServerPeerIdMethod.Invoke(ZRoutedRpc.instance, null); if (!(obj is long)) { return false; } peerId = (long)obj; return peerId != 0; } catch { return false; } } private static void AddConsole(Terminal context, string message) { if ((Object)(object)context != (Object)null) { context.AddString(message); } else if ((Object)(object)Console.instance != (Object)null) { ((Terminal)Console.instance).AddString(message); } } private static string SafeUserMessage(string message) { return DevToolsPolicy.SanitizeForLog(message, 160); } private static string ErrorCode(RequestError error) { return error.ToString().ToLowerInvariant(); } private static string SpawnErrorMessage(RequestError error, int limit) { return error switch { RequestError.SpawnAmount => "Amount must be between 1 and " + limit + ".", RequestError.SpawnLevel => "Level must be between 1 and 9.", RequestError.SpawnRadius => "Radius must be between 0 and 20.", _ => "Usage: spawn <prefab> [amount] [level] [radius].", }; } }