diff --git a/Content/Text/English.json b/Content/Text/English.json index 4056e45b..d57bb2b4 100644 --- a/Content/Text/English.json +++ b/Content/Text/English.json @@ -67,12 +67,16 @@ "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", "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})", "FujiOpenUserPath": "Open Game Folder", "FujiOpenUserPath.desc": "Open the user folder for Fuji", "FailedToLoadMods": "Failed to load {0} mods. Check Mods Menu for details.", diff --git a/Content/Textures/Overworld/splashscreen.png b/Content/Textures/Overworld/splashscreen.png new file mode 100644 index 00000000..1ca6cb13 Binary files /dev/null and b/Content/Textures/Overworld/splashscreen.png differ diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 50537df1..8e428c47 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -96,176 +96,153 @@ public static string ContentPath public static List Levels { get; private set; } = []; - public static void Load() + internal static Queue LoadQueue = []; + + /// + /// Load a mod's assets. + /// + /// The mod to load + internal static void LoadAssetsForMod(GameMod mod) { - var timer = Stopwatch.StartNew(); + 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(); - 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(); - ModLoader.RegisterAllMods(); + 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)>(); - var tasks = new List(); + IModFilesystem modFs = mod.Filesystem; - // NOTE: Make sure to update ModManager.OnModFileChanged() as well, for hot-reloading to work! + if (modFs == null) + { + Log.Error($"Error loading assets for {mod.ModInfo.Id}. Mod FileSystem not initialized."); + return; + } - var globalFs = ModManager.Instance.GlobalFilesystem; - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(MapsFolder, MapsExtension)) + // Load maps + foreach (var file in modFs.FindFilesInDirectoryRecursive(MapsFolder, MapsExtension)) { // Skip the "autosave" folder if (file.StartsWith($"{MapsFolder}/autosave", StringComparison.OrdinalIgnoreCase)) + { continue; + } tasks.Add(Task.Run(() => { - 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); } })); } - // load texture pngs - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(TexturesFolder, TexturesExtension)) + // Load textures + foreach (var file in modFs.FindFilesInDirectoryRecursive(TexturesFolder, TexturesExtension)) { tasks.Add(Task.Run(() => { - 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)); + images.Add((GetResourceNameFromVirt(file, TexturesFolder), image)); } })); } - // load faces - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(FacesFolder, FacesExtension)) + // Load character faces + foreach (var file in modFs.FindFilesInDirectoryRecursive(FacesFolder, FacesExtension)) { tasks.Add(Task.Run(() => { - var name = $"faces/{GetResourceNameFromVirt(file, FacesFolder)}"; - if (mod.Filesystem != null && mod.Filesystem.TryLoadImage(file, out var image)) + if (modFs.TryLoadImage(file, out var image)) { - images.Add((name, image, mod)); + var name = $"faces/{GetResourceNameFromVirt(file, FacesFolder)}"; + images.Add((name, image)); } })); } - // load glb models - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(ModelsFolder, ModelsExtension)) + // Load glb models + foreach (var file in modFs.FindFilesInDirectoryRecursive(ModelsFolder, ModelsExtension)) { tasks.Add(Task.Run(() => { - 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)); + models.Add((GetResourceNameFromVirt(file, ModelsFolder), model)); } })); } - // load languages - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(TextFolder, TextExtension)) + // Load language files + foreach (var file in modFs.FindFilesInDirectoryRecursive(TextFolder, TextExtension)) { tasks.Add(Task.Run(() => { - 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)); + { + langs.Add(lang); + } } })); } - // load audio - var allBankFiles = globalFs.FindFilesInDirectoryRecursiveWithMod(AudioFolder, AudioExtension).ToList(); - // load strings first - foreach (var (file, mod) 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) - { - 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 - Fuji Custom + foreach (var file in modFs.FindFilesInDirectoryRecursive(SoundsFolder, SoundsExtension)) { tasks.Add(Task.Run(() => { - 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)); } } })); } - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(MusicFolder, MusicExtension)) + // Load wav music - Fuji Custom + foreach (var file in modFs.FindFilesInDirectoryRecursive(MusicFolder, MusicExtension)) { tasks.Add(Task.Run(() => { - 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)); } } })); } - // load level, dialog jsons - foreach (var mod in ModManager.Instance.Mods) + // Load FMOD audio banks + var allBankFiles = modFs.FindFilesInDirectoryRecursive(AudioFolder, AudioExtension).ToList(); + // load strings first + foreach (var file in allBankFiles) { - 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); - // } - // } + if (file.EndsWith($".strings.{AudioExtension}")) + modFs.TryOpenFile(file, Audio.LoadBankFromStream); + } + // load banks second + foreach (var file in allBankFiles) + { + if (file.EndsWith($".{AudioExtension}") && !file.EndsWith($".strings.{AudioExtension}")) + modFs.TryOpenFile(file, Audio.LoadBankFromStream); } // load glsl shaders - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(ShadersFolder, ShadersExtension)) + foreach (var file in modFs.FindFilesInDirectoryRecursive(ShadersFolder, ShadersExtension)) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => LoadShader(file, stream), out var shader)) + if (modFs.TryOpenFile(file, stream => LoadShader(file, stream), out var shader)) { shader.Name = GetResourceNameFromVirt(file, ShadersFolder); Shaders.Add(shader.Name, shader, mod); @@ -273,15 +250,29 @@ public static void Load() } // load font files - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(FontsFolder, "")) + foreach (var file in modFs.FindFilesInDirectoryRecursive(FontsFolder, "")) { if (file.EndsWith($".{FontsExtensionTTF}") || file.EndsWith($".{FontsExtensionOTF}")) { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, stream => new Font(stream), out var font)) + if (modFs.TryOpenFile(file, stream => new Font(stream), out var font)) Fonts.Add(GetResourceNameFromVirt(file, FontsFolder), font, mod); } } + // load levels + mod.Levels.Clear(); + if (modFs.TryOpenFile(LevelsJSON, + 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); + } + // pack sprites into single texture { var packer = new Packer @@ -290,9 +281,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,34 +300,45 @@ 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); } } + // Load Skins + foreach (var file in modFs.FindFilesInDirectoryRecursive(SkinsFolder, SkinsExtension)) + { + 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}"); + } + } - // wait for tasks to finish + // wait for tasks to finish before adding them. { 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)) { @@ -349,54 +351,164 @@ public static void Load() } } } + } - // Load Skins - if (ModManager.Instance.VanillaGameMod != null) + /// + /// Load the vanilla mod. + /// + internal static void LoadVanillaMod() + { + GameMod? vanilla = ModManager.Instance.VanillaGameMod; + + if (vanilla is null) { - ModManager.Instance.VanillaGameMod.Skins.Add( - new SkinInfo - { - Name = "Madeline", - Model = "player", - HideHair = false, - HairNormal = 0xdb2c00, - HairNoDash = 0x6ec0ff, - HairTwoDash = 0xfa91ff, - HairRefillFlash = 0xffffff, - HairFeather = 0xf2d450 - } - ); + throw new Exception("Vanilla mod does not exist. This means something went horribly wrong!"); } - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(SkinsFolder, SkinsExtension)) + LoadAssetsForMod(vanilla); + + // make sure the active language is ready for use + Language.Current.Use(); + } + + /// + /// Unload currently loaded assets. + /// If a GameMod is passed, unload assets for that mod. + /// Otherwise, unload all assets. + /// + internal static void Unload(GameMod? mod) + { + if (mod == null) { Levels.Clear(); } + else { - 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 + 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()) { - Log.Warning($"Improperly configured skin: {file}"); + Map.ModActorFactories.Remove(kvp.Key); } } + } - // make sure the active language is ready for use - Language.Current.Use(); + /// + /// Fills the asset load queue with all enabled mods. + /// + /// The new length of the load queue. + internal static int FillLoadQueue() + { + LoadQueue = new Queue(ModManager.Instance.EnabledMods.Where(gm => gm is not VanillaGameMod)); + + return LoadQueue.Count; + } + + /// + /// 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.Any()) + { + return false; + } + + LoadAssetsForMod(LoadQueue.First()); + LoadQueue.Dequeue(); + + return true; + } + + /// + /// Loads every mod in the queue. + /// + internal static void LoadAllQueued() + { + while (LoadQueue.Any()) + { + 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. + */ + /// + /// All-in-one function to purge assets, then re-register all mods, then load assets. + /// + public static void Load() + { + var timer = Stopwatch.StartNew(); + + // Purge any existing assets... + Unload(null); + + /* + 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.CreateVanillaMod(); + + ModLoader.RegisterAllMods(); + + // Load vanilla assets first + LoadVanillaMod(); + + 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 + LoadAllQueued(); ModManager.Instance.OnAssetsLoaded(); Log.Info($"Loaded Assets in {timer.ElapsedMilliseconds}ms"); } - internal static string GetResourceNameFromVirt(string virtPath, string folder) + /// + /// Convert a virtual path into a real path + /// + /// The virtual path + /// The folder type + /// A real path + 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/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/Data/PersistedData/Settings_V01.cs b/Source/Data/PersistedData/Settings_V01.cs index ac0f17eb..626bac4e 100644 --- a/Source/Data/PersistedData/Settings_V01.cs +++ b/Source/Data/PersistedData/Settings_V01.cs @@ -75,6 +75,11 @@ public sealed class Settings_V01 : PersistedData /// 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 83e0be5d..1bd5ac5c 100644 --- a/Source/Data/Settings.cs +++ b/Source/Data/Settings.cs @@ -81,6 +81,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; @@ -97,6 +102,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/Game.cs b/Source/Game.cs index 1d308be9..e133e15f 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 bool ReloadAll; public float HoldOnBlackFor; } @@ -98,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 NeedsReload = false; - public Game() { if (IsDynamicRes) @@ -118,6 +117,11 @@ public Game() imGuiManager = new ImGuiManager(); } + public string GetFullVersionString() + { + return $"{VersionString}\n{LoaderVersion}"; + } + public void SetResolutionScale(int scale) { ResolutionScale = scale; @@ -198,6 +202,14 @@ private void HandleError(Exception e) public override void Update() { + if (Input.Keyboard.Pressed(Keys.F5) && !IsMidTransition) + { + if (Scene is Startup || Scene is GameErrorMessage) return; + + 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 + } + if (IsDynamicRes) { if (Height_old != Height || Width_old != Width) @@ -290,7 +302,58 @@ public override void Update() // reload assets if requested if (transition.PerformAssetReload) { - Assets.Load(); + if (transition.ReloadAll) + { + Assets.Load(); + } + else + { + List modsToReload = ModManager.Instance.Mods + .Where(mod => mod.NeedsReload) + .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; + for (int i = modsToReload.Count - 1; i >= 0; i--) + { + // 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]); + } + } + + if (!loadedModThisIteration) + { + throw new Exception($"Could not reload {modsToReload.Count} mods due to dependencies not reloading properly."); + } + } + + // Re-sort mods after loading. + 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(); + } } // perform transition @@ -423,16 +486,10 @@ 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(); - } } } - internal void ReloadAssets() + internal void ReloadAssets(bool reloadAll) { if (!scenes.TryPeek(out var scene)) return; @@ -448,7 +505,8 @@ internal void ReloadAssets() Scene = () => new World(world.Entry), ToPause = true, ToBlack = new AngledWipe(), - PerformAssetReload = true + PerformAssetReload = true, + ReloadAll = reloadAll }); } else @@ -459,7 +517,8 @@ internal void ReloadAssets() Scene = () => new Titlescreen(), ToPause = true, ToBlack = new AngledWipe(), - PerformAssetReload = true + PerformAssetReload = true, + ReloadAll = reloadAll }); } } diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index 70fbfc10..3b263120 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -26,6 +26,20 @@ public abstract class GameMod internal readonly Dictionary>> DialogLines = new(StringComparer.OrdinalIgnoreCase); internal readonly List Levels = new(); 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. @@ -414,7 +428,7 @@ public virtual void OnModSettingChanged(string settingName, object? newValue, bo { if (needsReload) { - Game.Instance.NeedsReload = true; + SetNeedsReloadRecursive(); } } @@ -438,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. @@ -453,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) @@ -484,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(); + } } } @@ -496,6 +544,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 c0f0b48e..43e94bff 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 @@ -44,20 +46,36 @@ public static string[] ModFolderPaths } } - internal static void RegisterAllMods() + internal static void CreateVanillaMod() { - FailedToLoadMods.Clear(); 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) + { + CreateVanillaMod(); + } Log.Info($"Loading mods from: \n- {String.Join("\n- ", ModFolderPaths)}"); @@ -110,12 +128,15 @@ 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: + // 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. @@ -128,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); @@ -222,6 +200,102 @@ 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); + + // Only Reload Hooks for mods not part of the current assembly. + if (mod.GetType().Assembly != Assembly.GetExecutingAssembly()) + { + // 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; + } + } + + 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)) @@ -319,7 +393,7 @@ private static GameMod LoadGameMod(ModInfo info, IModFilesystem fs) private static void FindAndRegisterHooks(ModInfo modInfo, Type type) { List hooks = []; - + try { // On. hooks @@ -327,20 +401,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 +427,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/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index 359ea4c2..77a10635 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; } @@ -114,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 { @@ -124,10 +127,10 @@ 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.")}"); } - - Game.Instance.ReloadAssets(); + ctx.Mod.SetNeedsReloadRecursive(); + if (Settings.EnableAutoReload) Game.Instance.ReloadAssets(false); } 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/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/Mod/Menu/GameOptionsMenu.cs b/Source/Mod/Menu/GameOptionsMenu.cs index bf0741fb..80a133d3 100644 --- a/Source/Mod/Menu/GameOptionsMenu.cs +++ b/Source/Mod/Menu/GameOptionsMenu.cs @@ -24,6 +24,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 Spacer()); FujiOptionsMenu.Add(new Option("FujiOpenUserPath", () => { diff --git a/Source/Mod/Menu/ModInfoMenu.cs b/Source/Mod/Menu/ModInfoMenu.cs index e7c05506..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); } } - - Game.Instance.NeedsReload = true; } else { diff --git a/Source/Scenes/Overworld.cs b/Source/Scenes/Overworld.cs index 3880d2db..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(); + 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(); + Game.Instance.ReloadAssets(false); } Audio.Play(Sfx.ui_unpause); Paused = false; diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs index 8d46e5dd..e7822a69 100644 --- a/Source/Scenes/Startup.cs +++ b/Source/Scenes/Startup.cs @@ -1,17 +1,33 @@ +using Celeste64.Mod; using Celeste64.Mod.Data; +using System.Diagnostics; 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 loadDelay = 5; + private int assetQueueSize; + private int queueIndex = 0; + private bool areModsRegistered = false; + private string lastLoadedModName = string.Empty; + private int delay = 5; + private Stopwatch timer; - private void BeginGame() + public Startup() { + timer = Stopwatch.StartNew(); + + // Register vanilla mod so that it can load its assets + // Assume this will be overridden later + ModLoader.CreateVanillaMod(); + if (ModManager.Instance.VanillaGameMod is not null) + { + ModManager.Instance.RegisterMod(ModManager.Instance.VanillaGameMod); + } + // load save file { SaveManager.Instance.LoadSaveByFileName(SaveManager.Instance.GetLastLoadedSave()); @@ -27,53 +43,108 @@ 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(); - - // make sure the active language is ready for use, - // since the save file may have loaded a different language than default. - Language.Current.Use(); + // load vanilla assets + Assets.LoadVanillaMod(); // try to load controls, or overwrite with defaults if they don't exist { Controls.LoadControlsByFileName(Controls.DefaultFileName); } + } + + public override void Update() + { + if (delay > 0) + { + delay--; + return; + } - // TODO: Move me once asset redo is merged!! - App.VSync = Settings.VSync; - - // enter game - //Assets.Levels[0].Enter(new AngledWipe()); - if (Input.Keyboard.CtrlOrCommand && !Game.Instance.IsMidTransition && Settings.EnableQuickStart) + 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 + lastLoadedModName = Assets.LoadQueue.Any() ? + (Assets.LoadQueue.First().ModInfo.Name ?? Assets.LoadQueue.First().ModInfo.Id) + : 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) { - Game.Instance.Goto(new Transition() + App.VSync = Settings.VSync; + + // 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 (Settings.EnableQuickStart && Input.Keyboard.CtrlOrCommand) + { + 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.FromHexStringRGB("#282c42")); + + Batcher batcher = new(); + Rect bounds = new(0, 0, target.Width, target.Height); + + string loadInfo; + + if (!areModsRegistered) { - loadDelay--; - if (loadDelay <= 0) - BeginGame(); + loadInfo = Loc.Str("FujiLoaderStatusRegistering"); + } + else + { + 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); } - } - public override void Render(Target target) - { - target.Clear(Color.Black); + 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 + } + + batcher.Render(target); + batcher.Clear(); } } diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 9a238731..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(); + Game.Instance.ReloadAssets(false); } var ply = Get();