using 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? _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(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 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 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 imageConversionTypes, Func 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 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 GetLoadableTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.OfType(); } } } 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(); } List list = new List(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(); } return Directory.GetFiles(folderPath).Where(IsSupportedImageFile).OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase) .ToArray(); } public static bool IsSupportedImageFile(string path) { string extension = Path.GetExtension(path); return SupportedExtensions.Contains(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(); public Sprite[] TutorialSprites { get; private set; } = Array.Empty(); 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()))); 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(); TutorialSprites = Array.Empty(); } } } 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 TypeCache = new ConcurrentDictionary(StringComparer.Ordinal); public static IReadOnlyList MaterializeObjects(object? value) { if (value == null) { return Array.Empty(); } if (value is IEnumerable enumerable) { List list = new List(); foreach (object item in enumerable) { list.Add(item); } return list; } object memberValue = GetMemberValue(value, "Count"); if (memberValue == null) { return Array.Empty(); } int num = Convert.ToInt32(memberValue); PropertyInfo property = value.GetType().GetProperty("Item", new Type[1] { typeof(int) }); if (property == null) { return Array.Empty(); } List list2 = new List(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 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 _enabledPreference; private readonly MelonPreferences_Entry _baseFolderPreference; private readonly MelonPreferences_Entry _useNormalImagesForTutorialPreference; private readonly MelonPreferences_Entry _watchFoldersPreference; private readonly MelonPreferences_Entry _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("enabled", true, "Whether the mod is enabled.", (string)null, false, false, (ValueValidator)null, (string)null); _baseFolderPreference = val.CreateEntry("baseFolder", string.Empty, "Optional override for the custom loading image base folder.", (string)null, false, false, (ValueValidator)null, (string)null); _useNormalImagesForTutorialPreference = val.CreateEntry("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("watchFolders", true, "Whether the mod should watch the custom image folder for changes.", (string)null, false, false, (ValueValidator)null, (string)null); _logMissingImagesOncePreference = val.CreateEntry("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; } } }