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));
}
}
}