Decompiled source of HOTServer v0.4.0

HOTServer.dll

Decompiled 9 hours ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using Microsoft.CodeAnalysis;
using PurrLobby;
using Steamworks;
using UnityEngine;
using UnityEngine.SceneManagement;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("HOTServer")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.4.0.0")]
[assembly: AssemblyInformationalVersion("0.4.0+6decb51e5ff16673f46408194e4ac3dd1167b8f8")]
[assembly: AssemblyProduct("HOTServer")]
[assembly: AssemblyTitle("HOTServer")]
[assembly: AssemblyVersion("0.4.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;
		}
	}
}
[BepInPlugin("awe.hotserver", "HOTServer", "0.1.0")]
public class HOTServerPlugin : BaseUnityPlugin
{
	private const int MenuScene = 1;

	private const int WorldScene = 2;

	private static readonly string[] TagKeys = new string[5] { "focus", "mature", "chill", "break", "modded" };

	private ConfigEntry<bool> _enabled;

	private ConfigEntry<bool> _public;

	private ConfigEntry<bool> _voice;

	private ConfigEntry<bool> _request;

	private ConfigEntry<bool> _rebootEnabled;

	private ConfigEntry<string> _lobbyName;

	private ConfigEntry<string> _tags;

	private ConfigEntry<string> _rebootTime;

	private ConfigEntry<int> _maxPlayers;

	private ConfigEntry<int> _port;

	private ConfigEntry<int> _maxFps;

	private readonly ConcurrentQueue<(string cmd, TaskCompletionSource<string> tcs)> _cmds = new ConcurrentQueue<(string, TaskCompletionSource<string>)>();

	private bool _inMenu;

	private float _nextAttempt = -1f;

	private float _lastAttempt;

	private int _attempts;

	private TcpListener _listener;

	private DateTime _nextReboot = DateTime.MaxValue;

	private bool _quitting;

	private bool _active;

	private static string StopFlag => Path.Combine(Paths.BepInExRootPath, "hotserver.stopped");

