using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Serialization; using Barotrauma; using Barotrauma.Items.Components; using Barotrauma.LuaCs.Data; using Barotrauma.Networking; using Microsoft.CodeAnalysis; using Microsoft.Xna.Framework; using OneOf; using Platform = Barotrauma.LuaCs.Data.Platform; // ReSharper disable ConvertClosureToMethodGroup // This file is cursed, we put everything in it, and I'm not sorry about it. namespace Barotrauma { public static class ModUtils { public static class ItemPrefab { internal static Barotrauma.ItemPrefab GetItemPrefab(string itemNameOrId) { Barotrauma.ItemPrefab itemPrefab = (Barotrauma.MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? Barotrauma.MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as Barotrauma.ItemPrefab; return itemPrefab; } } public static class Client { internal static ulong GetSteamId(Barotrauma.Networking.Client client) { if (client.AccountId.TryUnwrap(out AccountId outValue) && outValue is SteamId steamId) { return steamId.Value; } else { return 0; } } #if SERVER internal static void UnbanPlayer(string playerName) { GameMain.Server.UnbanPlayer(playerName); } internal static void BanPlayer(string player, string reason, bool range = false, float seconds = -1) { if (seconds == -1) { GameMain.Server.BanPlayer(player, reason, null); } else { GameMain.Server.BanPlayer(player, reason, TimeSpan.FromSeconds(seconds)); } } #endif internal static IReadOnlyList ClientList { get { if (GameMain.IsSingleplayer) { return new List(); } #if SERVER return GameMain.Server.ConnectedClients; #else return GameMain.Client.ConnectedClients; #endif } } } public static class Definitions { public const string LuaCsForBarotrauma = nameof(LuaCsForBarotrauma); } public static class Environment { internal static void SetCurrentThreadAsMain() => MainThreadId = Thread.CurrentThread.ManagedThreadId; public static int MainThreadId { get; private set; } = Int32.MinValue; public static bool IsMainThread { get { if (MainThreadId == Int32.MinValue) throw new ArgumentNullException("MainThread ID not set."); return Thread.CurrentThread.ManagedThreadId == MainThreadId; } } public static readonly Platform CurrentPlatform = #if WINDOWS Platform.Windows; #elif MACOS Platform.MacOS; #elif LINUX Platform.Linux; #else Platform.Linux; #endif public static readonly Target CurrentTarget = #if CLIENT Target.Client; #elif SERVER Target.Server; #else Target.Server; #endif } #region LOGGING public static class Logging { public static void PrintMessage(string s) { #if SERVER LuaCsSetup.Instance.Logger.LogMessage($"{s}"); #else LuaCsSetup.Instance.Logger.LogMessage($"{s}"); #endif } public static void PrintWarning(string s) { #if SERVER LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow); #else LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow); #endif } public static void PrintError(string s) { #if SERVER LuaCsSetup.Instance.Logger.LogError($"{s}"); #else LuaCsSetup.Instance.Logger.LogError($"{s}"); #endif } } #endregion #region FILE_IO // ReSharper disable once InconsistentNaming public static class IO { public static IEnumerable FindAllFilesInDirectory(string folder, string pattern, SearchOption option) { try { return Directory.GetFiles(folder, pattern, option); } catch (DirectoryNotFoundException e) { return new string[] { }; } } public static string PrepareFilePathString(string filePath) => PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath)); public static string PrepareFilePathString(string path, string fileName) => Path.Combine(SanitizePath(path), SanitizeFileName(fileName)); public static string SanitizeFileName(string fileName) { foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform()) fileName = fileName.Replace(c, '_'); return fileName; } /// /// Gets the sanitized path for the top-level directory for a given content package. /// /// /// public static string GetContentPackageDir(ContentPackage package) { return SanitizePath(Path.GetFullPath(package.Dir)); } public static string SanitizePath(string path) { foreach (char c in Path.GetInvalidPathChars()) path = path.Replace(c.ToString(), "_"); return path.CleanUpPath(); } public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText, Func fileDataFactory = null, bool createFile = true) { fileText = null; string fp = Path.GetFullPath(SanitizePath(filePath)); IOActionResultState ioActionResultState = IOActionResultState.Success; if (createFile) { ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory); } else if (!File.Exists(fp)) { return IOActionResultState.FileNotFound; } if (ioActionResultState == IOActionResultState.Success) { try { fileText = File.ReadAllText(fp!); return IOActionResultState.Success; } catch (ArgumentNullException ane) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); return IOActionResultState.FilePathNull; } catch (ArgumentException ae) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); return IOActionResultState.FilePathInvalid; } catch (DirectoryNotFoundException dnfe) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); return IOActionResultState.DirectoryMissing; } catch (PathTooLongException ptle) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); return IOActionResultState.PathTooLong; } catch (NotSupportedException nse) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); return IOActionResultState.InvalidOperation; } catch (IOException ioe) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); return IOActionResultState.IOFailure; } catch (Exception e) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); return IOActionResultState.UnknownError; } } return ioActionResultState; } public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath, Func fileDataFactory = null) { string file = Path.GetFileName(filePath); string path = Path.GetDirectoryName(filePath)!; formattedFilePath = IO.PrepareFilePathString(path, file); try { if (!Directory.Exists(path)) Directory.CreateDirectory(path); if (!File.Exists(formattedFilePath)) File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke()); return IOActionResultState.Success; } catch (ArgumentNullException ane) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}"); return IOActionResultState.FilePathNull; } catch (ArgumentException ae) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}"); return IOActionResultState.FilePathInvalid; } catch (DirectoryNotFoundException dnfe) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}"); return IOActionResultState.DirectoryMissing; } catch (PathTooLongException ptle) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}"); return IOActionResultState.PathTooLong; } catch (NotSupportedException nse) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}"); return IOActionResultState.InvalidOperation; } catch (IOException ioe) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}"); return IOActionResultState.IOFailure; } catch (Exception e) { ModUtils.Logging.PrintError( $"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}"); return IOActionResultState.UnknownError; } } public static IOActionResultState WriteFileText(string filePath, string fileText) { IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp); if (ioActionResultState == IOActionResultState.Success) { try { File.WriteAllText(fp!, fileText); return IOActionResultState.Success; } catch (ArgumentNullException ane) { ModUtils.Logging.PrintError( $"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); return IOActionResultState.FilePathNull; } catch (ArgumentException ae) { ModUtils.Logging.PrintError( $"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); return IOActionResultState.FilePathInvalid; } catch (DirectoryNotFoundException dnfe) { ModUtils.Logging.PrintError( $"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); return IOActionResultState.DirectoryMissing; } catch (PathTooLongException ptle) { ModUtils.Logging.PrintError( $"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); return IOActionResultState.PathTooLong; } catch (NotSupportedException nse) { ModUtils.Logging.PrintError( $"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); return IOActionResultState.InvalidOperation; } catch (IOException ioe) { ModUtils.Logging.PrintError( $"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); return IOActionResultState.IOFailure; } catch (Exception e) { ModUtils.Logging.PrintError( $"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); return IOActionResultState.UnknownError; } } return ioActionResultState; } /// /// /// /// /// /// /// /// /// public static bool LoadOrCreateTypeXml(out T instance, string filepath, Func typeFactory = null, bool createFile = true) where T : class, new() { instance = null; filepath = filepath.CleanUpPath(); if (IOActionResultState.Success == GetOrCreateFileText( filepath, out string fileText, typeFactory is not null ? () => { using StringWriter sw = new StringWriter(); T t = typeFactory?.Invoke(); if (t is not null) { XmlSerializer s = new XmlSerializer(typeof(T)); s.Serialize(sw, t); return sw.ToString(); } return ""; } : null, createFile)) { XmlSerializer s = new XmlSerializer(typeof(T)); try { using TextReader tr = new StringReader(fileText); instance = (T)s.Deserialize(tr); return true; } catch (InvalidOperationException ioe) { ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}."); #if DEBUG ModUtils.Logging.PrintError( $"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}"); #endif instance = null; return false; } } return false; } public enum IOActionResultState { Success, FileNotFound, FilePathNull, FilePathInvalid, DirectoryMissing, PathTooLong, InvalidOperation, IOFailure, UnknownError } } #endregion #region GAME public static class Game { /// /// Returns whether or not there is a round running. /// /// public static bool IsRoundInProgress() { #if CLIENT if (Screen.Selected is not null && Screen.Selected.IsEditor) return false; #endif return GameMain.GameSession is not null && Level.Loaded is not null; } } #endregion #region THREADING public static class Threading { /// /// Gets the boolean value of an integer with thread-safety via Interlocked. /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool GetBool(ref int var) => Interlocked.CompareExchange(ref var, 1, 1) > 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetBool(ref int var, bool value) { if (value) { Interlocked.CompareExchange(ref var, 1, 0); } else { Interlocked.CompareExchange(ref var, 0, 1); } } /// /// Gets if the integer is under 1 (is zero/false) and, if so, sets the value to one/true. /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CheckIfClearAndSetBool(ref int var) { return Interlocked.CompareExchange(ref var, 1, 0) < 1; } /// /// Gets if the integer is over 0 (is one/true) and, if so, sets the value to zero/false. /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CheckIfSetAndClearBool(ref int var) { return Interlocked.CompareExchange(ref var, 0, 1) > 0; } } #endregion #region UTILITIES_CORE public static V TryGetOrSet(this IDictionary dict, K key, Func valueFactory) where K : IEquatable { if (dict.TryGetValue(key, out var dictValue)) return dictValue; if (valueFactory is not null) dict.Add(key, valueFactory()); else return default; return dict[key]; } #endregion } public static class AssemblyExtensions { /// /// Gets all types in the given assembly. Handles invalid type scenarios. /// /// The assembly to scan /// An enumerable collection of types. public static IEnumerable GetSafeTypes(this Assembly assembly) { // Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67 try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException re) { try { return re.Types.Where(x => x != null)!; } catch (InvalidOperationException) { return new List(); } } catch (Exception) { return new List(); } } } public static class CollectionExtensions { /// /// Executes a series of asynchronous tasks with limited parallelism to maintain execution efficiency. /// /// /// /// /// /// public static Task ParallelForEachAsync(this IEnumerable source, Func funcBody, int maxDegreeOfParallelism = 4) { async Task AwaitParallelLimit(IEnumerator partition) { using (partition) { while (partition.MoveNext()) { await Task.Yield(); // prevents a sync/hot thread hangup await funcBody(partition.Current); } } } return Task.WhenAll( Partitioner .Create(source) .GetPartitions(maxDegreeOfParallelism) .AsParallel() .Select(p => AwaitParallelLimit(p))); } } } #region ExceptionData namespace FluentResults.LuaCs { public static class MetadataType { public static string ExceptionDetails = nameof(ExceptionDetails); /// /// The object that threw the exception. /// public static string ExceptionObject = nameof(ExceptionObject); /// /// The parameter-object responsible for the exception thrown (not the exception thrower). /// public static string RootObject = nameof(RootObject); /// /// Additional exception sources. /// public static string Sources = nameof(Sources); public static string StackTrace = nameof(StackTrace); } } #endregion