From 79c8f6f3f23b64bbcdae00dc33125866b8e50236 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 00:09:04 +0200 Subject: [PATCH 01/29] Set up infrastructure for redone asset loading Mod assets are now loaded one by one instead of all at once. Introduced more APIs for fine-grained control over asset loading. --- Source/Data/Assets.cs | 319 +++++++++++++++++++++++------------------- 1 file changed, 173 insertions(+), 146 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 50537df1..cc309ccf 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -1,4 +1,6 @@ using Celeste64.Mod; +using Mono.Cecil.Cil; +using MonoMod.RuntimeDetour; using System.Collections.Concurrent; using System.Diagnostics; using System.Text; @@ -96,24 +98,19 @@ public static string ContentPath public static List Levels { get; private set; } = []; - public static void Load() - { - var timer = Stopwatch.StartNew(); + private static List tasks = []; - Levels.Clear(); - Maps.Clear(); - Shaders.Clear(); - Textures.Clear(); - Subtextures.Clear(); - Models.Clear(); - Fonts.Clear(); - Languages.Clear(); - Sounds.Clear(); - Music.Clear(); - Audio.Unload(); + private static void LoadDirectoryRecursive(GameMod mod, string folder, string extension, ConcurrentBag into, Action> task) + { + foreach (var file in mod.Filesystem.FindFilesInDirectoryRecursive(folder, extension)) + { + task(file, mod, into); + } + } - Map.ModActorFactories.Clear(); - ModLoader.RegisterAllMods(); + public static void LoadMod(GameMod mod) + { + Log.Info($"Loading assets for mod {mod.ModInfo.Id}"); var maps = new ConcurrentBag<(Map, GameMod)>(); var images = new ConcurrentBag<(string, Image, GameMod)>(); @@ -121,168 +118,139 @@ public static void Load() var sounds = new ConcurrentBag<(string, FMOD.Sound, GameMod)>(); var music = new ConcurrentBag<(string, FMOD.Sound, GameMod)>(); var langs = new ConcurrentBag<(Language, GameMod)>(); - var tasks = new List(); + tasks = []; - // NOTE: Make sure to update ModManager.OnModFileChanged() as well, for hot-reloading to work! + IModFilesystem modFs = mod.Filesystem; - var globalFs = ModManager.Instance.GlobalFilesystem; - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(MapsFolder, MapsExtension)) + // Load maps + LoadDirectoryRecursive(mod, MapsFolder, MapsExtension, maps, (file, mod, maps) => { // Skip the "autosave" folder - if (file.StartsWith($"{MapsFolder}/autosave", StringComparison.OrdinalIgnoreCase)) - continue; + if (file.StartsWith($"{MapsFolder}/autosave", StringComparison.OrdinalIgnoreCase)) return; - tasks.Add(Task.Run(() => + if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, + stream => new Map(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, - stream => new Map(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) - { - maps.Add((map, mod)); - } - })); - } + maps.Add((map, mod)); + } + }); - // load texture pngs - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(TexturesFolder, TexturesExtension)) + // Load textures + LoadDirectoryRecursive(mod, TexturesFolder, TexturesExtension, images, (file, mod, images) => { - tasks.Add(Task.Run(() => + if (mod.Filesystem != null && mod.Filesystem.TryLoadImage(file, out var image)) { - if (mod.Filesystem != null && mod.Filesystem.TryLoadImage(file, out var image)) - { - images.Add((GetResourceNameFromVirt(file, TexturesFolder), image, mod)); - } - })); - } + images.Add((GetResourceNameFromVirt(file, TexturesFolder), image, mod)); + } + }); - // load faces - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(FacesFolder, FacesExtension)) + // Load character faces + LoadDirectoryRecursive(mod, FacesFolder, FacesExtension, images, (file, mod, images) => { - tasks.Add(Task.Run(() => + var name = $"faces/{GetResourceNameFromVirt(file, FacesFolder)}"; + if (mod.Filesystem != null && mod.Filesystem.TryLoadImage(file, out var image)) { - var name = $"faces/{GetResourceNameFromVirt(file, FacesFolder)}"; - if (mod.Filesystem != null && mod.Filesystem.TryLoadImage(file, out var image)) - { - images.Add((name, image, mod)); - } - })); - } + images.Add((name, image, mod)); + } + }); - // load glb models - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(ModelsFolder, ModelsExtension)) + // Load glb models + LoadDirectoryRecursive(mod, ModelsFolder, ModelsExtension, models, (file, mod, models) => { - tasks.Add(Task.Run(() => + if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => SharpGLTF.Schema2.ModelRoot.ReadGLB(stream), + out var input)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => SharpGLTF.Schema2.ModelRoot.ReadGLB(stream), - out var input)) - { - var model = new SkinnedTemplate(input); - models.Add((GetResourceNameFromVirt(file, ModelsFolder), model, mod)); - } - })); - } + var model = new SkinnedTemplate(input); + models.Add((GetResourceNameFromVirt(file, ModelsFolder), model, mod)); + } + }); - // load languages - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(TextFolder, TextExtension)) + // Load language files + LoadDirectoryRecursive(mod, TextFolder, TextExtension, langs, (file, mod, langs) => { - tasks.Add(Task.Run(() => + if (mod.Filesystem != null && mod.Filesystem.TryLoadText(file, out var data)) { - if (mod.Filesystem != null && mod.Filesystem.TryLoadText(file, out var data)) - { - if (JsonSerializer.Deserialize(data, LanguageContext.Default.Language) is { } lang) - langs.Add((lang, mod)); - } - })); - } + if (JsonSerializer.Deserialize(data, LanguageContext.Default.Language) is { } lang) + langs.Add((lang, mod)); + } + }); - // load audio - var allBankFiles = globalFs.FindFilesInDirectoryRecursiveWithMod(AudioFolder, AudioExtension).ToList(); + // Load FMOD audio banks + var allBankFiles = modFs.FindFilesInDirectoryRecursive(AudioFolder, AudioExtension).ToList(); // load strings first - foreach (var (file, mod) in allBankFiles) + foreach (var file in allBankFiles) { if (mod.Filesystem != null && file.EndsWith($".strings.{AudioExtension}")) mod.Filesystem.TryOpenFile(file, Audio.LoadBankFromStream); } // load banks second - foreach (var (file, mod) in allBankFiles) + foreach (var file in allBankFiles) { if (mod.Filesystem != null && file.EndsWith($".{AudioExtension}") && !file.EndsWith($".strings.{AudioExtension}")) mod.Filesystem.TryOpenFile(file, Audio.LoadBankFromStream); } - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(SoundsFolder, SoundsExtension)) + // Load wav sounds + LoadDirectoryRecursive(mod, SoundsFolder, SoundsExtension, sounds, (file, mod, sounds) => { - tasks.Add(Task.Run(() => + if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => Audio.LoadWavFromStream(stream), + out var sound)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => Audio.LoadWavFromStream(stream), - out var sound)) + if (sound != null) { - if (sound != null) - { - sounds.Add((GetResourceNameFromVirt(file, SoundsFolder), sound.Value, mod)); - } + sounds.Add((GetResourceNameFromVirt(file, SoundsFolder), sound.Value, mod)); } - })); - } + } + }); - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(MusicFolder, MusicExtension)) + // Load wav music + LoadDirectoryRecursive(mod, MusicFolder, MusicExtension, music, (file, mod, music) => { - tasks.Add(Task.Run(() => + if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => Audio.LoadWavFromStream(stream), + out var song)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => Audio.LoadWavFromStream(stream), - out var song)) + if (song != null) { - if (song != null) - { - music.Add((GetResourceNameFromVirt(file, MusicFolder), song.Value, mod)); - } + music.Add((GetResourceNameFromVirt(file, MusicFolder), song.Value, mod)); } - })); - } + } + }); // load level, dialog jsons - foreach (var mod in ModManager.Instance.Mods) + mod.Levels.Clear(); + if (modFs != null && modFs.TryOpenFile(LevelsJSON, + stream => JsonSerializer.Deserialize(stream, LevelInfoListContext.Default.ListLevelInfo) ?? [], + out var levels)) { - mod.Levels.Clear(); - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(LevelsJSON, - stream => JsonSerializer.Deserialize(stream, LevelInfoListContext.Default.ListLevelInfo) ?? [], - out var levels)) - { - mod.Levels.AddRange(levels); - Levels.AddRange(levels); - } - - // if (mod.Filesystem != null && mod.Filesystem.TryOpenFile("Dialog.json", - // stream => JsonSerializer.Deserialize(stream, DialogLineDictContext.Default.DictionaryStringListDialogLine) ?? [], - // out var dialog)) - // { - // foreach (var (key, value) in dialog) - // { - // Dialog.Add(key, value, mod); - // } - // } + mod.Levels.AddRange(levels); + Levels.AddRange(levels); } - // load glsl shaders - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(ShadersFolder, ShadersExtension)) + if (modFs != null) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => LoadShader(file, stream), out var shader)) + // load glsl shaders + foreach (var file in modFs.FindFilesInDirectoryRecursive(ShadersFolder, ShadersExtension)) { - shader.Name = GetResourceNameFromVirt(file, ShadersFolder); - Shaders.Add(shader.Name, shader, mod); + if (modFs.TryOpenFile(file, stream => LoadShader(file, stream), out var shader)) + { + shader.Name = GetResourceNameFromVirt(file, ShadersFolder); + Shaders.Add(shader.Name, shader, mod); + } } - } - // load font files - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(FontsFolder, "")) - { - if (file.EndsWith($".{FontsExtensionTTF}") || file.EndsWith($".{FontsExtensionOTF}")) + // load font files + foreach (var file in modFs.FindFilesInDirectoryRecursive(FontsFolder, "")) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => new Font(stream), out var font)) - Fonts.Add(GetResourceNameFromVirt(file, FontsFolder), font, mod); + if (file.EndsWith($".{FontsExtensionTTF}") || file.EndsWith($".{FontsExtensionOTF}")) + { + if (modFs.TryOpenFile(file, stream => new Font(stream), out var font)) + Fonts.Add(GetResourceNameFromVirt(file, FontsFolder), font, mod); + } } } // pack sprites into single texture + if (modFs != null) { var packer = new Packer { @@ -290,9 +258,9 @@ public static void Load() CombineDuplicates = false, Padding = 1 }; - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(SpritesFolder, SpritesExtension)) + foreach (var file in modFs.FindFilesInDirectoryRecursive(SpritesFolder, SpritesExtension)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => new Image(stream), out var img)) + if (modFs.TryOpenFile(file, stream => new Image(stream), out var img)) { packer.Add($"{mod.ModInfo.Id}:{GetResourceNameFromVirt(file, SpritesFolder)}", img); } @@ -309,11 +277,7 @@ public static void Load() foreach (var it in result.Entries) { string[] nameSplit = it.Name.Split(':'); - var mod = ModManager.Instance.Mods.FirstOrDefault(mod => mod.ModInfo.Id == nameSplit[0]) ?? ModManager.Instance.VanillaGameMod; - if (mod != null) - { - Subtextures.Add(nameSplit[1], new Subtexture(pages[it.Page], it.Source, it.Frame), mod); - } + Subtextures.Add(nameSplit[1], new Subtexture(pages[it.Page], it.Source, it.Frame), mod); } } @@ -323,20 +287,20 @@ public static void Load() foreach (var task in tasks) task.Wait(); - foreach (var (name, img, mod) in images) + foreach (var (name, img, _) in images) Textures.Add(name, new Texture(img) { Name = name }, mod); - foreach (var (map, mod) in maps) + foreach (var (map, _) in maps) Maps.Add(map.Name, map, mod); - foreach (var (name, sound, mod) in sounds) + foreach (var (name, sound, _) in sounds) Sounds.Add(name, sound, mod); - foreach (var (name, song, mod) in music) + foreach (var (name, song, _) in music) Music.Add(name, song, mod); - foreach (var (name, model, mod) in models) + foreach (var (name, model, _) in models) { model.ConstructResources(); Models.Add(name, model, mod); } - foreach (var (lang, mod) in langs) + foreach (var (lang, _) in langs) { if (Languages.TryGetValue(lang.ID, out var existing)) { @@ -368,21 +332,84 @@ public static void Load() ); } - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(SkinsFolder, SkinsExtension)) + if (modFs != null) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, - stream => JsonSerializer.Deserialize(stream, SkinInfoContext.Default.SkinInfo), out var skin) && skin.IsValid()) - { - mod.Skins.Add(skin); - } - else + foreach (var file in modFs.FindFilesInDirectoryRecursive(SkinsFolder, SkinsExtension)) { - Log.Warning($"Improperly configured skin: {file}"); + if (modFs.TryOpenFile(file, + stream => JsonSerializer.Deserialize(stream, SkinInfoContext.Default.SkinInfo), out var skin) && skin.IsValid()) + { + mod.Skins.Add(skin); + } + else + { + Log.Warning($"Improperly configured skin: {file}"); + } } } + } + + private static void StageVanilla() + { + GameMod? vanilla = ModManager.Instance.VanillaGameMod; + + if (vanilla is null) + { + throw new Exception("Vanilla mod does not exist. This means something went horribly wrong!"); + } + + LoadMod(vanilla); // make sure the active language is ready for use Language.Current.Use(); + } + + public static void Unload() + { + Levels.Clear(); + Maps.Clear(); + Shaders.Clear(); + Textures.Clear(); + Subtextures.Clear(); + Models.Clear(); + Fonts.Clear(); + Languages.Clear(); + Sounds.Clear(); + Music.Clear(); + Audio.Unload(); + + Map.ModActorFactories.Clear(); + } + + public static void Load() + { + var timer = Stopwatch.StartNew(); + List loadQueue; + + Unload(); + + ModLoader.RegisterAllMods(); + + StageVanilla(); + + loadQueue = ModManager.Instance.EnabledMods.Where(gm => gm is not VanillaGameMod).ToList(); + + // NOTE: Make sure to update ModManager.OnModFileChanged() as well, for hot-reloading to work! + + // Go through all of the mods in queue and load them + while (loadQueue.Count > 0) + { + LoadMod(loadQueue.First()); + + loadQueue.RemoveAt(0); + } + + StringBuilder x = new(); + + foreach (var y in Textures) + { + x.Append("\nsubtext " + y.Value.Name); + } ModManager.Instance.OnAssetsLoaded(); From 8d01935424bd0a709a3a23db0f4f934a4f9180f9 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 00:09:40 +0200 Subject: [PATCH 02/29] Remove unnecessary usings --- Source/Data/Assets.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index cc309ccf..3c20e3d6 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -1,6 +1,4 @@ using Celeste64.Mod; -using Mono.Cecil.Cil; -using MonoMod.RuntimeDetour; using System.Collections.Concurrent; using System.Diagnostics; using System.Text; From 334de1be77ed0058bffeccbf656ddf3a10dbc021 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 00:10:13 +0200 Subject: [PATCH 03/29] Remove debugging leftovers --- Source/Data/Assets.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 3c20e3d6..59e76c72 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -402,13 +402,6 @@ public static void Load() loadQueue.RemoveAt(0); } - StringBuilder x = new(); - - foreach (var y in Textures) - { - x.Append("\nsubtext " + y.Value.Name); - } - ModManager.Instance.OnAssetsLoaded(); Log.Info($"Loaded Assets in {timer.ElapsedMilliseconds}ms"); From 11e68b7c38c0d95b4639ee600bb0dad2ed1b383b Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 00:20:36 +0200 Subject: [PATCH 04/29] Add comments, add more utility functions --- Source/Data/Assets.cs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 59e76c72..8dce15af 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -97,6 +97,7 @@ public static string ContentPath public static List Levels { get; private set; } = []; private static List tasks = []; + public static List loadQueue = []; private static void LoadDirectoryRecursive(GameMod mod, string folder, string extension, ConcurrentBag into, Action> task) { @@ -379,28 +380,44 @@ public static void Unload() Map.ModActorFactories.Clear(); } + public static void FillLoadQueue() + { + loadQueue = ModManager.Instance.EnabledMods.Where(gm => gm is not VanillaGameMod).ToList(); + } + + public static void MoveLoadQueue() + { + LoadMod(loadQueue.First()); + + loadQueue.RemoveAt(0); + } + + public static void LoadAllQueued() + { + while (loadQueue.Count > 0) MoveLoadQueue(); + } + + /* + Asset loading was redone in 0.7.0. However, this function is preserved here. + For compatibility and ease of use, it has the exact same behaviour on the outside as before. + */ public static void Load() { var timer = Stopwatch.StartNew(); - List loadQueue; Unload(); ModLoader.RegisterAllMods(); + // Load vanilla assets first StageVanilla(); - loadQueue = ModManager.Instance.EnabledMods.Where(gm => gm is not VanillaGameMod).ToList(); + FillLoadQueue(); // NOTE: Make sure to update ModManager.OnModFileChanged() as well, for hot-reloading to work! // Go through all of the mods in queue and load them - while (loadQueue.Count > 0) - { - LoadMod(loadQueue.First()); - - loadQueue.RemoveAt(0); - } + LoadAllQueued(); ModManager.Instance.OnAssetsLoaded(); From e3d3fd65cfcf6ec1ea0a679bfa2a3a5d02916e12 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:05:22 +0200 Subject: [PATCH 05/29] Add further overhauls to asset loading, begin redo of startup scene --- Source/Data/Assets.cs | 15 +++-- Source/Mod/Core/ModLoader.cs | 16 +++--- Source/Mod/ImGui/ImGuiRenderer.cs | 2 +- Source/Scenes/Startup.cs | 96 ++++++++++++++++++++++--------- 4 files changed, 89 insertions(+), 40 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 8dce15af..7a7e81d1 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -109,7 +109,7 @@ private static void LoadDirectoryRecursive(GameMod mod, string folder, string public static void LoadMod(GameMod mod) { - Log.Info($"Loading assets for mod {mod.ModInfo.Id}"); + Log.Info($"Loading assets for {mod.ModInfo.Id}"); var maps = new ConcurrentBag<(Map, GameMod)>(); var images = new ConcurrentBag<(string, Image, GameMod)>(); @@ -348,7 +348,7 @@ public static void LoadMod(GameMod mod) } } - private static void StageVanilla() + public static void StageVanilla() { GameMod? vanilla = ModManager.Instance.VanillaGameMod; @@ -380,16 +380,21 @@ public static void Unload() Map.ModActorFactories.Clear(); } - public static void FillLoadQueue() + public static int FillLoadQueue() { loadQueue = ModManager.Instance.EnabledMods.Where(gm => gm is not VanillaGameMod).ToList(); + + return loadQueue.Count; } - public static void MoveLoadQueue() + public static bool MoveLoadQueue() { - LoadMod(loadQueue.First()); + if (loadQueue.Count < 1) return false; + LoadMod(loadQueue.First()); loadQueue.RemoveAt(0); + + return true; } public static void LoadAllQueued() diff --git a/Source/Mod/Core/ModLoader.cs b/Source/Mod/Core/ModLoader.cs index c0f0b48e..9fa9033e 100644 --- a/Source/Mod/Core/ModLoader.cs +++ b/Source/Mod/Core/ModLoader.cs @@ -47,7 +47,7 @@ public static string[] ModFolderPaths internal static void RegisterAllMods() { FailedToLoadMods.Clear(); - ModManager.Instance.VanillaGameMod = new VanillaGameMod + if (ModManager.Instance.VanillaGameMod is null) ModManager.Instance.VanillaGameMod = new VanillaGameMod { // Mod Infos are required now, so make a dummy mod info for the valilla game too. This shouldn't really be used for anything. ModInfo = new ModInfo @@ -151,7 +151,7 @@ internal static void RegisterAllMods() { var mod = LoadGameMod(info, fs); mod.Filesystem?.AssociateWithMod(mod); - + try { ModManager.Instance.RegisterMod(mod); @@ -169,7 +169,7 @@ internal static void RegisterAllMods() HookManager.Instance.ClearHooksOfMod(info); throw; } - + loaded.Add(info); loadedModInIteration = true; } @@ -319,7 +319,7 @@ private static GameMod LoadGameMod(ModInfo info, IModFilesystem fs) private static void FindAndRegisterHooks(ModInfo modInfo, Type type) { List hooks = []; - + try { // On. hooks @@ -327,20 +327,20 @@ private static void FindAndRegisterHooks(ModInfo modInfo, Type type) .Select(m => (m, m.GetCustomAttribute())) .Where(t => t.Item2 != null) .Cast<(MethodInfo, InternalOnHookGenTargetAttribute)>(); - + foreach (var (info, attr) in onHookMethods) { var onHook = new Hook(attr.Target, info); hooks.Add(onHook); HookManager.Instance.RegisterHook(onHook, modInfo); } - + // IL. hooks var ilHookMethods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) .Select(m => (m, m.GetCustomAttribute())) .Where(t => t.Item2 != null) .Cast<(MethodInfo, InternalILHookGenTargetAttribute)>(); - + foreach (var (info, attr) in ilHookMethods) { var ilHook = new ILHook(attr.Target, info.CreateDelegate()); @@ -353,7 +353,7 @@ private static void FindAndRegisterHooks(ModInfo modInfo, Type type) // Some hook failed. Need to dispose all previous ones foreach (var hook in hooks) hook.Dispose(); - + throw; } } diff --git a/Source/Mod/ImGui/ImGuiRenderer.cs b/Source/Mod/ImGui/ImGuiRenderer.cs index 6e8ddfe7..40b2068e 100644 --- a/Source/Mod/ImGui/ImGuiRenderer.cs +++ b/Source/Mod/ImGui/ImGuiRenderer.cs @@ -109,7 +109,7 @@ public void BeforeRender() Matrix.CreateOrthographicOffCenter(0f, target!.Width, target.Height, 0f, -1.0f, 1.0f)); } - if (spriteMaterial == null) + if (spriteMaterial == null && Assets.Shaders.ContainsKey("Sprite")) { spriteMaterial = new Material(Assets.Shaders["Sprite"]); diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 8146091d..8cab6700 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -1,4 +1,6 @@ +using Celeste64.Mod; using Celeste64.Mod.Data; +using System.Text; namespace Celeste64; @@ -8,10 +10,26 @@ namespace Celeste64; /// public class Startup : Scene { - private int loadDelay = 5; + private int assetQueueSize; + private int queueIndex = 0; + private bool areModsRegistered = false; - private void BeginGame() + public Startup() { + // Register vanilla mod so that it can load its assets + // Assume this will be overridden later + ModManager.Instance.VanillaGameMod = new VanillaGameMod + { + ModInfo = new ModInfo + { + Id = "Celeste64Vanilla", + Name = "Celeste 64: Fragments of the Mountain", + VersionString = "1.1.1", + }, + Filesystem = new FolderModFilesystem(Assets.ContentPath) + }; + ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); + // load save file { SaveManager.Instance.LoadSaveByFileName(SaveManager.Instance.GetLastLoadedSave()); @@ -27,9 +45,8 @@ private void BeginGame() ModSettings.LoadModSettingsByFileName(ModSettings.DefaultFileName); } - // load assets - // this currently needs to happen after the save file loads, because this also loads mods, which get their saved settings from the save file. - Assets.Load(); + // load vanilla assets + Assets.StageVanilla(); // make sure the active language is ready for use, // since the save file may have loaded a different language than default. @@ -39,38 +56,65 @@ private void BeginGame() { Controls.LoadControlsByFileName(Controls.DefaultFileName); } + } - // enter game - //Assets.Levels[0].Enter(new AngledWipe()); - if (Input.Keyboard.CtrlOrCommand && !Game.Instance.IsMidTransition && Settings.EnableQuickStart) + public override void Update() + { + if (!areModsRegistered) { - var entry = new Overworld.Entry(Assets.Levels[0], null); - entry.Level.Enter(); + // this also loads mods, which get their saved settings from the save file. + ModLoader.RegisterAllMods(); + assetQueueSize = Assets.FillLoadQueue(); + areModsRegistered = true; + return; } - else + + // load assets + bool shouldGo = !Assets.MoveLoadQueue(); + queueIndex++; + + if (shouldGo && !Game.Instance.IsMidTransition) { - Game.Instance.Goto(new Transition() + // enter game + if (Input.Keyboard.CtrlOrCommand && !Game.Instance.IsMidTransition && Settings.EnableQuickStart) + { + var entry = new Overworld.Entry(Assets.Levels[0], null); + entry.Level.Enter(); + } + else { - Mode = Transition.Modes.Replace, - Scene = () => new Titlescreen(), - ToBlack = null, - FromBlack = new AngledWipe(), - }); + Game.Instance.Goto(new Transition() + { + Mode = Transition.Modes.Replace, + Scene = () => new Titlescreen(), + ToBlack = null, + FromBlack = new AngledWipe(), + }); + } } } - public override void Update() + public override void Render(Target target) { - if (loadDelay > 0) + target.Clear(Color.Black); + + Batcher batcher = new(); + Rect bounds = new(0, 0, target.Width, target.Height); + + string loadInfo; + + if (!areModsRegistered) { - loadDelay--; - if (loadDelay <= 0) - BeginGame(); + loadInfo = "Registering modules..."; + } + else + { + loadInfo = $"Loading assets: {queueIndex + 1} of {assetQueueSize}"; } - } - public override void Render(Target target) - { - target.Clear(Color.Black); + UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(0, -28 * Game.RelativeScale), Vec2.Zero, Color.White); + + batcher.Render(target); + batcher.Clear(); } } From 4d3f00f00f1d9c0e52ab8681a8b1f070d389d235 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:29:54 +0200 Subject: [PATCH 06/29] Restore old delay behaviour, add strings, show current loading id --- Content/Text/English.json | 13 +++++++------ Source/Scenes/Startup.cs | 12 ++++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Content/Text/English.json b/Content/Text/English.json index 1013d496..44ad5721 100644 --- a/Content/Text/English.json +++ b/Content/Text/English.json @@ -28,8 +28,8 @@ "PauseSaveQuit.desc": "Save & Quit to the Title Screen", "PauseSaves": "Saves", "PauseSaves.desc": "Open the Save Select Menu", - "PauseModsMenu" : "Mods", - "PauseModsMenu.desc" : "Open the Mods Menu", + "PauseModsMenu": "Mods", + "PauseModsMenu.desc": "Open the Mods Menu", "OptionsTitle": "Options", "OptionsFullscreen": "Fullscreen", "OptionsFullscreen.desc": "Control if the Game is Fullscreen or Windowed", @@ -53,11 +53,10 @@ "OptionsNo": "No", "Skin": "Skin", "Skin.desc": "Choose a Skin to Wear", - - "SaveCreateFile": "Create new file?" , - "SaveDeleteFile": "Delete this file?" , + "SaveCreateFile": "Create new file?", + "SaveDeleteFile": "Delete this file?", "SaveDeleteDefaultFile": "Cannot fully delete default save file:'{0}'. Reset it to default instead?", - "SaveCopyFile": "Copy this file?" , + "SaveCopyFile": "Copy this file?", "FujiOptions": "Fuji Options...", "FujiOptions.desc": "Configure Options for the Fuji Mod Loader", "FujiWriteLog": "Write Log File", @@ -72,6 +71,8 @@ "FujiContinueToMods": "Continue", "FujiContinueToMods.desc": "Continue to the Mods Menu", "FujiOverworldModSlideNote": "Use Up/Down to filter by mod", + "FujiLoaderStatusRegistering": "Registering modules...", + "FujiLoaderStatusNormal": "Loading assets: {0} ({1} of {2})", "FailedToLoadMods": "Failed to load {0} mods. Check Mods Menu for details.", "ModEnabled": "Enabled", "ModOptions": "Mod Options", diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 8cab6700..8983326f 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -13,6 +13,7 @@ public class Startup : Scene private int assetQueueSize; private int queueIndex = 0; private bool areModsRegistered = false; + private int delay = 5; public Startup() { @@ -60,6 +61,12 @@ public Startup() public override void Update() { + if (delay > 0) + { + delay--; + return; + } + if (!areModsRegistered) { // this also loads mods, which get their saved settings from the save file. @@ -109,10 +116,11 @@ public override void Render(Target target) } else { - loadInfo = $"Loading assets: {queueIndex + 1} of {assetQueueSize}"; + string modId = Assets.loadQueue[0].ModInfo.Id; + loadInfo = $"Loading assets: {modId} ({queueIndex + 1} of {assetQueueSize})"; } - UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(0, -28 * Game.RelativeScale), Vec2.Zero, Color.White); + UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(-4 * Game.RelativeScale, -28 * Game.RelativeScale), Vec2.Zero, Color.White); batcher.Render(target); batcher.Clear(); From a5100a03a3378efa145575523b0fc43b439e37b8 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:40:34 +0200 Subject: [PATCH 07/29] Convert hardcoded strings to loc keys, add padding --- Source/Scenes/Startup.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 8983326f..674bb51f 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -13,6 +13,7 @@ public class Startup : Scene private int assetQueueSize; private int queueIndex = 0; private bool areModsRegistered = false; + private string lastLoadedId = string.Empty; private int delay = 5; public Startup() @@ -77,6 +78,7 @@ public override void Update() } // load assets + lastLoadedId = Assets.loadQueue.Count > 0 ? Assets.loadQueue[0].ModInfo.Id : string.Empty; bool shouldGo = !Assets.MoveLoadQueue(); queueIndex++; @@ -112,15 +114,14 @@ public override void Render(Target target) if (!areModsRegistered) { - loadInfo = "Registering modules..."; + loadInfo = Loc.Str("FujiLoaderStatusRegistering"); } else { - string modId = Assets.loadQueue[0].ModInfo.Id; - loadInfo = $"Loading assets: {modId} ({queueIndex + 1} of {assetQueueSize})"; + loadInfo = String.Format(Loc.Str("FujiLoaderStatusNormal"), lastLoadedId, queueIndex, assetQueueSize); } - UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(-4 * Game.RelativeScale, -28 * Game.RelativeScale), Vec2.Zero, Color.White); + UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(4 * Game.RelativeScale, -28 * Game.RelativeScale), Vec2.Zero, Color.White); batcher.Render(target); batcher.Clear(); From de89445c1ccfa6061a52ae19766fc0290aa79071 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:43:43 +0200 Subject: [PATCH 08/29] Restore timer logging on startup scene --- Source/Scenes/Startup.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 674bb51f..793fc5ce 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -1,6 +1,6 @@ using Celeste64.Mod; using Celeste64.Mod.Data; -using System.Text; +using System.Diagnostics; namespace Celeste64; @@ -15,9 +15,12 @@ public class Startup : Scene private bool areModsRegistered = false; private string lastLoadedId = string.Empty; private int delay = 5; + private Stopwatch timer; public Startup() { + timer = Stopwatch.StartNew(); + // Register vanilla mod so that it can load its assets // Assume this will be overridden later ModManager.Instance.VanillaGameMod = new VanillaGameMod @@ -84,6 +87,8 @@ public override void Update() if (shouldGo && !Game.Instance.IsMidTransition) { + Log.Info($"Loaded Assets in {timer.ElapsedMilliseconds}ms"); + // enter game if (Input.Keyboard.CtrlOrCommand && !Game.Instance.IsMidTransition && Settings.EnableQuickStart) { From 82d615aae71c382f5dc7036732e61e0bc332edd0 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 03:29:42 +0200 Subject: [PATCH 09/29] Various small fixes --- Source/Data/Assets.cs | 8 ++++++++ Source/Mod/Core/ModLoader.cs | 13 +++++++++---- Source/Scenes/Startup.cs | 20 ++++++++------------ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 7a7e81d1..eff4ddbe 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -410,8 +410,16 @@ public static void Load() { var timer = Stopwatch.StartNew(); + // Purge any exisitng assets... Unload(); + /* + Refresh our instance of the vanilla mod. + Did you know that if vanilla isn't the first mod, trying to load assets throws a cryptic error? + Me neither, until recently. + */ + ModLoader.CreateNewVanilla(); + ModLoader.RegisterAllMods(); // Load vanilla assets first diff --git a/Source/Mod/Core/ModLoader.cs b/Source/Mod/Core/ModLoader.cs index 9fa9033e..16269b3d 100644 --- a/Source/Mod/Core/ModLoader.cs +++ b/Source/Mod/Core/ModLoader.cs @@ -44,10 +44,9 @@ public static string[] ModFolderPaths } } - internal static void RegisterAllMods() + internal static void CreateNewVanilla() { - FailedToLoadMods.Clear(); - if (ModManager.Instance.VanillaGameMod is null) ModManager.Instance.VanillaGameMod = new VanillaGameMod + ModManager.Instance.VanillaGameMod = new VanillaGameMod { // Mod Infos are required now, so make a dummy mod info for the valilla game too. This shouldn't really be used for anything. ModInfo = new ModInfo @@ -58,6 +57,12 @@ internal static void RegisterAllMods() }, Filesystem = new FolderModFilesystem(Assets.ContentPath) }; + } + + internal static void RegisterAllMods() + { + FailedToLoadMods.Clear(); + if (ModManager.Instance.VanillaGameMod is null) CreateNewVanilla(); Log.Info($"Loading mods from: \n- {String.Join("\n- ", ModFolderPaths)}"); @@ -110,7 +115,7 @@ internal static void RegisterAllMods() ModManager.Instance.Unload(); // Load vanilla as a mod, to unify all asset loading code - ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); + if (ModManager.Instance.VanillaGameMod is not null) ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); // We use an slightly silly approach to load all dependencies first: // Load all mods which have their dependencies met and repeat until we're done. diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 793fc5ce..0f383a9c 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -23,17 +23,8 @@ public Startup() // Register vanilla mod so that it can load its assets // Assume this will be overridden later - ModManager.Instance.VanillaGameMod = new VanillaGameMod - { - ModInfo = new ModInfo - { - Id = "Celeste64Vanilla", - Name = "Celeste 64: Fragments of the Mountain", - VersionString = "1.1.1", - }, - Filesystem = new FolderModFilesystem(Assets.ContentPath) - }; - ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); + ModLoader.CreateNewVanilla(); + if (ModManager.Instance.VanillaGameMod is not null) ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); // load save file { @@ -88,6 +79,7 @@ public override void Update() if (shouldGo && !Game.Instance.IsMidTransition) { Log.Info($"Loaded Assets in {timer.ElapsedMilliseconds}ms"); + ModManager.Instance.OnAssetsLoaded(); // enter game if (Input.Keyboard.CtrlOrCommand && !Game.Instance.IsMidTransition && Settings.EnableQuickStart) @@ -110,7 +102,7 @@ public override void Update() public override void Render(Target target) { - target.Clear(Color.Black); + target.Clear(Color.FromHexStringRGB("#282c42")); Batcher batcher = new(); Rect bounds = new(0, 0, target.Width, target.Height); @@ -127,6 +119,10 @@ public override void Render(Target target) } UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(4 * Game.RelativeScale, -28 * Game.RelativeScale), Vec2.Zero, Color.White); + if (areModsRegistered && assetQueueSize > 0) + { + batcher.Rect(new Rect(0, bounds.Bottom - (6 * Game.RelativeScale), target.Width / assetQueueSize * queueIndex, bounds.Bottom), Color.White); // Progress bar + } batcher.Render(target); batcher.Clear(); From a14dfff9bdad344efc6cd01c455f053483d3f55f Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Sat, 13 Apr 2024 03:42:52 +0200 Subject: [PATCH 10/29] Add documentation for functions, convert some functions to internal --- Source/Data/Assets.cs | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index eff4ddbe..ef85e515 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -107,6 +107,10 @@ private static void LoadDirectoryRecursive(GameMod mod, string folder, string } } + /// + /// Load a mod's assets. + /// + /// The mod to load public static void LoadMod(GameMod mod) { Log.Info($"Loading assets for {mod.ModInfo.Id}"); @@ -348,7 +352,10 @@ public static void LoadMod(GameMod mod) } } - public static void StageVanilla() + /// + /// Load the vanilla mod. + /// + internal static void StageVanilla() { GameMod? vanilla = ModManager.Instance.VanillaGameMod; @@ -363,7 +370,10 @@ public static void StageVanilla() Language.Current.Use(); } - public static void Unload() + /// + /// Unload all currently loaded assets. + /// + internal static void Unload() { Levels.Clear(); Maps.Clear(); @@ -380,14 +390,22 @@ public static void Unload() Map.ModActorFactories.Clear(); } - public static int FillLoadQueue() + /// + /// Fills the asset load queue with all enabled mods. + /// + /// The new length of the load queue. + internal static int FillLoadQueue() { loadQueue = ModManager.Instance.EnabledMods.Where(gm => gm is not VanillaGameMod).ToList(); return loadQueue.Count; } - public static bool MoveLoadQueue() + /// + /// Move the load queue forward by a step, loading one mod. + /// + /// True if a mod exists and was loaded, false if there's nothing left in the queue + internal static bool MoveLoadQueue() { if (loadQueue.Count < 1) return false; @@ -397,7 +415,10 @@ public static bool MoveLoadQueue() return true; } - public static void LoadAllQueued() + /// + /// Loads every mod in the queue. + /// + internal static void LoadAllQueued() { while (loadQueue.Count > 0) MoveLoadQueue(); } @@ -406,6 +427,9 @@ public static void LoadAllQueued() Asset loading was redone in 0.7.0. However, this function is preserved here. For compatibility and ease of use, it has the exact same behaviour on the outside as before. */ + /// + /// All-in-one function to purge assets, then re-register all mods, then load assets. + /// public static void Load() { var timer = Stopwatch.StartNew(); @@ -437,6 +461,12 @@ Refresh our instance of the vanilla mod. Log.Info($"Loaded Assets in {timer.ElapsedMilliseconds}ms"); } + /// + /// Convert a virtual path into a real path + /// + /// The virtual path + /// The folder type + /// A real path internal static string GetResourceNameFromVirt(string virtPath, string folder) { var ext = Path.GetExtension(virtPath); From 482fcd98688f4cc8164b05148c4865e932d6fdcc Mon Sep 17 00:00:00 2001 From: Jasmine Stephens Date: Sun, 14 Apr 2024 14:13:06 -0700 Subject: [PATCH 11/29] Cleaning up and simplifying things, fixing some small bugs I noticed that this PR is removing the way it was previously separating things into tasks and loading them after. I think that's fine, but since we're doing that, it actually lets us clean things up and simplify things a little more, so now instead of adding all the assets afterwards, we're able to just do it in place while loading the assets. I also removed the LoadDirectoryRecursive function, because it didn't really feel needed after simplifying. I did have to change the order a little to accommodate this, because it means shaders must be loaded before models. I also moved the modFs null check to the beginning, since every asset type was checking that, so it's simpler to just do that first. I also noticed a bug where because of these changes, every mod was adding the vanilla skin. I reworked this, so the vanilla skin is now just set up when we create the vanilla mod. I also fixed a minor bug, where I think we need to call `Language.Current.Use();` after all mods are loaded, in case any new characters are used that need to be registered. I also changed it to show the mod name during asset load, instead of the Id. Since this is something that's displayed to the player, I think using the mod name is cleaner here. Aside from that, I also made a few minor formatting and naming tweaks. --- Source/Data/Assets.cs | 272 +++++++++++++++-------------------- Source/Data/Language.cs | 2 +- Source/Mod/Core/ModLoader.cs | 24 +++- Source/Scenes/Startup.cs | 31 ++-- 4 files changed, 151 insertions(+), 178 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index ef85e515..d4b2b0b4 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -1,5 +1,4 @@ using Celeste64.Mod; -using System.Collections.Concurrent; using System.Diagnostics; using System.Text; using System.Text.Json; @@ -96,132 +95,153 @@ public static string ContentPath public static List Levels { get; private set; } = []; - private static List tasks = []; - public static List loadQueue = []; - - private static void LoadDirectoryRecursive(GameMod mod, string folder, string extension, ConcurrentBag into, Action> task) - { - foreach (var file in mod.Filesystem.FindFilesInDirectoryRecursive(folder, extension)) - { - task(file, mod, into); - } - } + internal static Queue LoadQueue = []; /// /// Load a mod's assets. /// /// The mod to load - public static void LoadMod(GameMod mod) + internal static void LoadAssetsForMod(GameMod mod) { Log.Info($"Loading assets for {mod.ModInfo.Id}"); - var maps = new ConcurrentBag<(Map, GameMod)>(); - var images = new ConcurrentBag<(string, Image, GameMod)>(); - var models = new ConcurrentBag<(string, SkinnedTemplate, GameMod)>(); - var sounds = new ConcurrentBag<(string, FMOD.Sound, GameMod)>(); - var music = new ConcurrentBag<(string, FMOD.Sound, GameMod)>(); - var langs = new ConcurrentBag<(Language, GameMod)>(); - tasks = []; - IModFilesystem modFs = mod.Filesystem; + if (modFs == null) + { + Log.Error($"Error loading assets for {mod.ModInfo.Id}. Mod FileSystem not initialized."); + return; + } + // Load maps - LoadDirectoryRecursive(mod, MapsFolder, MapsExtension, maps, (file, mod, maps) => + foreach (var file in modFs.FindFilesInDirectoryRecursive(MapsFolder, MapsExtension)) { // Skip the "autosave" folder - if (file.StartsWith($"{MapsFolder}/autosave", StringComparison.OrdinalIgnoreCase)) return; + if (file.StartsWith($"{MapsFolder}/autosave", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, + if (modFs.TryOpenFile(file, stream => new Map(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) { - maps.Add((map, mod)); + Maps.Add(map.Name, map, mod); } - }); + } // Load textures - LoadDirectoryRecursive(mod, TexturesFolder, TexturesExtension, images, (file, mod, images) => + foreach (var file in modFs.FindFilesInDirectoryRecursive(TexturesFolder, TexturesExtension)) { - if (mod.Filesystem != null && mod.Filesystem.TryLoadImage(file, out var image)) + if (modFs.TryLoadImage(file, out var image)) { - images.Add((GetResourceNameFromVirt(file, TexturesFolder), image, mod)); + string name = GetResourceNameFromVirt(file, TexturesFolder); + Textures.Add(name, new Texture(image) { Name = name }, mod); } - }); + } // Load character faces - LoadDirectoryRecursive(mod, FacesFolder, FacesExtension, images, (file, mod, images) => + foreach (var file in modFs.FindFilesInDirectoryRecursive(FacesFolder, FacesExtension)) + { + if (modFs.TryLoadImage(file, out var image)) + { + var name = $"faces/{GetResourceNameFromVirt(file, FacesFolder)}"; + Textures.Add(name, new Texture(image) { Name = name }, mod); + } + } + + // load glsl shaders + foreach (var file in modFs.FindFilesInDirectoryRecursive(ShadersFolder, ShadersExtension)) { - var name = $"faces/{GetResourceNameFromVirt(file, FacesFolder)}"; - if (mod.Filesystem != null && mod.Filesystem.TryLoadImage(file, out var image)) + if (modFs.TryOpenFile(file, stream => LoadShader(file, stream), out var shader)) { - images.Add((name, image, mod)); + shader.Name = GetResourceNameFromVirt(file, ShadersFolder); + Shaders.Add(shader.Name, shader, mod); } - }); + } + + // load font files + foreach (var file in modFs.FindFilesInDirectoryRecursive(FontsFolder, "")) + { + if (file.EndsWith($".{FontsExtensionTTF}") || file.EndsWith($".{FontsExtensionOTF}")) + { + if (modFs.TryOpenFile(file, stream => new Font(stream), out var font)) + Fonts.Add(GetResourceNameFromVirt(file, FontsFolder), font, mod); + } + } // Load glb models - LoadDirectoryRecursive(mod, ModelsFolder, ModelsExtension, models, (file, mod, models) => + foreach (var file in modFs.FindFilesInDirectoryRecursive(ModelsFolder, ModelsExtension)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => SharpGLTF.Schema2.ModelRoot.ReadGLB(stream), - out var input)) + if (modFs.TryOpenFile(file, stream => SharpGLTF.Schema2.ModelRoot.ReadGLB(stream), out var input)) { var model = new SkinnedTemplate(input); - models.Add((GetResourceNameFromVirt(file, ModelsFolder), model, mod)); + model.ConstructResources(); + Models.Add(GetResourceNameFromVirt(file, ModelsFolder), model, mod); } - }); + } // Load language files - LoadDirectoryRecursive(mod, TextFolder, TextExtension, langs, (file, mod, langs) => + foreach (var file in modFs.FindFilesInDirectoryRecursive(TextFolder, TextExtension)) { - if (mod.Filesystem != null && mod.Filesystem.TryLoadText(file, out var data)) + if (modFs.TryLoadText(file, out var data)) { if (JsonSerializer.Deserialize(data, LanguageContext.Default.Language) is { } lang) - langs.Add((lang, mod)); + { + if (Languages.TryGetValue(lang.ID, out var existing)) + { + existing.Absorb(lang, mod); + } + else + { + lang.OnCreate(mod); + Languages.Add(lang.ID, lang); + } + } } - }); + } // Load FMOD audio banks var allBankFiles = modFs.FindFilesInDirectoryRecursive(AudioFolder, AudioExtension).ToList(); // load strings first foreach (var file in allBankFiles) { - if (mod.Filesystem != null && file.EndsWith($".strings.{AudioExtension}")) - mod.Filesystem.TryOpenFile(file, Audio.LoadBankFromStream); + if (file.EndsWith($".strings.{AudioExtension}")) + modFs.TryOpenFile(file, Audio.LoadBankFromStream); } // load banks second foreach (var file in allBankFiles) { - if (mod.Filesystem != null && file.EndsWith($".{AudioExtension}") && !file.EndsWith($".strings.{AudioExtension}")) - mod.Filesystem.TryOpenFile(file, Audio.LoadBankFromStream); + if (file.EndsWith($".{AudioExtension}") && !file.EndsWith($".strings.{AudioExtension}")) + modFs.TryOpenFile(file, Audio.LoadBankFromStream); } - // Load wav sounds - LoadDirectoryRecursive(mod, SoundsFolder, SoundsExtension, sounds, (file, mod, sounds) => + // Load wav sounds - Fuji Custom + foreach (var file in modFs.FindFilesInDirectoryRecursive(SoundsFolder, SoundsExtension)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => Audio.LoadWavFromStream(stream), - out var sound)) + if (modFs.TryOpenFile(file, Audio.LoadWavFromStream, out var sound)) { if (sound != null) { - sounds.Add((GetResourceNameFromVirt(file, SoundsFolder), sound.Value, mod)); + Sounds.Add(GetResourceNameFromVirt(file, SoundsFolder), sound.Value, mod); } } - }); + } - // Load wav music - LoadDirectoryRecursive(mod, MusicFolder, MusicExtension, music, (file, mod, music) => + // Load wav music - Fuji Custom + foreach (var file in modFs.FindFilesInDirectoryRecursive(MusicFolder, MusicExtension)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => Audio.LoadWavFromStream(stream), - out var song)) + if (modFs.TryOpenFile(file, Audio.LoadWavFromStream, out var song)) { if (song != null) { - music.Add((GetResourceNameFromVirt(file, MusicFolder), song.Value, mod)); + Music.Add(GetResourceNameFromVirt(file, MusicFolder), song.Value, mod); } } - }); + } - // load level, dialog jsons + // load levels mod.Levels.Clear(); - if (modFs != null && modFs.TryOpenFile(LevelsJSON, + if (modFs.TryOpenFile(LevelsJSON, stream => JsonSerializer.Deserialize(stream, LevelInfoListContext.Default.ListLevelInfo) ?? [], out var levels)) { @@ -229,31 +249,7 @@ public static void LoadMod(GameMod mod) Levels.AddRange(levels); } - if (modFs != null) - { - // load glsl shaders - foreach (var file in modFs.FindFilesInDirectoryRecursive(ShadersFolder, ShadersExtension)) - { - if (modFs.TryOpenFile(file, stream => LoadShader(file, stream), out var shader)) - { - shader.Name = GetResourceNameFromVirt(file, ShadersFolder); - Shaders.Add(shader.Name, shader, mod); - } - } - - // load font files - foreach (var file in modFs.FindFilesInDirectoryRecursive(FontsFolder, "")) - { - if (file.EndsWith($".{FontsExtensionTTF}") || file.EndsWith($".{FontsExtensionOTF}")) - { - if (modFs.TryOpenFile(file, stream => new Font(stream), out var font)) - Fonts.Add(GetResourceNameFromVirt(file, FontsFolder), font, mod); - } - } - } - // pack sprites into single texture - if (modFs != null) { var packer = new Packer { @@ -284,70 +280,17 @@ public static void LoadMod(GameMod mod) } } - - // wait for tasks to finish + // Load Skins + foreach (var file in modFs.FindFilesInDirectoryRecursive(SkinsFolder, SkinsExtension)) { - foreach (var task in tasks) - task.Wait(); - - foreach (var (name, img, _) in images) - Textures.Add(name, new Texture(img) { Name = name }, mod); - foreach (var (map, _) in maps) - Maps.Add(map.Name, map, mod); - foreach (var (name, sound, _) in sounds) - Sounds.Add(name, sound, mod); - foreach (var (name, song, _) in music) - Music.Add(name, song, mod); - foreach (var (name, model, _) in models) + if (modFs.TryOpenFile(file, + stream => JsonSerializer.Deserialize(stream, SkinInfoContext.Default.SkinInfo), out var skin) && skin.IsValid()) { - model.ConstructResources(); - Models.Add(name, model, mod); + mod.Skins.Add(skin); } - foreach (var (lang, _) in langs) - { - if (Languages.TryGetValue(lang.ID, out var existing)) - { - existing.Absorb(lang, mod); - } - else - { - lang.OnCreate(mod); - Languages.Add(lang.ID, lang); - } - } - } - - // Load Skins - if (ModManager.Instance.VanillaGameMod != null) - { - ModManager.Instance.VanillaGameMod.Skins.Add( - new SkinInfo - { - Name = "Madeline", - Model = "player", - HideHair = false, - HairNormal = 0xdb2c00, - HairNoDash = 0x6ec0ff, - HairTwoDash = 0xfa91ff, - HairRefillFlash = 0xffffff, - HairFeather = 0xf2d450 - } - ); - } - - if (modFs != null) - { - foreach (var file in modFs.FindFilesInDirectoryRecursive(SkinsFolder, SkinsExtension)) + else { - if (modFs.TryOpenFile(file, - stream => JsonSerializer.Deserialize(stream, SkinInfoContext.Default.SkinInfo), out var skin) && skin.IsValid()) - { - mod.Skins.Add(skin); - } - else - { - Log.Warning($"Improperly configured skin: {file}"); - } + Log.Warning($"Improperly configured skin: {file}"); } } } @@ -355,7 +298,7 @@ public static void LoadMod(GameMod mod) /// /// Load the vanilla mod. /// - internal static void StageVanilla() + internal static void LoadVanillaMod() { GameMod? vanilla = ModManager.Instance.VanillaGameMod; @@ -364,7 +307,7 @@ internal static void StageVanilla() throw new Exception("Vanilla mod does not exist. This means something went horribly wrong!"); } - LoadMod(vanilla); + LoadAssetsForMod(vanilla); // make sure the active language is ready for use Language.Current.Use(); @@ -396,9 +339,9 @@ internal static void Unload() /// The new length of the load queue. internal static int FillLoadQueue() { - loadQueue = ModManager.Instance.EnabledMods.Where(gm => gm is not VanillaGameMod).ToList(); + LoadQueue = new Queue(ModManager.Instance.EnabledMods.Where(gm => gm is not VanillaGameMod)); - return loadQueue.Count; + return LoadQueue.Count; } /// @@ -407,10 +350,13 @@ internal static int FillLoadQueue() /// True if a mod exists and was loaded, false if there's nothing left in the queue internal static bool MoveLoadQueue() { - if (loadQueue.Count < 1) return false; + if (!LoadQueue.Any()) + { + return false; + } - LoadMod(loadQueue.First()); - loadQueue.RemoveAt(0); + LoadAssetsForMod(LoadQueue.First()); + LoadQueue.Dequeue(); return true; } @@ -420,7 +366,10 @@ internal static bool MoveLoadQueue() /// internal static void LoadAllQueued() { - while (loadQueue.Count > 0) MoveLoadQueue(); + while (LoadQueue.Any()) + { + MoveLoadQueue(); + } } /* @@ -434,7 +383,7 @@ public static void Load() { var timer = Stopwatch.StartNew(); - // Purge any exisitng assets... + // Purge any existing assets... Unload(); /* @@ -442,12 +391,12 @@ Refresh our instance of the vanilla mod. Did you know that if vanilla isn't the first mod, trying to load assets throws a cryptic error? Me neither, until recently. */ - ModLoader.CreateNewVanilla(); + ModLoader.CreateVanillaMod(); ModLoader.RegisterAllMods(); // Load vanilla assets first - StageVanilla(); + LoadVanillaMod(); FillLoadQueue(); @@ -467,14 +416,21 @@ Refresh our instance of the vanilla mod. /// The virtual path /// The folder type /// A real path - internal static string GetResourceNameFromVirt(string virtPath, string folder) + private static string GetResourceNameFromVirt(string virtPath, string folder) { var ext = Path.GetExtension(virtPath); // +1 to account for the forward slash return virtPath.AsSpan((folder.Length + 1)..^ext.Length).ToString(); } - internal static Shader? LoadShader(string virtPath, Stream file) + /// + /// Loads a shader from a file stream. + /// + /// The Virtual Path to the file. Used if the shader includes things from other files. + /// The File Stream for the file. + /// The shader that was loaded. + /// Throws if we try to include something that doesn't exist. + private static Shader? LoadShader(string virtPath, Stream file) { using var reader = new StreamReader(file); var code = reader.ReadToEnd(); diff --git a/Source/Data/Language.cs b/Source/Data/Language.cs index fd1d9a4e..132d8d4b 100644 --- a/Source/Data/Language.cs +++ b/Source/Data/Language.cs @@ -135,7 +135,7 @@ public void Use() HashSet codepoints = []; - // add ascii codepoinets always + // add ascii codepoints always for (int i = 32; i < 128; i++) codepoints.Add(i); diff --git a/Source/Mod/Core/ModLoader.cs b/Source/Mod/Core/ModLoader.cs index 16269b3d..6a81f801 100644 --- a/Source/Mod/Core/ModLoader.cs +++ b/Source/Mod/Core/ModLoader.cs @@ -44,25 +44,36 @@ public static string[] ModFolderPaths } } - internal static void CreateNewVanilla() + internal static void CreateVanillaMod() { ModManager.Instance.VanillaGameMod = new VanillaGameMod { - // Mod Infos are required now, so make a dummy mod info for the valilla game too. This shouldn't really be used for anything. + // Mod Infos are required now, so make a dummy mod info for the vanilla game too. This shouldn't really be used for anything. ModInfo = new ModInfo { Id = "Celeste64Vanilla", Name = "Celeste 64: Fragments of the Mountain", VersionString = "1.1.1", }, - Filesystem = new FolderModFilesystem(Assets.ContentPath) + Filesystem = new FolderModFilesystem(Assets.ContentPath), + Skins = + { + new SkinInfo + { + Name = "Madeline", + Model = "player" + } + } }; } internal static void RegisterAllMods() { FailedToLoadMods.Clear(); - if (ModManager.Instance.VanillaGameMod is null) CreateNewVanilla(); + if (ModManager.Instance.VanillaGameMod is null) + { + CreateVanillaMod(); + } Log.Info($"Loading mods from: \n- {String.Join("\n- ", ModFolderPaths)}"); @@ -115,7 +126,10 @@ internal static void RegisterAllMods() ModManager.Instance.Unload(); // Load vanilla as a mod, to unify all asset loading code - if (ModManager.Instance.VanillaGameMod is not null) ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); + if (ModManager.Instance.VanillaGameMod is not null) + { + ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); + } // We use an slightly silly approach to load all dependencies first: // Load all mods which have their dependencies met and repeat until we're done. diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 0f383a9c..9d057a10 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -6,14 +6,13 @@ namespace Celeste64; /// /// Creates a slight delay so the window looks OK before we load Assets -/// TODO: Would be nice if Foster could hide the Window till assets are ready. /// public class Startup : Scene { private int assetQueueSize; private int queueIndex = 0; private bool areModsRegistered = false; - private string lastLoadedId = string.Empty; + private string lastLoadedModName = string.Empty; private int delay = 5; private Stopwatch timer; @@ -23,8 +22,11 @@ public Startup() // Register vanilla mod so that it can load its assets // Assume this will be overridden later - ModLoader.CreateNewVanilla(); - if (ModManager.Instance.VanillaGameMod is not null) ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); + ModLoader.CreateVanillaMod(); + if (ModManager.Instance.VanillaGameMod is not null) + { + ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); + } // load save file { @@ -42,11 +44,7 @@ public Startup() } // load vanilla assets - Assets.StageVanilla(); - - // make sure the active language is ready for use, - // since the save file may have loaded a different language than default. - Language.Current.Use(); + Assets.LoadVanillaMod(); // try to load controls, or overwrite with defaults if they don't exist { @@ -72,17 +70,22 @@ public override void Update() } // load assets - lastLoadedId = Assets.loadQueue.Count > 0 ? Assets.loadQueue[0].ModInfo.Id : string.Empty; - bool shouldGo = !Assets.MoveLoadQueue(); + lastLoadedModName = Assets.LoadQueue.Any() ? + (Assets.LoadQueue.First().ModInfo.Name ?? Assets.LoadQueue.First().ModInfo.Id) + : string.Empty; + bool finishedLoading = !Assets.MoveLoadQueue(); queueIndex++; - if (shouldGo && !Game.Instance.IsMidTransition) + if (finishedLoading && !Game.Instance.IsMidTransition) { + // Update the current language after all mods have finished loading. + Language.Current.Use(); + Log.Info($"Loaded Assets in {timer.ElapsedMilliseconds}ms"); ModManager.Instance.OnAssetsLoaded(); // enter game - if (Input.Keyboard.CtrlOrCommand && !Game.Instance.IsMidTransition && Settings.EnableQuickStart) + if (Settings.EnableQuickStart && Input.Keyboard.CtrlOrCommand) { var entry = new Overworld.Entry(Assets.Levels[0], null); entry.Level.Enter(); @@ -115,7 +118,7 @@ public override void Render(Target target) } else { - loadInfo = String.Format(Loc.Str("FujiLoaderStatusNormal"), lastLoadedId, queueIndex, assetQueueSize); + loadInfo = String.Format(Loc.Str("FujiLoaderStatusNormal"), lastLoadedModName, queueIndex, assetQueueSize); } UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(4 * Game.RelativeScale, -28 * Game.RelativeScale), Vec2.Zero, Color.White); From defa77d907a1650dec0f049f7a992095e77d3465 Mon Sep 17 00:00:00 2001 From: Jasmine Stephens Date: Sun, 14 Apr 2024 15:33:36 -0700 Subject: [PATCH 12/29] Readding tasks so mod loading will be asynchronous again --- Source/Data/Assets.cs | 168 ++++++++++++++++++++++++++++-------------- 1 file changed, 111 insertions(+), 57 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index d4b2b0b4..602646e6 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -1,4 +1,5 @@ using Celeste64.Mod; +using System.Collections.Concurrent; using System.Diagnostics; using System.Text; using System.Text.Json; @@ -103,6 +104,14 @@ public static string ContentPath /// The mod to load internal static void LoadAssetsForMod(GameMod mod) { + var maps = new ConcurrentBag(); + var images = new ConcurrentBag<(string, Image)>(); + var models = new ConcurrentBag<(string, SkinnedTemplate)>(); + var sounds = new ConcurrentBag<(string, FMOD.Sound)>(); + var music = new ConcurrentBag<(string, FMOD.Sound)>(); + var langs = new ConcurrentBag(); + var tasks = new List(); + Log.Info($"Loading assets for {mod.ModInfo.Id}"); IModFilesystem modFs = mod.Filesystem; @@ -122,82 +131,97 @@ internal static void LoadAssetsForMod(GameMod mod) continue; } - if (modFs.TryOpenFile(file, - stream => new Map(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) + tasks.Add(Task.Run(() => { - Maps.Add(map.Name, map, mod); - } + if (modFs.TryOpenFile(file, + stream => new Map(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) + { + maps.Add(map); + } + })); } // Load textures foreach (var file in modFs.FindFilesInDirectoryRecursive(TexturesFolder, TexturesExtension)) { - if (modFs.TryLoadImage(file, out var image)) + tasks.Add(Task.Run(() => { - string name = GetResourceNameFromVirt(file, TexturesFolder); - Textures.Add(name, new Texture(image) { Name = name }, mod); - } + if (modFs.TryLoadImage(file, out var image)) + { + images.Add((GetResourceNameFromVirt(file, TexturesFolder), image)); + } + })); } // Load character faces foreach (var file in modFs.FindFilesInDirectoryRecursive(FacesFolder, FacesExtension)) { - if (modFs.TryLoadImage(file, out var image)) + tasks.Add(Task.Run(() => { - var name = $"faces/{GetResourceNameFromVirt(file, FacesFolder)}"; - Textures.Add(name, new Texture(image) { Name = name }, mod); - } + if (modFs.TryLoadImage(file, out var image)) + { + var name = $"faces/{GetResourceNameFromVirt(file, FacesFolder)}"; + images.Add((name, image)); + } + })); } - // load glsl shaders - foreach (var file in modFs.FindFilesInDirectoryRecursive(ShadersFolder, ShadersExtension)) + // Load glb models + foreach (var file in modFs.FindFilesInDirectoryRecursive(ModelsFolder, ModelsExtension)) { - if (modFs.TryOpenFile(file, stream => LoadShader(file, stream), out var shader)) + tasks.Add(Task.Run(() => { - shader.Name = GetResourceNameFromVirt(file, ShadersFolder); - Shaders.Add(shader.Name, shader, mod); - } + if (modFs.TryOpenFile(file, stream => SharpGLTF.Schema2.ModelRoot.ReadGLB(stream), out var input)) + { + var model = new SkinnedTemplate(input); + models.Add((GetResourceNameFromVirt(file, ModelsFolder), model)); + } + })); } - // load font files - foreach (var file in modFs.FindFilesInDirectoryRecursive(FontsFolder, "")) + // Load language files + foreach (var file in modFs.FindFilesInDirectoryRecursive(TextFolder, TextExtension)) { - if (file.EndsWith($".{FontsExtensionTTF}") || file.EndsWith($".{FontsExtensionOTF}")) + tasks.Add(Task.Run(() => { - if (modFs.TryOpenFile(file, stream => new Font(stream), out var font)) - Fonts.Add(GetResourceNameFromVirt(file, FontsFolder), font, mod); - } + if (modFs.TryLoadText(file, out var data)) + { + if (JsonSerializer.Deserialize(data, LanguageContext.Default.Language) is { } lang) + { + langs.Add(lang); + } + } + })); } - // Load glb models - foreach (var file in modFs.FindFilesInDirectoryRecursive(ModelsFolder, ModelsExtension)) + // Load wav sounds - Fuji Custom + foreach (var file in modFs.FindFilesInDirectoryRecursive(SoundsFolder, SoundsExtension)) { - if (modFs.TryOpenFile(file, stream => SharpGLTF.Schema2.ModelRoot.ReadGLB(stream), out var input)) + tasks.Add(Task.Run(() => { - var model = new SkinnedTemplate(input); - model.ConstructResources(); - Models.Add(GetResourceNameFromVirt(file, ModelsFolder), model, mod); - } + if (modFs.TryOpenFile(file, Audio.LoadWavFromStream, out var sound)) + { + if (sound != null) + { + sounds.Add((GetResourceNameFromVirt(file, SoundsFolder), sound.Value)); + } + } + })); } - // Load language files - foreach (var file in modFs.FindFilesInDirectoryRecursive(TextFolder, TextExtension)) + // Load wav music - Fuji Custom + foreach (var file in modFs.FindFilesInDirectoryRecursive(MusicFolder, MusicExtension)) { - if (modFs.TryLoadText(file, out var data)) + tasks.Add(Task.Run(() => { - if (JsonSerializer.Deserialize(data, LanguageContext.Default.Language) is { } lang) + if (modFs.TryOpenFile(file, Audio.LoadWavFromStream, out var song)) { - if (Languages.TryGetValue(lang.ID, out var existing)) + if (song != null) { - existing.Absorb(lang, mod); - } - else - { - lang.OnCreate(mod); - Languages.Add(lang.ID, lang); + music.Add((GetResourceNameFromVirt(file, MusicFolder), song.Value)); } } - } + })); } // Load FMOD audio banks @@ -215,27 +239,23 @@ internal static void LoadAssetsForMod(GameMod mod) modFs.TryOpenFile(file, Audio.LoadBankFromStream); } - // Load wav sounds - Fuji Custom - foreach (var file in modFs.FindFilesInDirectoryRecursive(SoundsFolder, SoundsExtension)) + // load glsl shaders + foreach (var file in modFs.FindFilesInDirectoryRecursive(ShadersFolder, ShadersExtension)) { - if (modFs.TryOpenFile(file, Audio.LoadWavFromStream, out var sound)) + if (modFs.TryOpenFile(file, stream => LoadShader(file, stream), out var shader)) { - if (sound != null) - { - Sounds.Add(GetResourceNameFromVirt(file, SoundsFolder), sound.Value, mod); - } + shader.Name = GetResourceNameFromVirt(file, ShadersFolder); + Shaders.Add(shader.Name, shader, mod); } } - // Load wav music - Fuji Custom - foreach (var file in modFs.FindFilesInDirectoryRecursive(MusicFolder, MusicExtension)) + // load font files + foreach (var file in modFs.FindFilesInDirectoryRecursive(FontsFolder, "")) { - if (modFs.TryOpenFile(file, Audio.LoadWavFromStream, out var song)) + if (file.EndsWith($".{FontsExtensionTTF}") || file.EndsWith($".{FontsExtensionOTF}")) { - if (song != null) - { - Music.Add(GetResourceNameFromVirt(file, MusicFolder), song.Value, mod); - } + if (modFs.TryOpenFile(file, stream => new Font(stream), out var font)) + Fonts.Add(GetResourceNameFromVirt(file, FontsFolder), font, mod); } } @@ -293,6 +313,40 @@ internal static void LoadAssetsForMod(GameMod mod) Log.Warning($"Improperly configured skin: {file}"); } } + + // wait for tasks to finish before adding them. + { + foreach (var task in tasks) + { + task.Wait(); + } + + foreach (var (name, img) in images) + Textures.Add(name, new Texture(img) { Name = name }, mod); + foreach (var map in maps) + Maps.Add(map.Name, map, mod); + foreach (var (name, sound) in sounds) + Sounds.Add(name, sound, mod); + foreach (var (name, song) in music) + Music.Add(name, song, mod); + foreach (var (name, model) in models) + { + model.ConstructResources(); + Models.Add(name, model, mod); + } + foreach (var lang in langs) + { + if (Languages.TryGetValue(lang.ID, out var existing)) + { + existing.Absorb(lang, mod); + } + else + { + lang.OnCreate(mod); + Languages.Add(lang.ID, lang); + } + } + } } /// From 5af6b5628bf3f4c875ee1ef4605c17f6999bfdbb Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:39:15 +0200 Subject: [PATCH 13/29] Add small delay on loading screen --- Source/Scenes/Startup.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 9d057a10..4e2e3e67 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -75,6 +75,12 @@ public override void Update() : string.Empty; bool finishedLoading = !Assets.MoveLoadQueue(); queueIndex++; + /* + We introduce a tiny bit of delay after each mod so the game has time to render to the screen. + This makes the loading screen look less choppy overall. Since the delay is so small, any impact + it has on speed should be negligible. + */ + delay = 2; if (finishedLoading && !Game.Instance.IsMidTransition) { From ee5369763bb7219e1ecddd11de8c1eda4c7fa90d Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Thu, 25 Apr 2024 19:26:57 +0200 Subject: [PATCH 14/29] New, better, faster hot-reloading system The main optimization here is that we only reload assets for the mod whose assets have been changed. This saves us plenty of time that we spent waiting for unnecessary asset reloads before. --- Source/Data/Assets.cs | 57 ++++++++---- Source/Data/LevelInfo.cs | 1 + Source/Data/Map.cs | 1 + Source/Game.cs | 55 +++++++++-- Source/Mod/Core/GameMod.cs | 2 + Source/Mod/Core/ModLoader.cs | 112 +++++++++++++---------- Source/Mod/Core/ModManager.cs | 4 +- Source/Mod/Helpers/ModAssetDictionary.cs | 16 +++- Source/Scenes/Overworld.cs | 4 +- Source/Scenes/World.cs | 2 +- 10 files changed, 173 insertions(+), 81 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 602646e6..fa1d3b97 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -1,4 +1,5 @@ using Celeste64.Mod; +using Microsoft.VisualBasic; using System.Collections.Concurrent; using System.Diagnostics; using System.Text; @@ -265,6 +266,10 @@ internal static void LoadAssetsForMod(GameMod mod) stream => JsonSerializer.Deserialize(stream, LevelInfoListContext.Default.ListLevelInfo) ?? [], out var levels)) { + foreach (LevelInfo level in levels) // Assign the mod id to level infos + { + level.ModId = mod.ModInfo.Id; + } mod.Levels.AddRange(levels); Levels.AddRange(levels); } @@ -368,23 +373,43 @@ internal static void LoadVanillaMod() } /// - /// Unload all currently loaded assets. + /// Unload currently loaded assets. + /// If a GameMod is passed, unload assets for that mod. + /// Otherwise, unload all assets. /// - internal static void Unload() + internal static void Unload(GameMod? mod) { - Levels.Clear(); - Maps.Clear(); - Shaders.Clear(); - Textures.Clear(); - Subtextures.Clear(); - Models.Clear(); - Fonts.Clear(); - Languages.Clear(); - Sounds.Clear(); - Music.Clear(); - Audio.Unload(); - - Map.ModActorFactories.Clear(); + if (mod == null) { Levels.Clear(); } + else + { + Levels = Levels.Where((LevelInfo levelInfo) => { return levelInfo.ModId != mod.ModInfo.Id; }).ToList(); + } + + Maps.Clear(mod); + Shaders.Clear(mod); + Textures.Clear(mod); + Subtextures.Clear(mod); + Models.Clear(mod); + Fonts.Clear(mod); + Sounds.Clear(mod); + Music.Clear(mod); + if (mod == null) Languages.Clear(); // Language files should upsert safely + if (mod == null) Audio.Unload(); // I don't have the patience to figure this out right now. + + if (mod == null) { Map.ModActorFactories.Clear(); } + else + /* + https://stackoverflow.com/a/2131680 + Remove mod actor factories owned by the specified mod. + */ + { + foreach (KeyValuePair kvp in Map.ModActorFactories.Where( + (kvp) => { return kvp.Value.Mod == mod; } + ).ToList()) + { + Map.ModActorFactories.Remove(kvp.Key); + } + } } /// @@ -438,7 +463,7 @@ public static void Load() var timer = Stopwatch.StartNew(); // Purge any existing assets... - Unload(); + Unload(null); /* Refresh our instance of the vanilla mod. diff --git a/Source/Data/LevelInfo.cs b/Source/Data/LevelInfo.cs index 1fd842f6..c2092cd1 100644 --- a/Source/Data/LevelInfo.cs +++ b/Source/Data/LevelInfo.cs @@ -13,6 +13,7 @@ public class LevelInfo public int Strawberries { get; set; } = 0; public string Preview { get; set; } = string.Empty; public string Map { get; set; } = string.Empty; + public string ModId { get; set; } = string.Empty; public void Enter(ScreenWipe? toBlack = null, float holdTime = 0) { diff --git a/Source/Data/Map.cs b/Source/Data/Map.cs index da4a1db7..f2829ad9 100644 --- a/Source/Data/Map.cs +++ b/Source/Data/Map.cs @@ -15,6 +15,7 @@ public class Map { public class ActorFactory(Func create) { + public GameMod? Mod; public bool UseSolidsAsBounds; public bool IsSolidGeometry; public Func Create = create; diff --git a/Source/Game.cs b/Source/Game.cs index 456d1a9f..8fb0a8ad 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -23,6 +23,7 @@ public enum Modes public bool Saving; public bool StopMusic; public bool PerformAssetReload; + public GameMod? ReloadFor; public float HoldOnBlackFor; } @@ -99,6 +100,7 @@ public static float ResolutionScale public World? World => Scene as World; internal bool NeedsReload = false; + internal bool doUpdate = true; public Game() { @@ -213,6 +215,8 @@ public override void Update() scenes.TryPeek(out var scene); // gets the top scene + if (!doUpdate) return; + // update top scene try { @@ -290,7 +294,41 @@ public override void Update() // reload assets if requested if (transition.PerformAssetReload) { - Assets.Load(); + if (transition.ReloadFor == null) + { + Assets.Load(); + } + else + { + GameMod reloadFor = transition.ReloadFor; + Log.Info($"Re-registering mod {reloadFor.ModInfo.Id}"); + + // Re-register the changed mod to refresh its modules + ModManager.Instance.DeregisterMod(reloadFor); + + IModFilesystem newFs; + if (reloadFor.Filesystem is ZipModFilesystem) + { + newFs = new ZipModFilesystem(reloadFor.Filesystem.Root); + } + else if (reloadFor.Filesystem is FolderModFilesystem) + { + newFs = new FolderModFilesystem(reloadFor.Filesystem.Root); + } + else + { + throw new Exception("Can't guess type of filesystem"); + } + + ModLoader.Load(reloadFor.ModInfo, newFs); + + GameMod reloadedMod = ModManager.Instance.Mods.First((mod) => { return mod.ModInfo.Id == reloadFor.ModInfo.Id; }); + + Assets.Unload(reloadFor); + Assets.LoadAssetsForMod(reloadedMod); + + Language.Current.Use(); + } } // perform transition @@ -316,7 +354,7 @@ public override void Update() if (scenes.TryPeek(out var nextScene)) { if (Settings.EnableAdditionalLogging) Log.Info("Switching scene: " + nextScene.GetType()); - + try { nextScene.Entered(); @@ -427,12 +465,12 @@ public override void Update() // reload state if (Input.Keyboard.Ctrl && Input.Keyboard.Pressed(Keys.R) && !IsMidTransition) { - ReloadAssets(); + ReloadAssets(null); } } } - internal void ReloadAssets() + internal void ReloadAssets(GameMod? mod) { if (!scenes.TryPeek(out var scene)) return; @@ -447,8 +485,8 @@ internal void ReloadAssets() Mode = Transition.Modes.Replace, Scene = () => new World(world.Entry), ToPause = true, - ToBlack = new AngledWipe(), - PerformAssetReload = true + PerformAssetReload = true, + ReloadFor = mod }); } else @@ -458,14 +496,15 @@ internal void ReloadAssets() Mode = Transition.Modes.Replace, Scene = () => new Titlescreen(), ToPause = true, - ToBlack = new AngledWipe(), - PerformAssetReload = true + PerformAssetReload = true, + ReloadFor = mod }); } } public override void Render() { + if (!doUpdate) return; Graphics.Clear(Color.Black); imGuiManager.RenderHandlers(); diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index c01bc8c4..f0a44622 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -496,6 +496,8 @@ public void EnableDependencies() /// public void AddActorFactory(string name, Map.ActorFactory factory) { + factory.Mod = this; + if (Map.ModActorFactories.TryAdd(name, factory)) { OnUnloadedCleanup += () => Map.ModActorFactories.Remove(name); diff --git a/Source/Mod/Core/ModLoader.cs b/Source/Mod/Core/ModLoader.cs index 6a81f801..582b4f9f 100644 --- a/Source/Mod/Core/ModLoader.cs +++ b/Source/Mod/Core/ModLoader.cs @@ -14,6 +14,8 @@ public static class ModLoader internal static List FailedToLoadMods = []; + internal static HashSet loaded = []; + public static string[] ModFolderPaths { get @@ -131,10 +133,10 @@ internal static void RegisterAllMods() ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); } - // We use an slightly silly approach to load all dependencies first: + // We use a slightly silly approach to load all dependencies first: // Load all mods which have their dependencies met and repeat until we're done. bool loadedModInIteration = false; - HashSet loaded = []; + loaded = []; // Sort the mods by their ID alphabetically before loading. // This helps us ensure some level of consistency/determinism to hopefully avoid quirks in behaviour. @@ -147,56 +149,13 @@ internal static void RegisterAllMods() { var (info, fs) = modInfos[i]; - bool dependenciesSatisfied = true; - foreach (var (modID, versionString) in info.Dependencies) - { - var version = new Version(versionString); - - if (loaded.FirstOrDefault(loadedInfo => loadedInfo.Id == modID) is { } dep && - dep.Version.Major == version.Major && - (dep.Version.Minor > version.Minor || - dep.Version.Minor == version.Minor && dep.Version.Build >= version.Build)) - { - continue; - } - - dependenciesSatisfied = false; - break; - } - - if (!dependenciesSatisfied) continue; - try { - var mod = LoadGameMod(info, fs); - mod.Filesystem?.AssociateWithMod(mod); - - try - { - ModManager.Instance.RegisterMod(mod); - - // Load hooks after the mod has been registered - foreach (var type in mod.GetType().Assembly.GetTypes()) - { - FindAndRegisterHooks(info, type); - } - } - catch - { - // Perform cleanup - ModManager.Instance.DeregisterMod(mod); - HookManager.Instance.ClearHooksOfMod(info); - throw; - } - - loaded.Add(info); - loadedModInIteration = true; + loadedModInIteration = Load(info, fs); } - catch (Exception ex) + catch { - FailedToLoadMods.Add(info.Id); - Log.Error($"Fuji Error: An error occurred while trying to load mod: {info.Id}"); - Log.Error(ex.ToString()); + continue; } modInfos.RemoveAt(i); @@ -241,6 +200,63 @@ internal static void RegisterAllMods() Log.Info(modListString); } + internal static bool Load(ModInfo info, IModFilesystem fs) + { + bool dependenciesSatisfied = true; + foreach (var (modID, versionString) in info.Dependencies) + { + var version = new Version(versionString); + + if (loaded.FirstOrDefault(loadedInfo => loadedInfo.Id == modID) is { } dep && + dep.Version.Major == version.Major && + (dep.Version.Minor > version.Minor || + dep.Version.Minor == version.Minor && dep.Version.Build >= version.Build)) + { + continue; + } + + dependenciesSatisfied = false; + break; + } + + if (!dependenciesSatisfied) throw new Exception(); + + try + { + var mod = LoadGameMod(info, fs); + mod.Filesystem?.AssociateWithMod(mod); + + try + { + ModManager.Instance.RegisterMod(mod); + + // Load hooks after the mod has been registered + foreach (var type in mod.GetType().Assembly.GetTypes()) + { + FindAndRegisterHooks(info, type); + } + } + catch + { + // Perform cleanup + ModManager.Instance.DeregisterMod(mod); + HookManager.Instance.ClearHooksOfMod(info); + throw; + } + + loaded.Add(info); + return true; + } + catch (Exception ex) + { + FailedToLoadMods.Add(info.Id); + Log.Error($"Fuji Error: An error occurred while trying to load mod: {info.Id}"); + Log.Error(ex.ToString()); + + return false; + } + } + private static ModInfo? LoadModInfo(string modFolder, IModFilesystem fs) { if (!fs.TryOpenFile(Assets.FujiJSON, stream => JsonSerializer.Deserialize(stream, ModInfoContext.Default.ModInfo), out var info)) diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index 87eb1f3f..1c5b0538 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -84,7 +84,7 @@ internal void DeregisterMod(GameMod mod) } mod.OnUnloadedCleanup?.Invoke(); - + mod.ModInfo.AssemblyContext?.Dispose(); } @@ -127,7 +127,7 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) Log.Info($"Mod archive for mod {ctx.Mod.ModInfo.Name} changed. Reloading assets."); } - Game.Instance.ReloadAssets(); + Game.Instance.ReloadAssets(ctx.Mod); } internal void Update(float deltaTime) diff --git a/Source/Mod/Helpers/ModAssetDictionary.cs b/Source/Mod/Helpers/ModAssetDictionary.cs index 8190b29a..8461d0fc 100644 --- a/Source/Mod/Helpers/ModAssetDictionary.cs +++ b/Source/Mod/Helpers/ModAssetDictionary.cs @@ -9,13 +9,21 @@ public class ModAssetDictionary(ModAssetDictionary.GetDictionary getDictio public delegate Dictionary GetDictionary(GameMod mod); /// - /// Clear out all the assets of this type for every mod. + /// Clear out all assets from this dictionary. + /// If a GameMod is passed as the first argument, only clear assets from that mod. /// - public void Clear() + public void Clear(GameMod? target) { - foreach (var mod in ModManager.Instance.Mods) + if (target != null) { - getDictionary(mod).Clear(); + getDictionary(target).Clear(); + } + else + { + foreach (var mod in ModManager.Instance.Mods) + { + getDictionary(mod).Clear(); + } } } diff --git a/Source/Scenes/Overworld.cs b/Source/Scenes/Overworld.cs index 3880d2db..38cf5a8e 100644 --- a/Source/Scenes/Overworld.cs +++ b/Source/Scenes/Overworld.cs @@ -363,7 +363,7 @@ public override void Update() if (Game.Instance.NeedsReload) { Game.Instance.NeedsReload = false; - Game.Instance.ReloadAssets(); + Game.Instance.ReloadAssets(null); } Paused = false; })); @@ -445,7 +445,7 @@ public override void Update() if (Game.Instance.NeedsReload) { Game.Instance.NeedsReload = false; - Game.Instance.ReloadAssets(); + Game.Instance.ReloadAssets(null); } Audio.Play(Sfx.ui_unpause); Paused = false; diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 9a238731..d8955fee 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -526,7 +526,7 @@ public void SetPaused(bool paused) if (Game.Instance.NeedsReload) { Game.Instance.NeedsReload = false; - Game.Instance.ReloadAssets(); + Game.Instance.ReloadAssets(null); } var ply = Get(); From de75540843331cf42607d742880b2e79a8bf0f88 Mon Sep 17 00:00:00 2001 From: axiand Date: Thu, 25 Apr 2024 19:32:08 +0200 Subject: [PATCH 15/29] visual basic!? --- Source/Data/Assets.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index fa1d3b97..8e428c47 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -1,5 +1,4 @@ using Celeste64.Mod; -using Microsoft.VisualBasic; using System.Collections.Concurrent; using System.Diagnostics; using System.Text; From 2342fef186341a87e628eae051a0a271380acf57 Mon Sep 17 00:00:00 2001 From: Jasmine Stephens Date: Thu, 25 Apr 2024 14:42:32 -0700 Subject: [PATCH 16/29] Changing to only load changed mods when a mod is enabled/disabled or a setting is changed. Previously if a mod was enabled or disabled, or if a mod setting was changed that was marked as needing reload, it would always reload all mods. But because of these recent changes, we can change that to only reload mods that were actually touched, which should make that reload happen faster. I also changed it so disabled mods don't trigger a hot reload if a file is changed, since that file isn't going to be loaded anyways. And I also added some code to clear out hooks on reload as well. --- Source/Game.cs | 73 ++++++++++++++++++++-------------- Source/Mod/Core/GameMod.cs | 3 +- Source/Mod/Core/ModLoader.cs | 10 +++-- Source/Mod/Core/ModManager.cs | 11 +++-- Source/Mod/Menu/ModInfoMenu.cs | 2 +- Source/Scenes/Overworld.cs | 11 +++-- Source/Scenes/World.cs | 7 ++-- 7 files changed, 68 insertions(+), 49 deletions(-) diff --git a/Source/Game.cs b/Source/Game.cs index 8fb0a8ad..f1f07852 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -23,7 +23,7 @@ public enum Modes public bool Saving; public bool StopMusic; public bool PerformAssetReload; - public GameMod? ReloadFor; + public bool ReloadAll; public float HoldOnBlackFor; } @@ -99,7 +99,6 @@ public static float ResolutionScale public Scene? Scene => scenes.TryPeek(out var scene) ? scene : null; public World? World => Scene as World; - internal bool NeedsReload = false; internal bool doUpdate = true; public Game() @@ -294,38 +293,50 @@ public override void Update() // reload assets if requested if (transition.PerformAssetReload) { - if (transition.ReloadFor == null) + if (transition.ReloadAll) { Assets.Load(); } else { - GameMod reloadFor = transition.ReloadFor; - Log.Info($"Re-registering mod {reloadFor.ModInfo.Id}"); - - // Re-register the changed mod to refresh its modules - ModManager.Instance.DeregisterMod(reloadFor); - - IModFilesystem newFs; - if (reloadFor.Filesystem is ZipModFilesystem) - { - newFs = new ZipModFilesystem(reloadFor.Filesystem.Root); - } - else if (reloadFor.Filesystem is FolderModFilesystem) + List modsToReload = ModManager.Instance.Mods.Where(mod => mod.NeedsReload).ToList(); + foreach (var modToReload in modsToReload) { - newFs = new FolderModFilesystem(reloadFor.Filesystem.Root); + Log.Info($"Re-registering mod {modToReload.ModInfo.Id}"); + + // Re-register the changed mod to refresh its modules + ModManager.Instance.DeregisterMod(modToReload); + HookManager.Instance.ClearHooksOfMod(modToReload.ModInfo); + + IModFilesystem newFs; + if (modToReload.Filesystem is ZipModFilesystem) + { + newFs = new ZipModFilesystem(modToReload.Filesystem.Root); + } + else if (modToReload.Filesystem is FolderModFilesystem) + { + newFs = new FolderModFilesystem(modToReload.Filesystem.Root); + } + else + { + throw new Exception("Can't determine type of filesystem"); + } + + Assets.Unload(modToReload); + ModLoader.Load(modToReload.ModInfo, newFs); + + if (modToReload.Enabled) + { + GameMod reloadedMod = ModManager.Instance.Mods.First((mod) => { return mod.ModInfo.Id == modToReload.ModInfo.Id; }); + + Assets.LoadAssetsForMod(reloadedMod); + } + + modToReload.NeedsReload = false; } - else - { - throw new Exception("Can't guess type of filesystem"); - } - - ModLoader.Load(reloadFor.ModInfo, newFs); - - GameMod reloadedMod = ModManager.Instance.Mods.First((mod) => { return mod.ModInfo.Id == reloadFor.ModInfo.Id; }); - Assets.Unload(reloadFor); - Assets.LoadAssetsForMod(reloadedMod); + // Re-sort mods after loading. + ModManager.Instance.Mods = ModManager.Instance.Mods.OrderBy(mod => mod.ModInfo.Id).ToList(); Language.Current.Use(); } @@ -465,12 +476,12 @@ public override void Update() // reload state if (Input.Keyboard.Ctrl && Input.Keyboard.Pressed(Keys.R) && !IsMidTransition) { - ReloadAssets(null); + ReloadAssets(true); } } } - internal void ReloadAssets(GameMod? mod) + internal void ReloadAssets(bool reloadAll) { if (!scenes.TryPeek(out var scene)) return; @@ -485,8 +496,9 @@ internal void ReloadAssets(GameMod? mod) Mode = Transition.Modes.Replace, Scene = () => new World(world.Entry), ToPause = true, + ToBlack = new AngledWipe(), PerformAssetReload = true, - ReloadFor = mod + ReloadAll = reloadAll }); } else @@ -496,8 +508,9 @@ internal void ReloadAssets(GameMod? mod) Mode = Transition.Modes.Replace, Scene = () => new Titlescreen(), ToPause = true, + ToBlack = new AngledWipe(), PerformAssetReload = true, - ReloadFor = mod + ReloadAll = reloadAll }); } } diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index f0a44622..2224a563 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -26,6 +26,7 @@ public abstract class GameMod internal readonly Dictionary>> DialogLines = new(StringComparer.OrdinalIgnoreCase); internal readonly List Levels = new(); internal bool Loaded = false; + internal bool NeedsReload = false; /// /// Cleanup tasks that have to be performed when this mod gets unloaded. @@ -414,7 +415,7 @@ public virtual void OnModSettingChanged(string settingName, object? newValue, bo { if (needsReload) { - Game.Instance.NeedsReload = true; + NeedsReload = true; } } diff --git a/Source/Mod/Core/ModLoader.cs b/Source/Mod/Core/ModLoader.cs index 582b4f9f..18a4e811 100644 --- a/Source/Mod/Core/ModLoader.cs +++ b/Source/Mod/Core/ModLoader.cs @@ -230,10 +230,14 @@ internal static bool Load(ModInfo info, IModFilesystem fs) { ModManager.Instance.RegisterMod(mod); - // Load hooks after the mod has been registered - foreach (var type in mod.GetType().Assembly.GetTypes()) + // Only Reload Hooks for mods not part of the current assembly. + if (mod.GetType().Assembly != Assembly.GetExecutingAssembly()) { - FindAndRegisterHooks(info, type); + // Load hooks after the mod has been registered + foreach (var type in mod.GetType().Assembly.GetTypes()) + { + FindAndRegisterHooks(info, type); + } } } catch diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index 1c5b0538..6d4ace0a 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -21,6 +21,8 @@ private ModManager() { } internal GameMod? CurrentLevelMod { get; set; } + internal bool NeedsReload => Mods.Any(mod => mod.NeedsReload); + internal void Unload() { _modFilesystemCleanupTimerToken.Cancel(); @@ -56,11 +58,12 @@ internal void RegisterMod(GameMod mod) { Mods.Add(mod); GlobalFilesystem.Add(mod); - if (mod.Filesystem != null) - mod.Filesystem.OnFileChanged += OnModFileChanged; if (mod.Enabled) { + if (mod.Filesystem != null) + mod.Filesystem.OnFileChanged += OnModFileChanged; + mod.OnModLoaded(); mod.Loaded = true; } @@ -126,8 +129,8 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) { Log.Info($"Mod archive for mod {ctx.Mod.ModInfo.Name} changed. Reloading assets."); } - - Game.Instance.ReloadAssets(ctx.Mod); + ctx.Mod.NeedsReload = true; + Game.Instance.ReloadAssets(false); } internal void Update(float deltaTime) diff --git a/Source/Mod/Menu/ModInfoMenu.cs b/Source/Mod/Menu/ModInfoMenu.cs index e7c05506..6b3dc010 100644 --- a/Source/Mod/Menu/ModInfoMenu.cs +++ b/Source/Mod/Menu/ModInfoMenu.cs @@ -84,7 +84,7 @@ private void InitItems() } } - Game.Instance.NeedsReload = true; + Mod.NeedsReload = true; } else { diff --git a/Source/Scenes/Overworld.cs b/Source/Scenes/Overworld.cs index 38cf5a8e..bfc0f597 100644 --- a/Source/Scenes/Overworld.cs +++ b/Source/Scenes/Overworld.cs @@ -360,11 +360,11 @@ public override void Update() pauseMenu.Add(new Menu.Submenu("PauseModsMenu", pauseMenu, modMenu)); pauseMenu.Add(new Menu.Option("Exit", () => { - if (Game.Instance.NeedsReload) + if (ModManager.Instance.NeedsReload) { - Game.Instance.NeedsReload = false; - Game.Instance.ReloadAssets(null); + Game.Instance.ReloadAssets(false); } + Paused = false; })); @@ -442,10 +442,9 @@ public override void Update() { pauseMenu.CloseSubMenus(); } - if (Game.Instance.NeedsReload) + if (ModManager.Instance.NeedsReload) { - Game.Instance.NeedsReload = false; - Game.Instance.ReloadAssets(null); + Game.Instance.ReloadAssets(false); } Audio.Play(Sfx.ui_unpause); Paused = false; diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index d8955fee..be46e224 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -167,7 +167,7 @@ public World(EntryInfo entry) FromPause = true, ToPause = true, ToBlack = new SlideWipe(), - PerformAssetReload = Game.Instance.NeedsReload, + PerformAssetReload = ModManager.Instance.NeedsReload, Saving = true }))); } @@ -523,10 +523,9 @@ public void SetPaused(bool paused) if (paused == false) { - if (Game.Instance.NeedsReload) + if (ModManager.Instance.NeedsReload) { - Game.Instance.NeedsReload = false; - Game.Instance.ReloadAssets(null); + Game.Instance.ReloadAssets(false); } var ply = Get(); From 71c8dfa40ef059c87fa4dcc20a7974aaa17a222e Mon Sep 17 00:00:00 2001 From: Jasmine Stephens Date: Thu, 25 Apr 2024 17:37:30 -0700 Subject: [PATCH 17/29] Setting up to better handle reloading dependencies, now that we can reload single mods Mod dependencies should now be reloaded as well when a mod is changed through hot reloading or through the settings. It also should get handled better when a mod is enabled or disabled and the dependents and dependencies get disabled/enabled as well. It should load the dependencies similarly to how it does during initial load, so dependencies should get loaded before the mods that depend on them. I also updated it so enabling and disabling mods should recursively affect dependencies and dependents. Previously, it only went 1 level up or down. --- Source/Game.cs | 45 +++++++++++------------------ Source/Mod/Core/GameMod.cs | 53 ++++++++++++++++++++++++++++++++-- Source/Mod/Core/ModLoader.cs | 35 ++++++++++++++++++++++ Source/Mod/Core/ModManager.cs | 2 +- Source/Mod/Menu/ModInfoMenu.cs | 14 ++++----- 5 files changed, 110 insertions(+), 39 deletions(-) diff --git a/Source/Game.cs b/Source/Game.cs index f1f07852..010280e1 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -299,40 +299,29 @@ public override void Update() } else { - List modsToReload = ModManager.Instance.Mods.Where(mod => mod.NeedsReload).ToList(); - foreach (var modToReload in modsToReload) - { - Log.Info($"Re-registering mod {modToReload.ModInfo.Id}"); - - // Re-register the changed mod to refresh its modules - ModManager.Instance.DeregisterMod(modToReload); - HookManager.Instance.ClearHooksOfMod(modToReload.ModInfo); + List modsToReload = ModManager.Instance.Mods + .Where(mod => mod.NeedsReload) + .OrderBy(mod => mod.ModInfo.Id) + .ToList(); - IModFilesystem newFs; - if (modToReload.Filesystem is ZipModFilesystem) - { - newFs = new ZipModFilesystem(modToReload.Filesystem.Root); - } - else if (modToReload.Filesystem is FolderModFilesystem) - { - newFs = new FolderModFilesystem(modToReload.Filesystem.Root); - } - else + while (modsToReload.Count > 0) + { + bool loadedModThisIteration = false; + for (int i = modsToReload.Count - 1; i >= 0; i--) { - throw new Exception("Can't determine type of filesystem"); + // Only Reload this mod when all of it's dependencies are already loaded + if (!modsToReload[i].GetDependencies().Any(mod => mod.NeedsReload)) + { + ModLoader.ReloadChangedMod(modsToReload[i]); + loadedModThisIteration = true; + modsToReload.Remove(modsToReload[i]); + } } - Assets.Unload(modToReload); - ModLoader.Load(modToReload.ModInfo, newFs); - - if (modToReload.Enabled) + if (!loadedModThisIteration) { - GameMod reloadedMod = ModManager.Instance.Mods.First((mod) => { return mod.ModInfo.Id == modToReload.ModInfo.Id; }); - - Assets.LoadAssetsForMod(reloadedMod); + throw new Exception($"Could not reload {modsToReload.Count} mods due to dependencies not reloading properly."); } - - modToReload.NeedsReload = false; } // Re-sort mods after loading. diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index 2224a563..84ae1b29 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -28,6 +28,19 @@ public abstract class GameMod internal bool Loaded = false; internal bool NeedsReload = false; + internal void SetNeedsReloadRecursive() + { + NeedsReload = true; + + foreach (var dependent in GetDependents()) + { + if (!dependent.NeedsReload) + { + dependent.SetNeedsReloadRecursive(); + } + } + } + /// /// Cleanup tasks that have to be performed when this mod gets unloaded. /// Called after , not inside of it, @@ -415,7 +428,7 @@ public virtual void OnModSettingChanged(string settingName, object? newValue, bo { if (needsReload) { - NeedsReload = true; + SetNeedsReloadRecursive(); } } @@ -439,6 +452,24 @@ public List GetDependents() return depMods; } + /// + /// Get all mods which depend on this mod. + /// + internal List GetDependencies() + { + var depMods = new List(); + + foreach (var mod in ModManager.Instance.Mods) + { + if (ModInfo.Dependencies.ContainsKey(mod.ModInfo.Id)) + { + depMods.Add(mod); + } + } + + return depMods; + } + /// /// Disables the mod "safely" (accounts for dependent mods, etc.) /// If it returns true, this means it is not safe to disable the mod. @@ -454,7 +485,15 @@ public bool DisableSafe(bool simulate) { if (!simulate) { - ModSettings.GetOrMakeModSettings(dependent.ModInfo.Id).Enabled = false; + var modSettings = ModSettings.TryGetModSettings(dependent.ModInfo.Id); + if (modSettings != null && modSettings.Enabled) + { + modSettings.Enabled = false; + + var mod = ModManager.Instance.Mods.First(mod => mod.ModInfo.Id == dependent.ModInfo.Id); + mod.NeedsReload = true; + mod.DisableSafe(simulate); + } } if (dependent == ModManager.Instance.CurrentLevelMod) @@ -485,7 +524,15 @@ public void EnableDependencies() { foreach (var dep in ModInfo.Dependencies.Keys.ToList()) { - ModSettings.GetOrMakeModSettings(dep).Enabled = true; + var modSettings = ModSettings.TryGetModSettings(dep); + if (modSettings != null && !modSettings.Enabled) + { + modSettings.Enabled = true; + + var mod = ModManager.Instance.Mods.First(mod => mod.ModInfo.Id == dep); + mod.NeedsReload = true; + mod.EnableDependencies(); + } } } diff --git a/Source/Mod/Core/ModLoader.cs b/Source/Mod/Core/ModLoader.cs index 18a4e811..43e94bff 100644 --- a/Source/Mod/Core/ModLoader.cs +++ b/Source/Mod/Core/ModLoader.cs @@ -261,6 +261,41 @@ internal static bool Load(ModInfo info, IModFilesystem fs) } } + internal static void ReloadChangedMod(GameMod mod) + { + Log.Info($"Re-registering mod {mod.ModInfo.Id}"); + + // Re-register the changed mod to refresh its modules + ModManager.Instance.DeregisterMod(mod); + HookManager.Instance.ClearHooksOfMod(mod.ModInfo); + + IModFilesystem newFs; + if (mod.Filesystem is ZipModFilesystem) + { + newFs = new ZipModFilesystem(mod.Filesystem.Root); + } + else if (mod.Filesystem is FolderModFilesystem) + { + newFs = new FolderModFilesystem(mod.Filesystem.Root); + } + else + { + throw new Exception("Can't determine type of filesystem"); + } + + Assets.Unload(mod); + Load(mod.ModInfo, newFs); + + if (mod.Enabled) + { + GameMod reloadedMod = ModManager.Instance.Mods.First((modIterator) => { return modIterator.ModInfo.Id == mod.ModInfo.Id; }); + + Assets.LoadAssetsForMod(reloadedMod); + } + + mod.NeedsReload = false; + } + private static ModInfo? LoadModInfo(string modFolder, IModFilesystem fs) { if (!fs.TryOpenFile(Assets.FujiJSON, stream => JsonSerializer.Deserialize(stream, ModInfoContext.Default.ModInfo), out var info)) diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index 6d4ace0a..ba4b33f0 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -129,7 +129,7 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) { Log.Info($"Mod archive for mod {ctx.Mod.ModInfo.Name} changed. Reloading assets."); } - ctx.Mod.NeedsReload = true; + ctx.Mod.SetNeedsReloadRecursive(); Game.Instance.ReloadAssets(false); } diff --git a/Source/Mod/Menu/ModInfoMenu.cs b/Source/Mod/Menu/ModInfoMenu.cs index 6b3dc010..4ef7d6b8 100644 --- a/Source/Mod/Menu/ModInfoMenu.cs +++ b/Source/Mod/Menu/ModInfoMenu.cs @@ -39,11 +39,14 @@ private void InitItems() //If we are trying to disable the current mod, don't if (Mod != null && Mod != ModManager.Instance.CurrentLevelMod) { - ModSettings.GetOrMakeModSettings(Mod.ModInfo.Id).Enabled = !ModSettings.GetOrMakeModSettings(Mod.ModInfo.Id).Enabled; + var modSettings = ModSettings.GetOrMakeModSettings(Mod.ModInfo.Id); + modSettings.Enabled = !modSettings.Enabled; + Mod.NeedsReload = true; - if (ModSettings.GetOrMakeModSettings(Mod.ModInfo.Id).Enabled) + if (modSettings.Enabled) { Mod.EnableDependencies(); // Also enable dependencies of the mod being enabled (if any). + Mod.SetNeedsReloadRecursive(); } else { @@ -52,7 +55,7 @@ private void InitItems() safeDisableErrorMenu = new Menu { Title = Loc.Str("ModSafeDisableErrorMessage") }; safeDisableErrorMenu.Add(new Option("Exit", () => { - ModSettings.GetOrMakeModSettings(Mod.ModInfo.Id).Enabled = true; // Override the toggle if the operation can't be done. + modSettings.Enabled = true; // Override the toggle if the operation can't be done. PopRootSubMenu(); })); @@ -62,7 +65,6 @@ private void InitItems() return; } - if (Mod.GetDependents().Count > 0) { depWarningMenu = new Menu { Title = $"Warning, this mod is depended on by {Mod.GetDependents().Count} other mod(s).\nIf you disable this mod, those mods will also be disabled." }; @@ -75,7 +77,7 @@ private void InitItems() })); depWarningMenu.Add(new Option("Exit", () => { - ModSettings.GetOrMakeModSettings(Mod.ModInfo.Id).Enabled = true; // Override the toggle if the operation was cancelled. + modSettings.Enabled = true; // Override the toggle if the operation was cancelled. RootMenu?.PopSubMenu(); })); @@ -83,8 +85,6 @@ private void InitItems() RootMenu?.PushSubMenu(depWarningMenu); } } - - Mod.NeedsReload = true; } else { From 13a62ed6189df81e01d1dbdcbf375a5710914fc5 Mon Sep 17 00:00:00 2001 From: Jasmine Stephens Date: Thu, 25 Apr 2024 18:02:14 -0700 Subject: [PATCH 18/29] Adding simple splash screen :) --- Content/Textures/Overworld/splashscreen.png | Bin 0 -> 28099 bytes Source/Scenes/Startup.cs | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 Content/Textures/Overworld/splashscreen.png diff --git a/Content/Textures/Overworld/splashscreen.png b/Content/Textures/Overworld/splashscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..c4f3a11098306d3e124c7ee3ccffc251e2b40e11 GIT binary patch literal 28099 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 literal 0 HcmV?d00001 diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 4e2e3e67..22efc7dd 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -126,6 +126,10 @@ public override void Render(Target target) { loadInfo = String.Format(Loc.Str("FujiLoaderStatusNormal"), lastLoadedModName, queueIndex, assetQueueSize); } + if (Assets.Textures.TryGetValue("overworld/splashscreen", out Texture? splashTexture)) + { + batcher.Image(splashTexture, bounds.TopLeft, bounds.TopRight, bounds.BottomRight, bounds.BottomLeft, new Vec2(0, 0), new Vec2(1, 0), new Vec2(1, 1), new Vec2(0, 1), Color.White); + } UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(4 * Game.RelativeScale, -28 * Game.RelativeScale), Vec2.Zero, Color.White); if (areModsRegistered && assetQueueSize > 0) From dbe41701475e4405e6dc23cb64cf38ac6019800b Mon Sep 17 00:00:00 2001 From: axiand Date: Fri, 26 Apr 2024 15:34:38 +0200 Subject: [PATCH 19/29] remove doUpdate --- Source/Game.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Game.cs b/Source/Game.cs index 010280e1..20722b8e 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -99,8 +99,6 @@ public static float ResolutionScale public Scene? Scene => scenes.TryPeek(out var scene) ? scene : null; public World? World => Scene as World; - internal bool doUpdate = true; - public Game() { if (IsDynamicRes) From e4258cf2fb296c4196ef1d3445f2f1f493e65a20 Mon Sep 17 00:00:00 2001 From: axiand Date: Fri, 26 Apr 2024 15:36:05 +0200 Subject: [PATCH 20/29] remove remaining code related to doUpdate --- Source/Game.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Source/Game.cs b/Source/Game.cs index 20722b8e..15887d4e 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -212,8 +212,6 @@ public override void Update() scenes.TryPeek(out var scene); // gets the top scene - if (!doUpdate) return; - // update top scene try { @@ -504,7 +502,6 @@ internal void ReloadAssets(bool reloadAll) public override void Render() { - if (!doUpdate) return; Graphics.Clear(Color.Black); imGuiManager.RenderHandlers(); From ae11dbe6f36ea79bf0204d10dd94986ceaf09b02 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:12:31 +0200 Subject: [PATCH 21/29] Implement keybind to initiate manual reload --- Source/Game.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Game.cs b/Source/Game.cs index 15887d4e..a2dab28b 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -197,6 +197,13 @@ private void HandleError(Exception e) public override void Update() { + if (Input.Keyboard.Pressed(Keys.F5)) + { + if (Scene is Startup || Scene is GameErrorMessage) return; + + ReloadAssets(Input.Keyboard.CtrlOrCommand); // F5 - Reload changed; Ctrl + F5 - Reload all + } + if (IsDynamicRes) { if (Height_old != Height || Width_old != Width) From 5447a91281ea3e1e781926e53beaa497c25a67e6 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:16:17 +0200 Subject: [PATCH 22/29] Fix issue where vanilla mod wasn't always first after reloading --- Source/Game.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Game.cs b/Source/Game.cs index a2dab28b..8e04704c 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -328,7 +328,10 @@ public override void Update() } // Re-sort mods after loading. - ModManager.Instance.Mods = ModManager.Instance.Mods.OrderBy(mod => mod.ModInfo.Id).ToList(); + ModManager.Instance.Mods = ModManager.Instance.Mods + .OrderBy(mod => mod.ModInfo.Id) // Alphabetical + .OrderBy(mod => !(mod is VanillaGameMod)) // Put the vanilla mod first + .ToList(); Language.Current.Use(); } From f1c0d8d210ee34160361237976c723ed143bdbfc Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:29:20 +0200 Subject: [PATCH 23/29] Adjust splash screen, display version string on startup --- Content/Textures/Overworld/splashscreen.png | Bin 28099 -> 30275 bytes Source/Game.cs | 5 +++++ Source/Scenes/Startup.cs | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/Content/Textures/Overworld/splashscreen.png b/Content/Textures/Overworld/splashscreen.png index c4f3a11098306d3e124c7ee3ccffc251e2b40e11..1ca6cb13657574a81c593eaf5363b562921f00d0 100644 GIT binary patch literal 30275 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
8RLI6m!~ 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
5$GJps%5O(HIU0 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 diff --git a/Source/Game.cs b/Source/Game.cs index 8e04704c..57fa7953 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -117,6 +117,11 @@ public Game() imGuiManager = new ImGuiManager(); } + public string GetFullVersionString() + { + return $"{VersionString} + {LoaderVersion}"; + } + public void SetResolutionScale(int scale) { ResolutionScale = scale; diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 22efc7dd..d44c8b35 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -132,6 +132,11 @@ public override void Render(Target target) } UI.Text(batcher, loadInfo, bounds.BottomLeft + new Vec2(4 * Game.RelativeScale, -28 * Game.RelativeScale), Vec2.Zero, Color.White); + + batcher.PushMatrix(Matrix3x2.CreateScale(0.75f)); + UI.Text(batcher, Game.Instance.GetFullVersionString(), bounds.TopLeft + new Vec2(4 * Game.RelativeScale, 4 * Game.RelativeScale), Vec2.Zero, Color.LightGray); + batcher.PopMatrix(); + if (areModsRegistered && assetQueueSize > 0) { batcher.Rect(new Rect(0, bounds.Bottom - (6 * Game.RelativeScale), target.Width / assetQueueSize * queueIndex, bounds.Bottom), Color.White); // Progress bar From cde9481fa9d8039ab71fb7c3ac71ed3b36b929f1 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:38:34 +0200 Subject: [PATCH 24/29] Implement option to toggle automatic hot reload --- Content/Text/English.json | 2 ++ Source/Data/PersistedData/Settings_V01.cs | 7 ++++++- Source/Data/Settings.cs | 11 +++++++++++ Source/Mod/Core/ModManager.cs | 4 ++-- Source/Mod/Menu/GameOptionsMenu.cs | 1 + 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Content/Text/English.json b/Content/Text/English.json index 44ad5721..c327b4b8 100644 --- a/Content/Text/English.json +++ b/Content/Text/English.json @@ -65,6 +65,8 @@ "FujiEnableDebugMenu.desc": "Enable access to the Debug Menu by pressing F6", "FujiAdditionalLog": "Enable Additional Logging", "FujiAdditionalLog.desc": "Outputs more info to the console which may be useful to developers", + "FujiAutoReload": "Enable Auto Reload", + "FujiAutoReload.desc": "If enabled, automatically reloads mods whenever a file is changed", "FujiFailedToLoad": "Failed to load the following mods:", "FujiOpenLogFile": "Open Log File", "FujiOpenLogFile.desc": "Open the Game's Log File for more information", diff --git a/Source/Data/PersistedData/Settings_V01.cs b/Source/Data/PersistedData/Settings_V01.cs index 52fae45d..101a6fa7 100644 --- a/Source/Data/PersistedData/Settings_V01.cs +++ b/Source/Data/PersistedData/Settings_V01.cs @@ -64,12 +64,17 @@ public sealed class Settings_V01 : PersistedData /// Fuji Custom - The Current Game Resolution Scale. ///
public int ResolutionScale { get; set; } = 1; - + /// /// Fuji Custom - Whether the QuickStart feature is enabled /// public bool EnableQuickStart { get; set; } = true; + /// + /// Fuji Custom - Whether to enable automatic hot reload on file change + /// + public bool EnableAutoReload { get; set; } = true; + public override JsonTypeInfo GetTypeInfo() { diff --git a/Source/Data/Settings.cs b/Source/Data/Settings.cs index bafea07e..d083c3bf 100644 --- a/Source/Data/Settings.cs +++ b/Source/Data/Settings.cs @@ -76,6 +76,11 @@ public sealed class Settings ///
public static bool EnableQuickStart => Instance.EnableQuickStart; + /// + /// Fuji Custom - Whether auto hot reload is enabled + /// + public static bool EnableAutoReload => Instance.EnableAutoReload; + public static void ToggleFullscreen() { Instance.Fullscreen = !Instance.Fullscreen; @@ -92,6 +97,12 @@ public static void ToggleEnableAdditionalLogs() Instance.EnableAdditionalLogging = !EnableAdditionalLogging; } + public static void ToggleEnableAutoReload() + { + Instance.EnableAutoReload = !EnableAutoReload; + } + + public static void ToggleEnableDebugMenu() { Instance.EnableDebugMenu = !Instance.EnableDebugMenu; diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index ba4b33f0..83f89817 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -117,7 +117,7 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) filepath.ToLower() == Assets.LevelsJSON.ToLower() || filepath.ToLower() == Assets.FujiJSON.ToLower()) { - Log.Info($"File Changed: {filepath} (From mod {ctx.Mod.ModInfo.Name}). Reloading assets."); + Log.Info($"File Changed: {filepath} (From mod {ctx.Mod.ModInfo.Name}). {(Settings.EnableAutoReload ? "Reloading assets." : "Queued for reload.")}"); } else { @@ -130,7 +130,7 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) Log.Info($"Mod archive for mod {ctx.Mod.ModInfo.Name} changed. Reloading assets."); } ctx.Mod.SetNeedsReloadRecursive(); - Game.Instance.ReloadAssets(false); + if (Settings.EnableAutoReload) Game.Instance.ReloadAssets(false); } internal void Update(float deltaTime) diff --git a/Source/Mod/Menu/GameOptionsMenu.cs b/Source/Mod/Menu/GameOptionsMenu.cs index 0997da00..7600b848 100644 --- a/Source/Mod/Menu/GameOptionsMenu.cs +++ b/Source/Mod/Menu/GameOptionsMenu.cs @@ -22,6 +22,7 @@ public GameOptionsMenu(Menu? rootMenu) FujiOptionsMenu.Add(new Slider("OptionsResolution", 1, 5, () => Settings.ResolutionScale, Settings.SetResolutionScale)); FujiOptionsMenu.Add(new Toggle("OptionsQuickStart", Settings.ToggleQuickStart, () => Settings.EnableQuickStart)); FujiOptionsMenu.Add(new Toggle("FujiAdditionalLog", Settings.ToggleEnableAdditionalLogs, () => Settings.EnableAdditionalLogging)); + FujiOptionsMenu.Add(new Toggle("FujiAutoReload", Settings.ToggleEnableAutoReload, () => Settings.EnableAutoReload)); FujiOptionsMenu.Add(new Option("Exit", () => { PopSubMenu(); From f2362db6cf33fa3ceca0b9e0ee56f7c3a6a05d88 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:39:39 +0200 Subject: [PATCH 25/29] Change one more string --- Source/Mod/Core/ModManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index 83f89817..cc6cff08 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -127,7 +127,7 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) } else { - Log.Info($"Mod archive for mod {ctx.Mod.ModInfo.Name} changed. Reloading assets."); + Log.Info($"Mod archive for mod {ctx.Mod.ModInfo.Name} changed. {(Settings.EnableAutoReload ? "Reloading assets." : "Queued for reload.")}"); } ctx.Mod.SetNeedsReloadRecursive(); if (Settings.EnableAutoReload) Game.Instance.ReloadAssets(false); From b373f6dfefd875827de3c842fd9924fbdd2840a9 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:41:54 +0200 Subject: [PATCH 26/29] Log when a manual reload is initiated --- Source/Game.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Game.cs b/Source/Game.cs index 57fa7953..ffaa6474 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -206,6 +206,7 @@ public override void Update() { if (Scene is Startup || Scene is GameErrorMessage) return; + Log.Info($"--- User has initiated a{(Input.Keyboard.CtrlOrCommand ? " full" : String.Empty)} reload. ---"); ReloadAssets(Input.Keyboard.CtrlOrCommand); // F5 - Reload changed; Ctrl + F5 - Reload all } From 1ec4be64b8042a60ed4f4aedc09b33eefb923ab3 Mon Sep 17 00:00:00 2001 From: Axi <98420477+axiand@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:49:08 +0200 Subject: [PATCH 27/29] Improve logging for mod reloads --- Source/Game.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Source/Game.cs b/Source/Game.cs index ffaa6474..e2d60d88 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -206,7 +206,7 @@ public override void Update() { if (Scene is Startup || Scene is GameErrorMessage) return; - Log.Info($"--- User has initiated a{(Input.Keyboard.CtrlOrCommand ? " full" : String.Empty)} reload. ---"); + Log.Info($"--- User has initiated a{(Input.Keyboard.CtrlOrCommand ? " full" : String.Empty)} manual reload. ---"); ReloadAssets(Input.Keyboard.CtrlOrCommand); // F5 - Reload changed; Ctrl + F5 - Reload all } @@ -313,6 +313,19 @@ public override void Update() .OrderBy(mod => mod.ModInfo.Id) .ToList(); + if (Settings.EnableAdditionalLogging) + { + StringBuilder reloadList = new(); + + reloadList.Append($"Reloading {modsToReload.Count} mods: "); + foreach (GameMod mod in modsToReload) + { + reloadList.Append($"\n- {mod.ModInfo.Id}"); + } + + Log.Info(reloadList); + } + while (modsToReload.Count > 0) { bool loadedModThisIteration = false; From b75a99e8585798c338fc1546ad2ea87c718f96ef Mon Sep 17 00:00:00 2001 From: axiand Date: Fri, 26 Apr 2024 23:31:53 +0200 Subject: [PATCH 28/29] Make full version string multiline --- Source/Game.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Game.cs b/Source/Game.cs index 56d3e30c..2b7b52f2 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -119,7 +119,7 @@ public Game() public string GetFullVersionString() { - return $"{VersionString} + {LoaderVersion}"; + return $"{VersionString}\n{LoaderVersion}"; } public void SetResolutionScale(int scale) From 22f53ea558c251f1d2946a6b67f89e8d297d5bcb Mon Sep 17 00:00:00 2001 From: axiand Date: Fri, 26 Apr 2024 23:36:08 +0200 Subject: [PATCH 29/29] Remove ctrl+r in favour of new F5 reload key --- Source/Game.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Source/Game.cs b/Source/Game.cs index 2b7b52f2..e133e15f 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -202,7 +202,7 @@ private void HandleError(Exception e) public override void Update() { - if (Input.Keyboard.Pressed(Keys.F5)) + if (Input.Keyboard.Pressed(Keys.F5) && !IsMidTransition) { if (Scene is Startup || Scene is GameErrorMessage) return; @@ -486,12 +486,6 @@ public override void Update() // toggle fullsrceen if ((Input.Keyboard.Alt && Input.Keyboard.Pressed(Keys.Enter)) || Input.Keyboard.Pressed(Keys.F4)) Settings.ToggleFullscreen(); - - // reload state - if (Input.Keyboard.Ctrl && Input.Keyboard.Pressed(Keys.R) && !IsMidTransition) - { - ReloadAssets(true); - } } }