Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of CustomLoadingScreens v1.1.1
CustomLoadingScreens.dll
Decompiled 14 hours agousing System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using CustomLoadingScreens; using CustomLoadingScreens.Config; using CustomLoadingScreens.Integrations; using CustomLoadingScreens.Integrations.Interop; using CustomLoadingScreens.LoadingScreen; using CustomLoadingScreens.Utils; using HarmonyLib; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using S1API.Logging; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: MelonInfo(typeof(Core), "Custom Loading Screens", "1.1.0", "Riccaforte", null)] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: MelonAuthorColor(1, 68, 2, 152)] [assembly: MelonColor(1, 68, 2, 152)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("CustomLoadingScreens")] [assembly: AssemblyConfiguration("CrossCompat")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+505a2426662e697aa09e6181e7830337d1bc9e02")] [assembly: AssemblyProduct("CustomLoadingScreens")] [assembly: AssemblyTitle("CustomLoadingScreens")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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; } } } namespace CustomLoadingScreens { public class Core : MelonMod { private static ModPreferences? _preferences; private PreferencesFileWatcher? _preferencesWatcher; private ImageFolderWatcher? _imageFolderWatcher; private LoadingScreenBackgroundController? _backgroundController; public readonly Log Logger = new Log("CustomLoadingScreens"); public static Core? Instance { get; private set; } internal static ModPreferences? Preferences => _preferences; public override void OnInitializeMelon() { Instance = this; _preferences = new ModPreferences(); _backgroundController = new LoadingScreenBackgroundController(Logger); _preferencesWatcher = new PreferencesFileWatcher(Logger); RecreateImageWatcher(); HarmonyPatches.SetModInstance(this); RuntimeReflection.Initialize(); LoadSpritesOnce(); Logger.Msg("Monitoring custom loading images under '" + _preferences.ResolvedBaseFolder + "'."); } private void LoadSpritesOnce() { _backgroundController?.MarkSpriteCacheDirty(); if (_preferences != null) { _backgroundController?.LoadSpritesOnce(_preferences); } } public override void OnUpdate() { PreferencesFileWatcher? preferencesWatcher = _preferencesWatcher; if (preferencesWatcher != null && preferencesWatcher.TryConsumeScheduledReload()) { ReloadPreferencesFromDisk(); } ImageFolderWatcher? imageFolderWatcher = _imageFolderWatcher; if (imageFolderWatcher != null && imageFolderWatcher.TryConsumeScheduledReload()) { _backgroundController?.MarkSpriteCacheDirty(); } } public override void OnApplicationQuit() { _imageFolderWatcher?.Dispose(); _imageFolderWatcher = null; _preferencesWatcher?.Dispose(); _preferencesWatcher = null; _backgroundController?.Dispose(); _backgroundController = null; _preferences = null; Instance = null; } internal void ApplyLoadingScreenOverrides(object loadingScreen) { if (_preferences != null && _backgroundController != null) { _backgroundController.ApplyToLoadingScreen(loadingScreen, _preferences); } } private void ReloadPreferencesFromDisk() { string text = _preferencesWatcher?.FilePath; if (string.IsNullOrWhiteSpace(text)) { return; } if (!PreferencesFileParser.TryReadSnapshot(text, out PreferencesSnapshot snapshot)) { _preferencesWatcher?.ScheduleReload(); return; } ModPreferences? preferences = _preferences; if (preferences != null && preferences.ApplySnapshot(snapshot)) { _backgroundController?.MarkSpriteCacheDirty(); RecreateImageWatcher(); Logger.Msg("Reloaded preferences from MelonPreferences.cfg. Using image folder '" + _preferences.ResolvedBaseFolder + "'."); } } private void RecreateImageWatcher() { _imageFolderWatcher?.Dispose(); _imageFolderWatcher = null; ModPreferences? preferences = _preferences; if (preferences != null && preferences.WatchFolders) { _imageFolderWatcher = new ImageFolderWatcher(_preferences.ResolvedBaseFolder, Logger); } } } } namespace CustomLoadingScreens.Utils { public static class Constants { public static class PreferenceKeys { public const string ENABLED = "enabled"; public const string BASE_FOLDER = "baseFolder"; public const string USE_NORMAL_IMAGES_FOR_TUTORIAL = "useNormalImagesForTutorial"; public const string WATCH_FOLDERS = "watchFolders"; public const string LOG_MISSING_IMAGES_ONCE = "logMissingImagesOnce"; } public static class Defaults { public const bool ENABLED = true; public const bool USE_NORMAL_IMAGES_FOR_TUTORIAL = true; public const bool WATCH_FOLDERS = true; public const bool LOG_MISSING_IMAGES_ONCE = true; public const string BACKGROUND_IMAGES_FOLDER_NAME = "BackgroundImages"; public const string TUTORIAL_BACKGROUND_IMAGES_FOLDER_NAME = "TutorialBackgroundImages"; } public static class Game { public const string GAME_STUDIO = "TVGS"; public const string GAME_NAME = "Schedule I"; } public const string MOD_NAME = "Custom Loading Screens"; public const string MOD_FOLDER_NAME = "CustomLoadingScreens"; public const string MOD_VERSION = "1.1.0"; public const string MOD_AUTHOR = "Riccaforte"; public const string PREFERENCES_CATEGORY = "Custom Loading Screens"; } internal static class RuntimeReflection { private static Func<Texture2D, byte[], bool>? _loadImageInvoker; public static void Initialize() { if (_loadImageInvoker != null) { return; } Type[] array = FindLoadedTypes("UnityEngine.ImageConversion").ToArray(); if (array.Length == 0) { throw new InvalidOperationException("UnityEngine.ImageConversion type was not found in the loaded runtime assemblies."); } bool flag = IsIl2CppRuntime(); MethodInfo methodInfo = (flag ? FindRuntimeLoadImageMethod(array, IsIl2CppByteArrayType) : FindRuntimeLoadImageMethod(array, (Type parameterType) => parameterType == typeof(byte[]))); if (methodInfo == null) { methodInfo = (flag ? FindRuntimeLoadImageMethod(array, (Type parameterType) => parameterType == typeof(byte[])) : FindRuntimeLoadImageMethod(array, IsIl2CppByteArrayType)); } if (methodInfo == null) { throw new InvalidOperationException("UnityEngine.ImageConversion.LoadImage did not expose a supported overload across the loaded runtime assemblies. Assemblies searched: " + string.Join(", ", array.Select((Type type) => type.Assembly.GetName().Name).Distinct<string>(StringComparer.Ordinal)) + "."); } bool flag2 = methodInfo != null && methodInfo.GetParameters().Length >= 2 && IsIl2CppByteArrayType(methodInfo.GetParameters()[1].ParameterType); _loadImageInvoker = ((flag && flag2) ? CreateIl2CppByteArrayInvoker(methodInfo) : CreateManagedByteArrayInvoker(methodInfo)); } public static bool TryLoadImage(Texture2D texture, byte[] imageBytes) { return _loadImageInvoker != null && _loadImageInvoker(texture, imageBytes); } private static bool IsIl2CppByteArrayType(Type parameterType) { return parameterType.IsGenericType && string.Equals(parameterType.GetGenericTypeDefinition().FullName, "Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray`1", StringComparison.Ordinal) && parameterType.GenericTypeArguments.Length == 1 && parameterType.GenericTypeArguments[0] == typeof(byte); } private static Func<Texture2D, byte[], bool> CreateManagedByteArrayInvoker(MethodInfo managedOverload) { ParameterInfo[] parameters = managedOverload.GetParameters(); return delegate(Texture2D texture, byte[] imageBytes) { object obj = ((parameters.Length == 2) ? managedOverload.Invoke(null, new object[2] { texture, imageBytes }) : managedOverload.Invoke(null, new object[3] { texture, imageBytes, false })); return obj != null && Convert.ToBoolean(obj); }; } private static Func<Texture2D, byte[], bool> CreateIl2CppByteArrayInvoker(MethodInfo il2cppOverload) { ParameterInfo[] parameters = il2cppOverload.GetParameters(); return delegate(Texture2D texture, byte[] imageBytes) { Type type = FindIl2CppByteArrayType(); if (type == null) { return false; } ConstructorInfo constructor = type.GetConstructor(new Type[1] { typeof(byte[]) }); if (constructor == null) { return false; } object obj = constructor.Invoke(new object[1] { imageBytes }); object obj2 = ((parameters.Length != 2) ? il2cppOverload.Invoke(null, new object[3] { texture, obj, false }) : il2cppOverload.Invoke(null, new object[2] { texture, obj })); return obj2 != null && Convert.ToBoolean(obj2); }; } private static Type? FindIl2CppByteArrayType() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (!string.Equals(assembly.GetName().Name, "Il2CppInterop.Runtime", StringComparison.Ordinal)) { continue; } try { Type type = assembly.GetType("Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray`1"); if (type != null) { try { return type.MakeGenericType(typeof(byte)); } catch { } } } catch { } foreach (Type loadableType in GetLoadableTypes(assembly)) { if (string.Equals(loadableType.FullName, "Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray`1", StringComparison.Ordinal) && loadableType.GenericTypeArguments.Length == 1 && loadableType.GenericTypeArguments[0] == typeof(byte)) { return loadableType; } } } return null; } private static MethodInfo? FindRuntimeLoadImageMethod(IEnumerable<Type> imageConversionTypes, Func<Type, bool> matchesSecondParameterType) { foreach (Type imageConversionType in imageConversionTypes) { MethodInfo[] methods = imageConversionType.GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo in methods) { if (string.Equals(methodInfo.Name, "LoadImage", StringComparison.Ordinal) && !(methodInfo.ReturnType != typeof(bool))) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length >= 2 && parameters.Length <= 3 && !(parameters[0].ParameterType != typeof(Texture2D)) && matchesSecondParameterType(parameters[1].ParameterType) && (parameters.Length == 2 || parameters[2].ParameterType == typeof(bool))) { return methodInfo; } } } } return null; } private static bool IsIl2CppRuntime() { return AppDomain.CurrentDomain.GetAssemblies().Any((Assembly assembly) => string.Equals(assembly.GetName().Name, "Il2CppInterop.Runtime", StringComparison.Ordinal)); } private static IEnumerable<Type> FindLoadedTypes(string fullTypeName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type exactType = assembly.GetType(fullTypeName, throwOnError: false, ignoreCase: false); if (exactType != null) { yield return exactType; continue; } foreach (Type candidateType in GetLoadableTypes(assembly)) { if (string.Equals(candidateType.FullName, fullTypeName, StringComparison.Ordinal)) { yield return candidateType; break; } } } } private static IEnumerable<Type> GetLoadableTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.OfType<Type>(); } } } internal static class SpriteLoader { private static readonly Log _logger = new Log("SpriteLoader"); public static Sprite? TryCreateSprite(string filePath) { try { byte[] imageBytes = File.ReadAllBytes(filePath); Texture2D val = CreateTexture(imageBytes, Path.GetFileNameWithoutExtension(filePath)); if (!RuntimeReflection.TryLoadImage(val, imageBytes)) { Object.Destroy((Object)(object)val); _logger.Warning("Skipping custom loading image '" + filePath + "' because Unity could not decode it."); return null; } return CreateSpriteFromTexture(val); } catch (IOException ex) { _logger.Warning("Skipping custom loading image '" + filePath + "' because it could not be read: " + ex.Message); return null; } catch (UnauthorizedAccessException ex2) { _logger.Warning("Skipping custom loading image '" + filePath + "' because it could not be read: " + ex2.Message); return null; } catch (Exception ex3) { _logger.Warning("Skipping custom loading image '" + filePath + "' because it failed to load: " + ex3.Message); return null; } } private static Texture2D CreateTexture(byte[] imageBytes, string name) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false); ((Object)val).name = name; ((Texture)val).wrapMode = (TextureWrapMode)1; ((Texture)val).filterMode = (FilterMode)1; return val; } private static Sprite CreateSpriteFromTexture(Texture2D texture) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) Sprite val = Sprite.Create(texture, new Rect(0f, 0f, (float)((Texture)texture).width, (float)((Texture)texture).height), new Vector2(0.5f, 0.5f), 100f); ((Object)val).name = ((Object)texture).name; return val; } public static Sprite[] LoadSprites(string[] filePaths) { if (filePaths.Length == 0) { return Array.Empty<Sprite>(); } List<Sprite> list = new List<Sprite>(filePaths.Length); foreach (string filePath in filePaths) { Sprite val = TryCreateSprite(filePath); if ((Object)(object)val != (Object)null) { list.Add(val); } } return list.ToArray(); } } } namespace CustomLoadingScreens.LoadingScreen { internal static class ImageCatalog { private static readonly string[] SupportedExtensions = new string[3] { ".png", ".jpg", ".jpeg" }; public static string GetBackgroundFolderPath(string baseFolder) { return Path.Combine(baseFolder, "BackgroundImages"); } public static string GetTutorialBackgroundFolderPath(string baseFolder) { return Path.Combine(baseFolder, "TutorialBackgroundImages"); } public static string[] GetImageFiles(string folderPath) { if (!Directory.Exists(folderPath)) { return Array.Empty<string>(); } return Directory.GetFiles(folderPath).Where(IsSupportedImageFile).OrderBy<string, string>(Path.GetFileName, StringComparer.OrdinalIgnoreCase) .ToArray(); } public static bool IsSupportedImageFile(string path) { string extension = Path.GetExtension(path); return SupportedExtensions.Contains<string>(extension, StringComparer.OrdinalIgnoreCase); } } internal sealed class LoadingScreenBackgroundController : IDisposable { private readonly Log _logger; private readonly SpriteCache _spriteCache; private object? _capturedLoadingScreenInstance; private object? _originalBackgroundImages; private object? _originalTutorialBackgroundImages; public LoadingScreenBackgroundController(Log logger) { _logger = logger; _spriteCache = new SpriteCache(logger); } public void MarkSpriteCacheDirty() { _spriteCache.MarkDirty(); } public void LoadSpritesOnce(ModPreferences preferences) { _spriteCache.EnsureLoaded(preferences); } public void ApplyToLoadingScreen(object loadingScreen, ModPreferences preferences) { CaptureOriginalArrays(loadingScreen); if (!preferences.IsEnabled) { RestoreOriginalArrays(loadingScreen); return; } _spriteCache.EnsureLoaded(preferences); ApplySpritesToLoadingScreen(loadingScreen, "BackgroundImages", _spriteCache.NormalSprites, _originalBackgroundImages); ApplySpritesToLoadingScreen(loadingScreen, "TutorialBackgroundImages", _spriteCache.TutorialSprites, _originalTutorialBackgroundImages); } public void Dispose() { _spriteCache.Dispose(); } private void CaptureOriginalArrays(object loadingScreen) { if (_capturedLoadingScreenInstance != loadingScreen) { _capturedLoadingScreenInstance = loadingScreen; _originalBackgroundImages = RuntimeInterop.GetMemberValue(loadingScreen, "BackgroundImages"); _originalTutorialBackgroundImages = RuntimeInterop.GetMemberValue(loadingScreen, "TutorialBackgroundImages"); if (_originalBackgroundImages == null || _originalTutorialBackgroundImages == null) { _logger.Warning("Could not capture one or more original loading-screen image arrays. Fallback restoration may be limited."); } } } private static void ApplySpritesToLoadingScreen(object loadingScreen, string memberName, Sprite[] customSprites, object? originalValue) { if (customSprites.Length != 0) { try { RuntimeInterop.SetSpriteArrayMemberValue(loadingScreen, memberName, customSprites); return; } catch (InvalidOperationException) { return; } } if (originalValue != null) { RuntimeInterop.SetMemberValue(loadingScreen, memberName, originalValue); } } private void RestoreOriginalArrays(object loadingScreen) { if (_originalBackgroundImages != null) { RuntimeInterop.SetMemberValue(loadingScreen, "BackgroundImages", _originalBackgroundImages); } if (_originalTutorialBackgroundImages != null) { RuntimeInterop.SetMemberValue(loadingScreen, "TutorialBackgroundImages", _originalTutorialBackgroundImages); } } } internal sealed class SpriteCache : IDisposable { private readonly Log _logger; private bool _isDirty = true; private string? _lastBaseFolder; private bool _lastUseNormalImagesForTutorial; public Sprite[] NormalSprites { get; private set; } = Array.Empty<Sprite>(); public Sprite[] TutorialSprites { get; private set; } = Array.Empty<Sprite>(); public SpriteCache(Log logger) { _logger = logger; } public void MarkDirty() { _isDirty = true; } public void EnsureLoaded(ModPreferences preferences) { string resolvedBaseFolder = preferences.ResolvedBaseFolder; if (_isDirty || !string.Equals(_lastBaseFolder, resolvedBaseFolder, StringComparison.Ordinal) || _lastUseNormalImagesForTutorial != preferences.UseNormalImagesForTutorial) { Rebuild(preferences, resolvedBaseFolder); } } public void Dispose() { ClearOwnedObjects(); } private void Rebuild(ModPreferences preferences, string baseFolder) { ClearOwnedObjects(); _lastBaseFolder = baseFolder; _lastUseNormalImagesForTutorial = preferences.UseNormalImagesForTutorial; _isDirty = false; string backgroundFolderPath = ImageCatalog.GetBackgroundFolderPath(baseFolder); string tutorialBackgroundFolderPath = ImageCatalog.GetTutorialBackgroundFolderPath(baseFolder); Directory.CreateDirectory(backgroundFolderPath); Directory.CreateDirectory(tutorialBackgroundFolderPath); NormalSprites = SpriteLoader.LoadSprites(ImageCatalog.GetImageFiles(backgroundFolderPath)); Sprite[] array = SpriteLoader.LoadSprites(ImageCatalog.GetImageFiles(tutorialBackgroundFolderPath)); TutorialSprites = (Sprite[])((array.Length != 0) ? array : (preferences.UseNormalImagesForTutorial ? ((Array)NormalSprites) : ((Array)Array.Empty<Sprite>()))); if (NormalSprites.Length == 0 && TutorialSprites.Length == 0) { _logger.Msg("No custom loading images found. Add images to '" + backgroundFolderPath + "' and '" + tutorialBackgroundFolderPath + "'."); } else { _logger.Msg($"Loaded {NormalSprites.Length} normal loading images and {array.Length} tutorial loading images."); } } private void ClearOwnedObjects() { Sprite[] normalSprites = NormalSprites; foreach (Sprite val in normalSprites) { if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } } NormalSprites = Array.Empty<Sprite>(); TutorialSprites = Array.Empty<Sprite>(); } } } namespace CustomLoadingScreens.Integrations { public static class HarmonyPatches { [HarmonyPatch] private static class LoadingScreenOpenPatch { [HarmonyPrepare] private static bool Prepare() { return CanPatch(GameTypeNames.LoadingScreen, "Open", typeof(bool)); } [HarmonyTargetMethod] private static MethodBase TargetMethod() { return GetRequiredPatchMethod(GameTypeNames.LoadingScreen, "Open", typeof(bool)); } [HarmonyPrefix] private static void Prefix(object __instance, bool __0) { if (_modInstance == null) { return; } try { _modInstance.ApplyLoadingScreenOverrides(__instance); } catch (Exception arg) { _modInstance.Logger.Error($"Failed to apply custom loading screen images (tutorial={__0}): {arg}"); } } } private static Core? _modInstance; public static void SetModInstance(Core modInstance) { _modInstance = modInstance; } private static bool CanPatch(string[] typeNames, string methodName, params Type[] argumentTypes) { Type type = RuntimeInterop.ResolveGameType(typeNames); return type != null && AccessTools.Method(type, methodName, argumentTypes, (Type[])null) != null; } private static MethodBase GetRequiredPatchMethod(string[] typeNames, string methodName, params Type[] argumentTypes) { Type type = RuntimeInterop.ResolveGameType(typeNames); MethodBase methodBase = ((type == null) ? null : AccessTools.Method(type, methodName, argumentTypes, (Type[])null)); if (methodBase != null) { return methodBase; } throw new MissingMethodException("Could not resolve Harmony patch target '" + methodName + "'."); } } } namespace CustomLoadingScreens.Integrations.Interop { internal static class GameTypeNames { public static readonly string[] LoadingScreen = new string[2] { "ScheduleOne.UI.LoadingScreen", "Il2CppScheduleOne.UI.LoadingScreen" }; } internal static class RuntimeInterop { private const BindingFlags MemberLookupFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static readonly ConcurrentDictionary<(Type Type, string MemberName), MemberInfo?> MemberCache = new ConcurrentDictionary<(Type, string), MemberInfo>(); private static readonly ConcurrentDictionary<string, Type?> TypeCache = new ConcurrentDictionary<string, Type>(StringComparer.Ordinal); public static IReadOnlyList<object?> MaterializeObjects(object? value) { if (value == null) { return Array.Empty<object>(); } if (value is IEnumerable enumerable) { List<object> list = new List<object>(); foreach (object item in enumerable) { list.Add(item); } return list; } object memberValue = GetMemberValue(value, "Count"); if (memberValue == null) { return Array.Empty<object>(); } int num = Convert.ToInt32(memberValue); PropertyInfo property = value.GetType().GetProperty("Item", new Type[1] { typeof(int) }); if (property == null) { return Array.Empty<object>(); } List<object> list2 = new List<object>(num); for (int i = 0; i < num; i++) { list2.Add(property.GetValue(value, new object[1] { i })); } return list2; } public static object? GetMemberValue(object? instance, string memberName) { if (instance == null) { return null; } MemberInfo memberInfo = ResolveMember(instance.GetType(), memberName); if (memberInfo is PropertyInfo propertyInfo) { return propertyInfo.GetValue(instance); } return (memberInfo is FieldInfo fieldInfo) ? fieldInfo.GetValue(instance) : null; } public static void SetMemberValue(object instance, string memberName, object? value) { MemberInfo memberInfo = ResolveMember(instance.GetType(), memberName); if (memberInfo is PropertyInfo { CanWrite: not false } propertyInfo) { propertyInfo.SetValue(instance, value); } else if (memberInfo is FieldInfo fieldInfo) { fieldInfo.SetValue(instance, value); } } public static void SetSpriteArrayMemberValue(object instance, string memberName, Sprite[] sprites) { MemberInfo member = ResolveMember(instance.GetType(), memberName); Type memberType = GetMemberType(member); if (!(memberType == null)) { object value = CreateCompatibleSpriteArrayValue(memberType, sprites); SetMemberValue(instance, memberName, value); } } public static Type? ResolveGameType(IEnumerable<string> typeNames) { foreach (string typeName in typeNames) { Type type = ResolveLoadedType(typeName); if (type != null) { return type; } } return null; } private static MemberInfo? ResolveMember(Type type, string memberName) { return MemberCache.GetOrAdd((type, memberName), ((Type Type, string MemberName) key) => FindMember(key.Type, key.MemberName)); } private static MemberInfo? FindMember(Type type, string memberName) { Type type2 = type; while (type2 != null) { PropertyInfo property = type2.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property; } FieldInfo field = type2.GetField(memberName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field; } type2 = type2.BaseType; } return null; } private static Type? GetMemberType(MemberInfo? member) { if (member is PropertyInfo propertyInfo) { return propertyInfo.PropertyType; } return (member is FieldInfo fieldInfo) ? fieldInfo.FieldType : null; } private static object CreateCompatibleSpriteArrayValue(Type memberType, Sprite[] sprites) { if (memberType == typeof(Sprite[])) { return sprites; } if (memberType.IsArray && memberType.GetElementType() == typeof(Sprite)) { return sprites; } string? fullName = memberType.FullName; if (fullName != null && fullName.StartsWith("Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppReferenceArray`1", StringComparison.Ordinal)) { ConstructorInfo constructor = memberType.GetConstructor(new Type[1] { typeof(Sprite[]) }); if (constructor != null) { return constructor.Invoke(new object[1] { sprites }); } } throw new InvalidOperationException("Unsupported sprite array target type '" + memberType.FullName + "'."); } private static Type? ResolveLoadedType(string typeName) { return TypeCache.GetOrAdd(typeName, (string name) => FindLoadedType(name)); } private static Type? FindLoadedType(string typeName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false); if (type != null) { return type; } } return null; } } } namespace CustomLoadingScreens.Config { internal sealed class ImageFolderWatcher : IDisposable { private static readonly TimeSpan ImageReloadDebounce = TimeSpan.FromMilliseconds(250.0); private readonly object _reloadLock = new object(); private readonly string _baseFolderPath; private readonly string _normalizedBaseFolderPath; private FileSystemWatcher? _watcher; private DateTime? _reloadDueUtc; public ImageFolderWatcher(string baseFolderPath, Log logger) { _baseFolderPath = baseFolderPath; _normalizedBaseFolderPath = NormalizePath(baseFolderPath); Initialize(logger); } public void ScheduleReload() { lock (_reloadLock) { _reloadDueUtc = DateTime.UtcNow + ImageReloadDebounce; } } public bool TryConsumeScheduledReload() { lock (_reloadLock) { DateTime? reloadDueUtc = _reloadDueUtc; if (!reloadDueUtc.HasValue || DateTime.UtcNow < _reloadDueUtc.Value) { return false; } _reloadDueUtc = null; return true; } } public void Dispose() { if (_watcher != null) { _watcher.EnableRaisingEvents = false; _watcher.Changed -= OnFolderChanged; _watcher.Created -= OnFolderChanged; _watcher.Deleted -= OnFolderChanged; _watcher.Renamed -= OnFolderRenamed; _watcher.Dispose(); _watcher = null; } } private void Initialize(Log logger) { string text = (Directory.Exists(_baseFolderPath) ? _baseFolderPath : Path.GetDirectoryName(_baseFolderPath)); if (string.IsNullOrWhiteSpace(text) || !Directory.Exists(text)) { logger.Warning("Custom image directory watcher could not start because '" + text + "' does not exist."); return; } _watcher = new FileSystemWatcher(text) { Filter = "*.*", NotifyFilter = (NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.CreationTime), IncludeSubdirectories = true, EnableRaisingEvents = true }; _watcher.Changed += OnFolderChanged; _watcher.Created += OnFolderChanged; _watcher.Deleted += OnFolderChanged; _watcher.Renamed += OnFolderRenamed; } private void OnFolderChanged(object sender, FileSystemEventArgs e) { if (ShouldWatchPath(e.FullPath)) { ScheduleReload(); } } private void OnFolderRenamed(object sender, RenamedEventArgs e) { if (ShouldWatchPath(e.FullPath) || ShouldWatchPath(e.OldFullPath)) { ScheduleReload(); } } private bool ShouldWatchPath(string path) { string text = NormalizePath(path); if (text.StartsWith(_normalizedBaseFolderPath, StringComparison.OrdinalIgnoreCase)) { return true; } return ImageCatalog.IsSupportedImageFile(path); } private static string NormalizePath(string path) { return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } } internal sealed class ModPreferences { private readonly MelonPreferences_Entry<bool> _enabledPreference; private readonly MelonPreferences_Entry<string> _baseFolderPreference; private readonly MelonPreferences_Entry<bool> _useNormalImagesForTutorialPreference; private readonly MelonPreferences_Entry<bool> _watchFoldersPreference; private readonly MelonPreferences_Entry<bool> _logMissingImagesOncePreference; public bool IsEnabled => _enabledPreference.Value; public string BaseFolder => _baseFolderPreference.Value?.Trim() ?? string.Empty; public bool UseNormalImagesForTutorial => _useNormalImagesForTutorialPreference.Value; public bool WatchFolders => _watchFoldersPreference.Value; public bool LogMissingImagesOnce => _logMissingImagesOncePreference.Value; public string ResolvedBaseFolder { get { if (!string.IsNullOrWhiteSpace(BaseFolder)) { return BaseFolder; } string userDataDirectory = MelonEnvironment.UserDataDirectory; if (!string.IsNullOrWhiteSpace(userDataDirectory)) { return Path.Combine(userDataDirectory, "CustomLoadingScreens"); } return Path.Combine(AppContext.BaseDirectory, "CustomLoadingScreens"); } } public ModPreferences() { MelonPreferences_Category val = MelonPreferences.CreateCategory("Custom Loading Screens"); _enabledPreference = val.CreateEntry<bool>("enabled", true, "Whether the mod is enabled.", (string)null, false, false, (ValueValidator)null, (string)null); _baseFolderPreference = val.CreateEntry<string>("baseFolder", string.Empty, "Optional override for the custom loading image base folder.", (string)null, false, false, (ValueValidator)null, (string)null); _useNormalImagesForTutorialPreference = val.CreateEntry<bool>("useNormalImagesForTutorial", true, "Whether tutorial loading screens should reuse normal images when no tutorial images are present.", (string)null, false, false, (ValueValidator)null, (string)null); _watchFoldersPreference = val.CreateEntry<bool>("watchFolders", true, "Whether the mod should watch the custom image folder for changes.", (string)null, false, false, (ValueValidator)null, (string)null); _logMissingImagesOncePreference = val.CreateEntry<bool>("logMissingImagesOnce", true, "Whether to log missing image folders or images only once until valid images are found.", (string)null, false, false, (ValueValidator)null, (string)null); } public bool ApplySnapshot(PreferencesSnapshot snapshot) { bool result = false; bool? isEnabled = snapshot.IsEnabled; if (isEnabled.HasValue) { bool valueOrDefault = isEnabled == true; if (_enabledPreference.Value != valueOrDefault) { _enabledPreference.Value = valueOrDefault; result = true; } } if (snapshot.BaseFolder != null && !string.Equals(_baseFolderPreference.Value, snapshot.BaseFolder, StringComparison.Ordinal)) { _baseFolderPreference.Value = snapshot.BaseFolder; result = true; } isEnabled = snapshot.UseNormalImagesForTutorial; if (isEnabled.HasValue) { bool valueOrDefault2 = isEnabled == true; if (_useNormalImagesForTutorialPreference.Value != valueOrDefault2) { _useNormalImagesForTutorialPreference.Value = valueOrDefault2; result = true; } } isEnabled = snapshot.WatchFolders; if (isEnabled.HasValue) { bool valueOrDefault3 = isEnabled == true; if (_watchFoldersPreference.Value != valueOrDefault3) { _watchFoldersPreference.Value = valueOrDefault3; result = true; } } isEnabled = snapshot.LogMissingImagesOnce; if (isEnabled.HasValue) { bool valueOrDefault4 = isEnabled == true; if (_logMissingImagesOncePreference.Value != valueOrDefault4) { _logMissingImagesOncePreference.Value = valueOrDefault4; result = true; } } return result; } } internal static class PreferencesFileParser { public static bool TryReadSnapshot(string preferencesFilePath, out PreferencesSnapshot snapshot) { snapshot = new PreferencesSnapshot(); if (!File.Exists(preferencesFilePath)) { return false; } try { string[] array = File.ReadAllLines(preferencesFilePath); bool flag = false; bool? isEnabled = null; string baseFolder = null; bool? useNormalImagesForTutorial = null; bool? watchFolders = null; bool? logMissingImagesOnce = null; string[] array2 = array; foreach (string text in array2) { string text2 = text.Trim(); if (text2.Length == 0) { continue; } if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { string a = text2.Substring(1, text2.Length - 2); flag = string.Equals(a, "Custom Loading Screens", StringComparison.Ordinal); } else { if (!flag) { continue; } int num = text2.IndexOf('='); if (num > 0) { string a2 = text2.Substring(0, num).Trim(); string value = text2.Substring(num + 1).Trim(); bool result2; bool result3; bool result4; if (string.Equals(a2, "enabled", StringComparison.Ordinal) && bool.TryParse(value, out var result)) { isEnabled = result; } else if (string.Equals(a2, "baseFolder", StringComparison.Ordinal)) { baseFolder = TrimEnclosingQuotes(value); } else if (string.Equals(a2, "useNormalImagesForTutorial", StringComparison.Ordinal) && bool.TryParse(value, out result2)) { useNormalImagesForTutorial = result2; } else if (string.Equals(a2, "watchFolders", StringComparison.Ordinal) && bool.TryParse(value, out result3)) { watchFolders = result3; } else if (string.Equals(a2, "logMissingImagesOnce", StringComparison.Ordinal) && bool.TryParse(value, out result4)) { logMissingImagesOnce = result4; } } } } snapshot = new PreferencesSnapshot(isEnabled, baseFolder, useNormalImagesForTutorial, watchFolders, logMissingImagesOnce); return true; } catch (IOException) { return false; } catch (UnauthorizedAccessException) { return false; } } private static string TrimEnclosingQuotes(string value) { if (value.Length >= 2 && value[0] == '"') { if (value[value.Length - 1] == '"') { return value.Substring(1, value.Length - 2); } } return value; } } internal sealed class PreferencesFileWatcher : IDisposable { private const string PreferencesFileName = "MelonPreferences.cfg"; private static readonly TimeSpan PreferencesReloadDebounce = TimeSpan.FromMilliseconds(150.0); private readonly object _preferencesReloadLock = new object(); private FileSystemWatcher? _preferencesWatcher; private DateTime? _preferencesReloadDueUtc; public string? FilePath { get; } public PreferencesFileWatcher(Log logger) { FilePath = ResolvePreferencesFilePath(); Initialize(logger); } public void ScheduleReload() { lock (_preferencesReloadLock) { _preferencesReloadDueUtc = DateTime.UtcNow + PreferencesReloadDebounce; } } public bool TryConsumeScheduledReload() { lock (_preferencesReloadLock) { DateTime? preferencesReloadDueUtc = _preferencesReloadDueUtc; if (!preferencesReloadDueUtc.HasValue || DateTime.UtcNow < _preferencesReloadDueUtc.Value) { return false; } _preferencesReloadDueUtc = null; return true; } } public void Dispose() { if (_preferencesWatcher != null) { _preferencesWatcher.EnableRaisingEvents = false; _preferencesWatcher.Changed -= OnPreferencesFileChanged; _preferencesWatcher.Created -= OnPreferencesFileChanged; _preferencesWatcher.Renamed -= OnPreferencesFileRenamed; _preferencesWatcher.Dispose(); _preferencesWatcher = null; } } private void Initialize(Log logger) { if (string.IsNullOrWhiteSpace(FilePath)) { logger.Warning("Could not resolve the MelonPreferences path; live config reload is disabled."); return; } string directoryName = Path.GetDirectoryName(FilePath); if (string.IsNullOrWhiteSpace(directoryName) || !Directory.Exists(directoryName)) { logger.Warning("MelonPreferences directory was not found: " + directoryName); return; } _preferencesWatcher = new FileSystemWatcher(directoryName, "MelonPreferences.cfg") { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.CreationTime), IncludeSubdirectories = false, EnableRaisingEvents = true }; _preferencesWatcher.Changed += OnPreferencesFileChanged; _preferencesWatcher.Created += OnPreferencesFileChanged; _preferencesWatcher.Renamed += OnPreferencesFileRenamed; } private void OnPreferencesFileChanged(object sender, FileSystemEventArgs e) { ScheduleReload(); } private void OnPreferencesFileRenamed(object sender, RenamedEventArgs e) { ScheduleReload(); } private static string? ResolvePreferencesFilePath() { string userDataDirectory = MelonEnvironment.UserDataDirectory; if (string.IsNullOrWhiteSpace(userDataDirectory)) { return null; } return Path.Combine(userDataDirectory, "MelonPreferences.cfg"); } } internal sealed class PreferencesSnapshot { public bool? IsEnabled { get; } public string? BaseFolder { get; } public bool? UseNormalImagesForTutorial { get; } public bool? WatchFolders { get; } public bool? LogMissingImagesOnce { get; } public PreferencesSnapshot(bool? isEnabled = null, string? baseFolder = null, bool? useNormalImagesForTutorial = null, bool? watchFolders = null, bool? logMissingImagesOnce = null) { IsEnabled = isEnabled; BaseFolder = baseFolder; UseNormalImagesForTutorial = useNormalImagesForTutorial; WatchFolders = watchFolders; LogMissingImagesOnce = logMissingImagesOnce; } } }