using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using BepInEx; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyTitle("NarcLab Resources")] [assembly: AssemblyDescription("Private server-authoritative Wood and Stone commands for Valheim")] [assembly: AssemblyCompany("NarcLab")] [assembly: AssemblyProduct("NarcLab Resources")] [assembly: ComVisible(false)] [assembly: AssemblyFileVersion("0.1.1.0")] [assembly: AssemblyVersion("0.1.1.0")] namespace NarcLab.Valheim.Resources; internal enum ResourceRequestError { None, Syntax, ResourceNotAllowed, AmountOutOfRange, ProtocolUnsupported } internal sealed class ResourceRequest { internal string Resource { get; private set; } internal int Amount { get; private set; } internal ResourceRequest(string resource, int amount) { Resource = resource; Amount = amount; } } internal sealed class ResourceRequestResult { internal bool IsValid => Error == ResourceRequestError.None; internal ResourceRequest Request { get; private set; } internal ResourceRequestError Error { get; private set; } private ResourceRequestResult(ResourceRequest request, ResourceRequestError error) { Request = request; Error = error; } internal static ResourceRequestResult Valid(string resource, int amount) { return new ResourceRequestResult(new ResourceRequest(resource, amount), ResourceRequestError.None); } internal static ResourceRequestResult Invalid(ResourceRequestError error) { return new ResourceRequestResult(null, error); } } internal static class ResourcePolicy { internal const int ProtocolVersion = 1; internal const int MinimumAmount = 1; internal const int MaximumAmount = 1000; private static readonly IDictionary AllowedResources = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "wood", "Wood" }, { "stone", "Stone" } }; internal static int AllowedResourceCount => AllowedResources.Count; internal static ResourceRequestResult ParseCommand(string commandLine) { if (string.IsNullOrWhiteSpace(commandLine)) { return ResourceRequestResult.Invalid(ResourceRequestError.Syntax); } string[] array = commandLine.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (array.Length != 3 || !string.Equals(array[0], "spawn", StringComparison.OrdinalIgnoreCase)) { return ResourceRequestResult.Invalid(ResourceRequestError.Syntax); } if (!int.TryParse(array[2], NumberStyles.None, CultureInfo.InvariantCulture, out var result)) { return ResourceRequestResult.Invalid(ResourceRequestError.AmountOutOfRange); } return ValidateRpc(1, array[1], result); } internal static ResourceRequestResult ValidateRpc(int protocolVersion, string requestedResource, int amount) { if (protocolVersion != 1) { return ResourceRequestResult.Invalid(ResourceRequestError.ProtocolUnsupported); } if (string.IsNullOrWhiteSpace(requestedResource) || !AllowedResources.TryGetValue(requestedResource.Trim(), out var value)) { return ResourceRequestResult.Invalid(ResourceRequestError.ResourceNotAllowed); } if (amount < 1 || amount > 1000) { return ResourceRequestResult.Invalid(ResourceRequestError.AmountOutOfRange); } return ResourceRequestResult.Valid(value, amount); } } internal static class ResourceStackPlan { internal static bool TryCreate(int amount, int maximumStackSize, out List stacks) { stacks = new List(); if (amount < 1 || amount > 1000 || maximumStackSize < 1) { return false; } int num = amount; while (num > 0) { int num2 = Math.Min(num, maximumStackSize); stacks.Add(num2); num -= num2; } return true; } } internal static class ResourceDropTransaction { internal static bool TryCreate(IList stacks, Func create, Action cleanup, out List created, out Exception failure) where T : class { created = new List(); failure = null; if (stacks == null || stacks.Count == 0 || create == null || cleanup == null) { failure = new ArgumentException("invalid resource delivery transaction"); return false; } try { for (int i = 0; i < stacks.Count; i++) { T val = create(stacks[i], i); if (object.ReferenceEquals(val, null)) { throw new InvalidOperationException("resource delivery factory returned null"); } created.Add(val); } return true; } catch (Exception ex) { failure = ex; for (int num = created.Count - 1; num >= 0; num--) { try { cleanup(created[num]); } catch { } } return false; } } } [BepInPlugin("ca.narclab.valheim.resources", "NarcLab Resources", "0.1.1")] public sealed class NarcLabResourcesPlugin : BaseUnityPlugin { public const string PluginGuid = "ca.narclab.valheim.resources"; public const string PluginName = "NarcLab Resources"; public const string PluginVersion = "0.1.1"; private const string RequestRpc = "NarclabResources_Request_v1"; private const string ResponseRpc = "NarclabResources_Response_v1"; private ZRoutedRpc registeredRpc; private ZRoutedRpc failedRpc; private ConsoleCommand resourceCommand; private ConsoleCommand previousSpawnCommand; private bool commandRegistrationRefused; private void Awake() { ((BaseUnityPlugin)this).Logger.LogInfo((object)"NarcLab Resources 0.1.1 initialized; server allowlist=Wood,Stone"); } private void Update() { EnsureRpcRegistration(); EnsureCommandRegistration(); } private void OnDestroy() { if (resourceCommand != null || commandRegistrationRefused) { NarclabTerminalRegistry.RestoreSpawn(resourceCommand, previousSpawnCommand); } } private void EnsureRpcRegistration() { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null || object.ReferenceEquals(instance, registeredRpc) || object.ReferenceEquals(instance, failedRpc)) { return; } try { instance.Register("NarclabResources_Request_v1", (Action)HandleResourceRequest); instance.Register("NarclabResources_Response_v1", (Method)HandleResourceResponse); registeredRpc = instance; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Fixed NarcLab Resources RPC contract registered"); } catch (Exception ex) { failedRpc = instance; ((BaseUnityPlugin)this).Logger.LogError((object)("Fixed RPC registration failed: " + ex.GetType().Name)); } } private void EnsureCommandRegistration() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown if (resourceCommand != null) { return; } if (!NarclabTerminalRegistry.TryReplaceVanillaSpawn(new ConsoleEvent(HandleSpawnCommand), out var previous, out var replacement, out var failureReason)) { if (!string.IsNullOrEmpty(failureReason)) { ((BaseUnityPlugin)this).Logger.LogError((object)("Safe spawn command registration refused: " + failureReason)); commandRegistrationRefused = true; } } else { previousSpawnCommand = previous; resourceCommand = replacement; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Vanilla spawn command replaced with the restricted Wood/Stone command"); } } private void HandleSpawnCommand(ConsoleEventArgs args) { ResourceRequestResult resourceRequestResult = ResourcePolicy.ParseCommand(args?.FullLine); if (!resourceRequestResult.IsValid) { AddConsoleMessage(args, MessageForError(resourceRequestResult.Error)); return; } if ((Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { AddConsoleMessage(args, "[NarcLab] Resource request failed."); return; } try { if (!TryGetServerPeerId(out var serverPeerId)) { AddConsoleMessage(args, "[NarcLab] Resource request failed."); return; } ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerId, "NarclabResources_Request_v1", new object[3] { 1, resourceRequestResult.Request.Resource, resourceRequestResult.Request.Amount }); AddConsoleMessage(args, "[NarcLab] Resource request sent."); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Resource request RPC failed: " + ex.GetType().Name)); AddConsoleMessage(args, "[NarcLab] Resource request failed."); } } private void HandleResourceRequest(long sender, int protocolVersion, string resource, int amount) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("REJECT peer=" + sender + " reason=not_server")); return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer == null || !ZNet.instance.IsConnected(sender) || !peer.IsReady() || ((ZDOID)(ref peer.m_characterID)).IsNone()) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("REJECT peer=" + sender + " reason=peer_not_active")); return; } ResourceRequestResult resourceRequestResult = ResourcePolicy.ValidateRpc(protocolVersion, resource, amount); string error; Exception deliveryException; if (!resourceRequestResult.IsValid) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("REJECT player=" + SafeLogValue(peer.m_playerName) + " peer=" + peer.m_uid + " resource=" + SafeLogValue(resource) + " amount=" + amount + " reason=" + ErrorCode(resourceRequestResult.Error))); SendResponse(sender, resourceRequestResult.Error, string.Empty, 0); } else if (!TryDropResource(peer, resourceRequestResult.Request, out error, out deliveryException)) { ((BaseUnityPlugin)this).Logger.LogError((object)("REJECT player=" + SafeLogValue(peer.m_playerName) + " peer=" + peer.m_uid + " resource=" + resourceRequestResult.Request.Resource + " amount=" + resourceRequestResult.Request.Amount + " reason=" + error)); if (deliveryException != null) { ((BaseUnityPlugin)this).Logger.LogError((object)("DROP DIAGNOSTIC resource=" + resourceRequestResult.Request.Resource + " amount=" + resourceRequestResult.Request.Amount + " exception=" + SafeException(deliveryException))); } SendResponse(sender, ResourceRequestError.Syntax, string.Empty, 0, "delivery_failed"); } else { ((BaseUnityPlugin)this).Logger.LogInfo((object)("ACCEPT player=" + SafeLogValue(peer.m_playerName) + " peer=" + peer.m_uid + " resource=" + resourceRequestResult.Request.Resource + " amount=" + resourceRequestResult.Request.Amount)); SendResponse(sender, ResourceRequestError.None, resourceRequestResult.Request.Resource, resourceRequestResult.Request.Amount); } } private bool TryDropResource(ZNetPeer peer, ResourceRequest request, out string error, out Exception deliveryException) { //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) error = null; deliveryException = null; if ((Object)(object)ZNetScene.instance == (Object)null) { error = "runtime_not_ready"; return false; } GameObject networkPrefab = ZNetScene.instance.GetPrefab(request.Resource); ItemDrop val = (((Object)(object)networkPrefab == (Object)null) ? null : networkPrefab.GetComponent()); ZNetView val2 = (((Object)(object)networkPrefab == (Object)null) ? null : networkPrefab.GetComponent()); if ((Object)(object)val == (Object)null || val.m_itemData == null || val.m_itemData.m_shared == null || (Object)(object)val2 == (Object)null) { error = "qualified_network_prefab_missing"; return false; } int maxStackSize = val.m_itemData.m_shared.m_maxStackSize; if (maxStackSize < 1) { error = "invalid_stack_size"; return false; } if (!ResourceStackPlan.TryCreate(request.Amount, maxStackSize, out var stacks)) { error = "invalid_stack_plan"; return false; } Vector3 origin = peer.GetRefPos() + Vector3.up * 0.75f; if (!ResourceDropTransaction.TryCreate(stacks, delegate(int stack, int index) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) float num = (float)index * 0.7f; Vector3 val3 = new Vector3(Mathf.Cos(num), 0f, Mathf.Sin(num)) * (0.25f + 0.04f * (float)index); return CreateNetworkedDrop(networkPrefab, stack, origin + val3); }, DestroyTrackedDrop, out var _, out deliveryException)) { error = "drop_failed_" + ((deliveryException == null) ? "Unknown" : deliveryException.GetType().Name); return false; } return true; } private static ItemDrop CreateNetworkedDrop(GameObject networkPrefab, int stack, 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(networkPrefab, position, Quaternion.identity); if ((Object)(object)val == (Object)null) { throw new InvalidOperationException("network prefab instantiation returned null"); } ItemDrop component = val.GetComponent(); ZNetView component2 = val.GetComponent(); if ((Object)(object)component == (Object)null || component.m_itemData == null || component.m_itemData.m_shared == null || (Object)(object)component2 == (Object)null) { throw new InvalidOperationException("network prefab is missing required item components"); } if (!component2.IsValid() || !component2.IsOwner()) { throw new InvalidOperationException("created network item is not server-owned"); } component.m_itemData.m_stack = stack; ZDO zDO = component2.GetZDO(); if (zDO == null) { throw new InvalidOperationException("created network item has no ZDO"); } ItemDrop.SaveToZDO(component.m_itemData, zDO, -1); return component; } catch { DestroyTrackedObject(val); throw; } } private static void DestroyTrackedDrop(ItemDrop drop) { if ((Object)(object)drop != (Object)null) { DestroyTrackedObject(((Component)drop).gameObject); } } private static void DestroyTrackedObject(GameObject gameObject) { if (!((Object)(object)gameObject == (Object)null)) { if ((Object)(object)ZNetScene.instance != (Object)null) { ZNetScene.instance.Destroy(gameObject); } else { Object.Destroy((Object)(object)gameObject); } } } private void SendResponse(long peerId, ResourceRequestError error, string resource, int amount) { SendResponse(peerId, error, resource, amount, ErrorCode(error)); } private void SendResponse(long peerId, ResourceRequestError error, string resource, int amount, string code) { try { ZRoutedRpc.instance.InvokeRoutedRPC(peerId, "NarclabResources_Response_v1", new object[4] { 1, resource ?? string.Empty, amount, code }); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Resource response RPC failed: " + ex.GetType().Name)); } } private void HandleResourceResponse(long sender, int protocolVersion, string resource, int amount, string code) { if (ZRoutedRpc.instance == null || !TryGetServerPeerId(out var serverPeerId) || sender != serverPeerId) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Ignored resource response from non-server peer=" + sender)); return; } if (protocolVersion != 1) { AddConsoleMessage(null, "[NarcLab] Resource request failed."); return; } ResourceRequestResult resourceRequestResult = ResourcePolicy.ValidateRpc(protocolVersion, resource, amount); if (string.Equals(code, "success", StringComparison.Ordinal) && resourceRequestResult.IsValid) { AddConsoleMessage(null, "[NarcLab] Spawned " + amount + " " + resourceRequestResult.Request.Resource + "."); } else { AddConsoleMessage(null, MessageForCode(code)); } } private static string MessageForError(ResourceRequestError error) { return error switch { ResourceRequestError.ResourceNotAllowed => "[NarcLab] Only Wood and Stone are allowed.", ResourceRequestError.AmountOutOfRange => "[NarcLab] Amount must be between 1 and 1000.", _ => "[NarcLab] Usage: spawn wood <1-1000> or spawn stone <1-1000>", }; } private static string MessageForCode(string code) { if (string.Equals(code, "resource_not_allowed", StringComparison.Ordinal)) { return "[NarcLab] Only Wood and Stone are allowed."; } if (string.Equals(code, "amount_out_of_range", StringComparison.Ordinal)) { return "[NarcLab] Amount must be between 1 and 1000."; } return "[NarcLab] Resource request failed."; } private static string ErrorCode(ResourceRequestError error) { return error switch { ResourceRequestError.None => "success", ResourceRequestError.ResourceNotAllowed => "resource_not_allowed", ResourceRequestError.AmountOutOfRange => "amount_out_of_range", ResourceRequestError.ProtocolUnsupported => "protocol_unsupported", _ => "invalid_request", }; } private static string SafeLogValue(string value) { if (string.IsNullOrEmpty(value)) { return "unknown"; } string text = value.Replace('\r', '_').Replace('\n', '_').Replace('\t', '_'); if (text.Length <= 64) { return text; } return text.Substring(0, 64); } private static string SafeException(Exception exception) { string text = exception.ToString().Replace('\r', '_').Replace('\n', ' ') .Replace('\t', ' '); if (text.Length <= 2048) { return text; } return text.Substring(0, 2048); } private static bool TryGetServerPeerId(out long serverPeerId) { serverPeerId = 0L; if ((Object)(object)ZNet.instance == (Object)null) { return false; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer == null || serverPeer.m_uid == 0) { return false; } serverPeerId = serverPeer.m_uid; return true; } private static void AddConsoleMessage(ConsoleEventArgs args, string message) { if (args != null && (Object)(object)args.Context != (Object)null) { args.Context.AddString(message); } else if ((Object)(object)Console.instance != (Object)null) { ((Terminal)Console.instance).AddString(message); } } } internal static class NarclabTerminalRegistry { private static readonly FieldInfo CommandsField = typeof(Terminal).GetField("commands", BindingFlags.Static | BindingFlags.NonPublic); private static Dictionary GetCommands() { if (!(CommandsField == null)) { return CommandsField.GetValue(null) as Dictionary; } return null; } internal static bool TryReplaceVanillaSpawn(ConsoleEvent action, out ConsoleCommand previous, out ConsoleCommand replacement, out string failureReason) { //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Expected O, but got Unknown previous = null; replacement = null; failureReason = null; Dictionary commands; try { commands = GetCommands(); } catch (Exception ex) { failureReason = "Valheim command registry read failed: " + ex.GetType().Name; return false; } if (commands == null) { failureReason = "Valheim 1.0.7 command registry is unavailable"; return false; } if (!commands.TryGetValue("spawn", out previous)) { return false; } if (!string.Equals(previous.Command, "spawn", StringComparison.OrdinalIgnoreCase) || !previous.IsCheat || !previous.OnlyAdmin) { failureReason = "existing spawn command is not the qualified vanilla command"; return false; } try { replacement = new ConsoleCommand("spawn", "wood|stone <1-1000> - request a server-authoritative NarcLab resource drop", action, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } catch (Exception ex2) { commands["spawn"] = previous; replacement = null; failureReason = "qualified spawn registration failed: " + ex2.GetType().Name; return false; } return object.ReferenceEquals(commands["spawn"], replacement); } internal static void RestoreSpawn(ConsoleCommand replacement, ConsoleCommand previous) { try { Dictionary commands = GetCommands(); if (commands != null && commands.TryGetValue("spawn", out var value) && object.ReferenceEquals(value, replacement) && previous != null) { commands["spawn"] = previous; } } catch { } } }