Decompiled source of NarcLab Resources v0.1.1

BepInEx/plugins/NarcLabResources/NarcLabResources.dll

Decompiled 7 hours ago
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<string, string> AllowedResources = new Dictionary<string, string>(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<int> stacks)
	{
		stacks = new List<int>();
		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<T>(IList<int> stacks, Func<int, int, T> create, Action<T> cleanup, out List<T> created, out Exception failure) where T : class
	{
		created = new List<T>();
		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<int, string, int>("NarclabResources_Request_v1", (Action<long, int, string, int>)HandleResourceRequest);
			instance.Register<int, string, int, string>("NarclabResources_Response_v1", (Method<int, string, int, string>)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<ItemDrop>());
		ZNetView val2 = (((Object)(object)networkPrefab == (Object)null) ? null : networkPrefab.GetComponent<ZNetView>());
		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<GameObject>(networkPrefab, position, Quaternion.identity);
			if ((Object)(object)val == (Object)null)
			{
				throw new InvalidOperationException("network prefab instantiation returned null");
			}
			ItemDrop component = val.GetComponent<ItemDrop>();
			ZNetView component2 = val.GetComponent<ZNetView>();
			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<string, ConsoleCommand> GetCommands()
	{
		if (!(CommandsField == null))
		{
			return CommandsField.GetValue(null) as Dictionary<string, ConsoleCommand>;
		}
		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<string, ConsoleCommand> 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<string, ConsoleCommand> commands = GetCommands();
			if (commands != null && commands.TryGetValue("spawn", out var value) && object.ReferenceEquals(value, replacement) && previous != null)
			{
				commands["spawn"] = previous;
			}
		}
		catch
		{
		}
	}
}