	private void Awake()
	{
		_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Auto-creates the server when you reach the main menu.\ntrue  = this system runs as a headless server (auto-hosts on game launch only when -batchmode arg exists).\nfalse = the mod does nothing. Mainly for playing the game normally.");
		_port = ((BaseUnityPlugin)this).Config.Bind<int>("General", "Port", 27555, "Port that the server should listen on.\nYou generally should not change this value.");
		_rebootEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "RebootEnabled", false, "Schedule server reboots at a specific time");
		_rebootTime = ((BaseUnityPlugin)this).Config.Bind<string>("General", "RebootTime", "00:00", "Format: HH:MM (24h)");
		_maxFps = ((BaseUnityPlugin)this).Config.Bind<int>("General", "TargetFps", 30, "Target tick/frame rate for the server");
		_lobbyName = ((BaseUnityPlugin)this).Config.Bind<string>("Lobby", "Name", "My HOTServer", "Lobby name.");
		_maxPlayers = ((BaseUnityPlugin)this).Config.Bind<int>("Lobby", "MaxPlayers", 16, "Max players (>16 needs other third party mods that increase player limit).");
		_public = ((BaseUnityPlugin)this).Config.Bind<bool>("Lobby", "Public", true, "Server visibility (Public or Private, false = Private).");
		_voice = ((BaseUnityPlugin)this).Config.Bind<bool>("Lobby", "VoiceChat", false, "Enable voice chat functionality.");
		_request = ((BaseUnityPlugin)this).Config.Bind<bool>("Lobby", "RequestToJoin", false, "Players must request to join.");
		_tags = ((BaseUnityPlugin)this).Config.Bind<string>("Lobby", "Tags", "focus", "List of tags, comma separated: focus,mature,chill,break,modded");
		if (!_enabled.Value || !Application.isBatchMode)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)"HOTServer idle (needs Enabled = true and -batchmode).");
			return;
		}
		_active = true;
		try
		{
			if (File.Exists(StopFlag))
			{
				File.Delete(StopFlag);
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Stop flag: " + ex.Message));
		}
		Application.runInBackground = true;
		ApplyPerf();
		ScheduleReboot();
		SceneManager.sceneLoaded += OnSceneLoaded;
		StartControl();
		((BaseUnityPlugin)this).Logger.LogInfo((object)$"HOTServer up. batchmode={Application.isBatchMode} control=127.0.0.1:{_port.Value}");
	}

	private void ApplyPerf()
	{
		QualitySettings.vSyncCount = 0;
		Application.targetFrameRate = Math.Max(1, _maxFps.Value);
		AudioListener.volume = 0f;
		if (Application.isBatchMode)
		{
			Camera[] allCameras = Camera.allCameras;
			for (int i = 0; i < allCameras.Length; i++)
			{
				((Behaviour)allCameras[i]).enabled = false;
			}
		}
	}

	private void ScheduleReboot()
	{
		if (!_rebootEnabled.Value)
		{
			_nextReboot = DateTime.MaxValue;
			return;
		}
		if (!DateTime.TryParseExact(_rebootTime.Value.Trim(), "H:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Bad RebootTime '" + _rebootTime.Value + "', using 00:00"));
			result = DateTime.Today;
		}
		DateTime dateTime = DateTime.Today.Add(result.TimeOfDay);
		if (dateTime <= DateTime.Now.AddMinutes(1.0))
		{
			dateTime = dateTime.AddDays(1.0);
		}
		_nextReboot = dateTime;
		((BaseUnityPlugin)this).Logger.LogInfo((object)$"Next reboot: {_nextReboot:yyyy-MM-dd HH:mm}");
	}

	private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
	{
		((BaseUnityPlugin)this).Logger.LogInfo((object)$"Scene loaded: {((Scene)(ref scene)).buildIndex} {((Scene)(ref scene)).name}");
		ApplyPerf();
		_inMenu = ((Scene)(ref scene)).buildIndex == 1;
		if (_inMenu)
		{
			_attempts = 0;
			_nextAttempt = Time.realtimeSinceStartup + 10f;
		}
		else
		{
			_nextAttempt = -1f;
		}
	}

	private void Update()
	{
		if (!_active)
		{
			return;
		}
		(string, TaskCompletionSource<string>) result;
		while (_cmds.TryDequeue(out result))
		{
			try
			{
				result.Item2.TrySetResult(Handle(result.Item1));
			}
			catch (Exception ex)
			{
				result.Item2.TrySetResult("ERR " + ex.Message);
			}
		}
		if (!_quitting && DateTime.Now >= _nextReboot)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Scheduled reboot.");
			Quit(intentionalStop: false);
		}
		else if (_inMenu && _nextAttempt > 0f && Time.realtimeSinceStartup >= _nextAttempt)
		{
			TryHost();
		}
	}

	private void TryHost()
	{
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		MultiplayerManager i = MonoSingleton<MultiplayerManager>.I;
		MainMenuUIController i2 = MonoSingleton<MainMenuUIController>.I;
		if ((Object)(object)i == (Object)null || (Object)(object)i2 == (Object)null)
		{
			_nextAttempt = realtimeSinceStartup + 5f;
			((BaseUnityPlugin)this).Logger.LogWarning((object)"Menu singletons not ready.");
			return;
		}
		if (i.IsConnecting)
		{
			if (_attempts <= 0 || !(realtimeSinceStartup - _lastAttempt > 60f))
			{
				_nextAttempt = realtimeSinceStartup + 10f;
				return;
			}
			((BaseUnityPlugin)this).Logger.LogWarning((object)"Stuck connecting, resetting.");
			i.IsConnecting = false;
		}
		ApplyFilters(i);
		i2.CreateSessionNameInputField.text = _lobbyName.Value;
		_attempts++;
		_lastAttempt = realtimeSinceStartup;
		_nextAttempt = realtimeSinceStartup + Math.Min(30f * (float)_attempts, 300f);
		((BaseUnityPlugin)this).Logger.LogInfo((object)$"Hosting '{_lobbyName.Value}' (attempt {_attempts})");
		i.CreateLobby();
	}

	private void ApplyFilters(MultiplayerManager mm)
	{
		//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_0045: Unknown result type (might be due to invalid IL or missing references)
		//IL_0046: 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)
		mm.FilterPlayerValue = Math.Max(1, _maxPlayers.Value) - 1;
		FilterType val = (FilterType)Enum.Parse(typeof(FilterType), "Private");
		mm.FilterTypeValue = (FilterType)(_public.Value ? (1 - val) : val);
		mm.FilterIsVoiceChat = _voice.Value;
		mm.FilterIsRequestToJoin = _request.Value;
		string[] source = TagSet();
		for (int i = 0; i < TagKeys.Length; i++)
		{
			mm.FilterSocialTags[i] = source.Contains(TagKeys[i]);
		}
	}

	private string[] TagSet()
	{
		return _tags.Value.ToLowerInvariant().Split(new char[2] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
	}

	private bool TryGetOwnedLobby(out CSteamID id)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_0027: Unknown result type (might be due to invalid IL or missing references)
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		//IL_0048: Unknown result type (might be due to invalid IL or missing references)
		//IL_004e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0053: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		id = default(CSteamID);
		try
		{
			LobbyManager val = MonoSingleton<MultiplayerManager>.I?._lobbyManager;
			if ((Object)(object)val == (Object)null)
			{
				return false;
			}
			if (!ulong.TryParse(val.CurrentLobby.lobbyId, out var result) || result == 0L)
			{
				return false;
			}
			id = new CSteamID(result);
			return SteamMatchmaking.GetLobbyOwner(id) == SteamUser.GetSteamID();
		}
		catch
		{
			return false;
		}
	}

	private static string TF(bool b)
	{
		if (!b)
		{
			return "f";
		}
		return "t";
	}

	private void PushLive(string key)
	{
		//IL_0060: Unknown result type (might be due to invalid IL or missing references)
		//IL_0078: 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_00b2: Unknown result type (might be due to invalid IL or missing references)
		//IL_0106: Unknown result type (might be due to invalid IL or missing references)
		//IL_0118: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e5: Unknown result type (might be due to invalid IL or missing references)
		if (!TryGetOwnedLobby(out var id))
		{
			return;
		}
		switch (key)
		{
		case "name":
			SteamMatchmaking.SetLobbyData(id, "Name", _lobbyName.Value);
			break;
		case "public":
			SteamMatchmaking.SetLobbyData(id, "public", TF(_public.Value));
			break;
		case "voice":
			SteamMatchmaking.SetLobbyData(id, "voice", TF(_voice.Value));
			break;
		case "request":
			SteamMatchmaking.SetLobbyData(id, "request", TF(_request.Value));
			break;
		case "tags":
		{
			string[] source = TagSet();
			string[] tagKeys = TagKeys;
			foreach (string text in tagKeys)
			{
				SteamMatchmaking.SetLobbyData(id, text, TF(source.Contains(text)));
			}
			break;
		}
		case "maxplayers":
			SteamMatchmaking.SetLobbyMemberLimit(id, _maxPlayers.Value);
			SteamMatchmaking.SetLobbyData(id, "count", _maxPlayers.Value.ToString());
			break;
		}
	}

	private string Handle(string line)
	{
		//IL_0284: Unknown result type (might be due to invalid IL or missing references)
		//IL_0176: Unknown result type (might be due to invalid IL or missing references)
		//IL_017b: Unknown result type (might be due to invalid IL or missing references)
		//IL_02aa: Unknown result type (might be due to invalid IL or missing references)
		//IL_02ae: Unknown result type (might be due to invalid IL or missing references)
		//IL_02b3: Unknown result type (might be due to invalid IL or missing references)
		//IL_02bc: Unknown result type (might be due to invalid IL or missing references)
		//IL_02c8: Unknown result type (might be due to invalid IL or missing references)
		//IL_01fd: Unknown result type (might be due to invalid IL or missing references)
		//IL_020c: Unknown result type (might be due to invalid IL or missing references)
		//IL_021b: Unknown result type (might be due to invalid IL or missing references)
		//IL_022a: Unknown result type (might be due to invalid IL or missing references)
		string[] array = line.Split(new char[1] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
		if (array.Length == 0)
		{
			return "";
		}
		switch (array[0].ToLowerInvariant())
		{
		case "help":
			return "status | players | config | set <name|maxplayers|public|voice|request|reboot> <value> | tag <add|remove> <tag> | reboot | stop | start";
		case "status":
		{
			StringBuilder stringBuilder2 = new StringBuilder();
			Scene activeScene = SceneManager.GetActiveScene();
			stringBuilder2.AppendLine($"scene={((Scene)(ref activeScene)).buildIndex} fps={1f / Time.smoothDeltaTime:F1} batchmode={Application.isBatchMode}");
			stringBuilder2.AppendLine((_nextReboot == DateTime.MaxValue) ? "reboot=off" : $"reboot={_nextReboot:yyyy-MM-dd HH:mm}");
			if (TryGetOwnedLobby(out var id2))
			{
				stringBuilder2.Append(string.Format("lobby={0} members={1}/{2} name=\"{3}\"", id2.m_SteamID, SteamMatchmaking.GetNumLobbyMembers(id2), SteamMatchmaking.GetLobbyMemberLimit(id2), SteamMatchmaking.GetLobbyData(id2, "Name")));
			}
			else
			{
				stringBuilder2.Append($"lobby=none inMenu={_inMenu} attempts={_attempts}");
			}
			return stringBuilder2.ToString();
		}
		case "players":
		{
			if (!TryGetOwnedLobby(out var id))
			{
				return "no lobby";
			}
			int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(id);
			StringBuilder stringBuilder = new StringBuilder($"{numLobbyMembers} member(s)");
			for (int i = 0; i < numLobbyMembers; i++)
			{
				CSteamID lobbyMemberByIndex = SteamMatchmaking.GetLobbyMemberByIndex(id, i);
				stringBuilder.Append($"\n{lobbyMemberByIndex.m_SteamID} {SteamFriends.GetFriendPersonaName(lobbyMemberByIndex)}");
			}
			return stringBuilder.ToString();
		}
		case "tag":
		{
			string text3 = ((array.Length > 1) ? array[1].ToLowerInvariant() : "");
			if (array.Length < 3 || (text3 != "add" && text3 != "remove"))
			{
				return "usage: tag <add|remove> <tag>   (tags: " + string.Join(",", TagKeys) + ")";
			}
			string text4 = array[2].Trim().ToLowerInvariant();
			if (!TagKeys.Contains(text4))
			{
				return "ERR unknown tag (tags: " + string.Join(",", TagKeys) + ")";
			}
			List<string> list = TagSet().ToList();
			if (text3 == "add")
			{
				if (!list.Contains(text4))
				{
					list.Add(text4);
				}
			}
			else
			{
				list.Remove(text4);
			}
			_tags.Value = string.Join(",", list);
			PushLive("tags");
			return "OK tags=" + ((_tags.Value.Length > 0) ? _tags.Value : "(none)");
		}
		case "config":
			return $"name=\"{_lobbyName.Value}\" maxplayers={_maxPlayers.Value} public={_public.Value} voice={_voice.Value} request={_request.Value} tags={_tags.Value}";
		case "set":
		{
			if (array.Length < 3)
			{
				return "usage: set <key> <value>";
			}
			string text = array[1].ToLowerInvariant();
			string text2 = array[2].Trim();
			switch (text)
			{
			case "name":
				_lobbyName.Value = text2;
				break;
			case "maxplayers":
			{
				if (!int.TryParse(text2, out var result2) || result2 < 1)
				{
					return "ERR int >= 1";
				}
				_maxPlayers.Value = result2;
				break;
			}
			case "public":
				_public.Value = ParseBool(text2);
				break;
			case "voice":
				_voice.Value = ParseBool(text2);
				break;
			case "request":
				_request.Value = ParseBool(text2);
				break;
			case "reboot":
				if (text2.ToLowerInvariant() == "off")
				{
					_rebootEnabled.Value = false;
				}
				else
				{
					if (!DateTime.TryParseExact(text2, "H:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var _))
					{
						return "ERR format HH:MM (24h) or off";
					}
					_rebootTime.Value = text2;
					_rebootEnabled.Value = true;
				}
				ScheduleReboot();
				if (!_rebootEnabled.Value)
				{
					return "OK reboot off";
				}
				return $"OK next reboot {_nextReboot:yyyy-MM-dd HH:mm}";
			default:
				return "ERR unknown key";
			}
			PushLive(text);
			if (!(text == "maxplayers"))
			{
				return "OK";
			}
			return "OK (relaunch to save changes)";
		}
		case "reboot":
			Quit(intentionalStop: false);
			return "OK rebooting";
		case "stop":
			Quit(intentionalStop: true);
			return "OK stopping";
		case "start":
			return "already running";
		default:
			return "ERR unknown command (try: help)";
		}
	}

	private void Quit(bool intentionalStop)
	{
		if (_quitting)
		{
			return;
		}
		_quitting = true;
		if (intentionalStop)
		{
			try
			{
				File.WriteAllText(StopFlag, DateTime.Now.ToString("o"));
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Stop flag: " + ex.Message));
			}
		}
		((MonoBehaviour)this).StartCoroutine(QuitSoon());
	}

	private static bool ParseBool(string v)
	{
		v = v.ToLowerInvariant();
		switch (v)
		{
		default:
			return v == "t";
		case "1":
		case "true":
		case "yes":
		case "on":
			return true;
		}
	}

	private IEnumerator QuitSoon()
	{
		yield return (object)new WaitForSecondsRealtime(1f);
		Application.Quit();
	}

	private void StartControl()
	{
		try
		{
			_listener = new TcpListener(IPAddress.Loopback, _port.Value);
			_listener.Start();
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("Control port failed: " + ex.Message));
			return;
		}
		Thread thread = new Thread((ThreadStart)delegate
		{
			while (true)
			{
				try
				{
					TcpClient client = _listener.AcceptTcpClient();
					ThreadPool.QueueUserWorkItem(delegate
					{
						Serve(client);
					});
				}
				catch (Exception ex2)
				{
					((BaseUnityPlugin)this).Logger.LogError((object)ex2);
					Thread.Sleep(1000);
				}
			}
		});
		thread.IsBackground = true;
		thread.Name = "HOTServerControl";
		thread.Start();
	}

	private void Serve(TcpClient client)
	{
		try
		{
			using (client)
			{
				using NetworkStream stream = client.GetStream();
				using StreamReader streamReader = new StreamReader(stream);
				using StreamWriter streamWriter = new StreamWriter(stream)
				{
					AutoFlush = true,
					NewLine = "\n"
				};
				string text;
				while ((text = streamReader.ReadLine()) != null)
				{
					TaskCompletionSource<string> taskCompletionSource = new TaskCompletionSource<string>();
					_cmds.Enqueue((text.Trim(), taskCompletionSource));
					streamWriter.WriteLine(taskCompletionSource.Task.Wait(5000) ? taskCompletionSource.Task.Result : "ERR timeout (main thread busy)");
					streamWriter.WriteLine(".");
				}
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Control client: " + ex.Message));
		}
	}
}