diff --git a/C7/Assets b/C7/Assets index 897564c51..716625cc6 160000 --- a/C7/Assets +++ b/C7/Assets @@ -1 +1 @@ -Subproject commit 897564c51142d854875da9da98826d4d77b95446 +Subproject commit 716625cc6c68e872f253c48efe7f934b77d9ba0c diff --git a/C7/Audio/AudioLoader.cs b/C7/Audio/AudioLoader.cs new file mode 100644 index 000000000..f6f8b8cee --- /dev/null +++ b/C7/Audio/AudioLoader.cs @@ -0,0 +1,110 @@ +using System; +using Godot; +using System.Collections.Generic; +using System.IO; +using MoonSharp.Interpreter; +using Script = MoonSharp.Interpreter.Script; +using C7Engine.Lua; + +public static class AudioLoader { + + private static Script lua; + private static Table audioConfig; + + private static Dictionary configKeyCache = []; + + static AudioLoader() { + // Initialize when running in the editor + // In game it is done by GlobalSingleton, but it's not accessible in the editor + if (Engine.IsEditorHint()) { + GameMode gameMode = GameMode.Load(GamePaths.GameModesDir, GamePaths.basic); + var (script, table) = gameMode.audio; + AudioLoader.SetConfig(script, table); + } + } + + public static void SetConfig(Script lua, Table audioConfig) { + ClearCache(); + + AudioLoader.lua = lua; + AudioLoader.audioConfig = audioConfig; + } + + /// Returns an audio stream based on the config key. + /// The config key should be a string separated by dots, representing the path through the + /// configuration hierarchy (e.g., "menu.main_menu_1"). + public static AudioStream Load(string configKey) { + if (configKeyCache.TryGetValue(configKey, out AudioStream cachedAudio)) + return cachedAudio; + + object entry = GetEntryByPath(configKey); + if (entry == null) + throw new Exception($"Audio config not found for key: {configKey}"); + + object entry2 = GetEntryByModPath(configKey); + + AudioStream audioStream = LoadFromLuaObject(entry2 ?? entry); + + configKeyCache[configKey] = audioStream; + + return audioStream; + } + + private static object GetEntryByModPath(string configKey) { + object current = audioConfig; + + if (current is not Table table) + throw new Exception($"Root is not table"); + + if (table["map_object_to_sprite"] is not Closure func) + return null; + + var arg = DynValue.FromObject(lua, configKey); + object result = lua.SafeCall(func, arg).ToObject(); + + return result; + } + + private static AudioStream LoadFromLuaObject(object entry) { + return LoadFromPath(ParsePath(entry)); + } + + private static string ParsePath(object entry) { + if (entry is string simplePath) { + return simplePath; + } + + throw new ArgumentException($"Invalid audio config format: {entry?.GetType().Name ?? "null"}"); + } + + private static AudioStream LoadFromPath(string path) { + string ext = Path.GetExtension(path).ToLowerInvariant(); + + return ext switch { + ".wav" => Util.LoadCiv3WAVFromDisk(path), + ".mp3" => Util.LoadCiv3Mp3FromDisk(path), + ".ogg" => Util.LoadCiv3OggFromDisk(path), + ".oga" => Util.LoadCiv3OggFromDisk(path), + _ => throw new FormatException($"Unknown audio format: {path}"), + }; + } + + private static object GetEntryByPath(string configKey) { + string[] parts = configKey.Split('.'); + object current = audioConfig; + + foreach (string part in parts) { + if (current is Table table && table[part] != null) { + current = table[part]; + } else { + return null; + } + } + + return current; + } + + public static void ClearCache() { + configKeyCache.Clear(); + } +} diff --git a/C7/GlobalSingleton.cs b/C7/GlobalSingleton.cs index dbd4b801b..759bd0622 100644 --- a/C7/GlobalSingleton.cs +++ b/C7/GlobalSingleton.cs @@ -1,7 +1,10 @@ +using System; +using C7.Map; using Godot; using C7Engine; using C7GameData.Save; using C7Engine.Lua; +using MoonSharp.Interpreter; /**** Need to pass values from one scene to another, particularly when loading @@ -44,9 +47,14 @@ public void ActivateGameMode(GameMode.Config config) { GameMode = GameMode.Load(GamePaths.GameModesDir, config); + LuaTypeRegistrations(); + var (script, textureConfig) = GameMode.textures; TextureLoader.SetConfig(script, textureConfig); + var (audioLua, audioConfig) = GameMode.audio; + AudioLoader.SetConfig(audioLua, audioConfig); + if (config.addonPaths.Contains("standalone")) { C7Settings.SetValue("locations", "useStandaloneMode", "true"); } else { @@ -56,6 +64,22 @@ public void ActivateGameMode(GameMode.Config config) { C7Settings.SaveSettings(); } + private void LuaTypeRegistrations() { + // We need to register the "Type" type to be able to inspect + // the types of C# objects in the Lua code + UserData.RegisterType(); + + // Note: classes in the C7GameData namespace are already registered as part of GameModeLoader logic + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + + // Note, we register all of AdvisorHeader rather than just + // AdvisorHead.AdvisorGraphicsDetails because we access the nums + // in the class as well. + UserData.RegisterType(); + } + public void ToggleStandaloneMode() { GameMode.Config newConfig = C7Settings.UseStandaloneMode() ? GamePaths.basic : GamePaths.standalone; diff --git a/C7/Lua/civ3/audio.lua b/C7/Lua/civ3/audio.lua new file mode 100644 index 000000000..61675083d --- /dev/null +++ b/C7/Lua/civ3/audio.lua @@ -0,0 +1,22 @@ +-- Base paths +local SOUNDS = "Sounds/" +local MENU = SOUNDS .. "Menu/" + +-- Audio definitions +local audio = {} + +audio.menu = { + main_menu_1 = MENU .. "Menu1.mp3" +} + +audio.buttons = { + button_1 = SOUNDS .. "Button1.wav" +} + +audio.popups = { + advisor = SOUNDS .. "PopupAdvisor.wav", + console = SOUNDS .. "PopupConsole.wav", + info = SOUNDS .. "PopupInfo.wav" +} + +return audio diff --git a/C7/Lua/standalone/audio.lua b/C7/Lua/standalone/audio.lua new file mode 100644 index 000000000..3b051df86 --- /dev/null +++ b/C7/Lua/standalone/audio.lua @@ -0,0 +1,18 @@ + +local audio_map = { + ["menu.main_menu_1"] = "Audio/Music/Icarus/Icarus_alt.ogg" +} + +--[[ + Main audio override function +--]] +return function(civ3_audio) + local oc3_audio = civ3_audio + + function oc3_audio.map_object_to_sprite(item) + local value = audio_map[tostring(item)] or nil + return value + end + + return oc3_audio +end diff --git a/C7/Textures/TextureLoader.cs b/C7/Textures/TextureLoader.cs index 5ee0c47fe..c80b3a86d 100644 --- a/C7/Textures/TextureLoader.cs +++ b/C7/Textures/TextureLoader.cs @@ -80,20 +80,6 @@ public ConfigEntry() { private static Dictionary<(string configKey, string animationName), SpriteFrames> animationCache = []; static TextureLoader() { - // Note: classes in the C7GameData namespace are already registered as part of GameModeLoader logic - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - - // Note, we register all of AdvisorHeader rather than just - // AdvisorHead.AdvisorGraphicsDetails because we access the nums - // in the class as well. - UserData.RegisterType(); - - // We need to register the "Type" type to be able to inspect - // the types of C# objects in the Lua code - UserData.RegisterType(); - // Initialize the TextureLoader when running in the editor // In game it is done by GlobalSingleton, but it's not accessible in the editor if (Engine.IsEditorHint()) { diff --git a/C7/UIElements/MainMenu/MainMenu.cs b/C7/UIElements/MainMenu/MainMenu.cs index d48dac487..fd4f43aa1 100644 --- a/C7/UIElements/MainMenu/MainMenu.cs +++ b/C7/UIElements/MainMenu/MainMenu.cs @@ -149,11 +149,12 @@ public void _on_Exit_pressed() { } private void PlayButtonPressedSound() { - AudioStreamWav wav = Util.LoadCiv3WAVFromDisk("Sounds/Button1.wav"); - if (wav == null) { + AudioStream stream = AudioLoader.Load("buttons.button_1"); + + if (stream == null) return; - } - player.Stream = wav; + + player.Stream = stream; player.Play(); } diff --git a/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs b/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs index 3b1264c33..c7b776242 100644 --- a/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs +++ b/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs @@ -14,14 +14,8 @@ public override void _Ready() { //Figured out how to load the mp3 from this post: https://godotengine.org/qa/30210/how-do-load-resource-works try { - string mp3Path = Util.Civ3MediaPath("Sounds/Menu/Menu1.mp3"); - FileAccess mp3File = FileAccess.Open(mp3Path, FileAccess.ModeFlags.Read); - - AudioStreamMP3 mp3 = new AudioStreamMP3(); - long fileSize = (long)mp3File.GetLength(); //might blow up if it's > 2 GB, oh well - mp3.Data = mp3File.GetBuffer(fileSize); - mp3.Loop = true; - this.Stream = mp3; + AudioStream stream = AudioLoader.Load("menu.main_menu_1"); + this.Stream = stream; string volume = C7Settings.GetSettingValue("audio", "musicVolume"); float targetVolumeOffset = GetVolumeOffset(volume); diff --git a/C7/UIElements/Popups/PopupOverlay.cs b/C7/UIElements/Popups/PopupOverlay.cs index d9ddee64f..7345ca5f0 100644 --- a/C7/UIElements/Popups/PopupOverlay.cs +++ b/C7/UIElements/Popups/PopupOverlay.cs @@ -38,9 +38,9 @@ public void OnHidePopup() { public bool ShowingPopup => currentChild is not null; - public void PlaySound(AudioStreamWav wav) { + public void PlaySound(AudioStream stream) { AudioStreamPlayer player = GetNode("PopupSound"); - player.Stream = wav; + player.Stream = stream; player.Play(); } @@ -61,13 +61,13 @@ public void ShowPopup(Popup child, PopupCategory category) { currentChild = child; var soundFile = category switch { - PopupCategory.Advisor => "Sounds/PopupAdvisor.wav", - PopupCategory.Console => "Sounds/PopupConsole.wav", - PopupCategory.Info => "Sounds/PopupInfo.wav", + PopupCategory.Advisor => "popups.advisor", + PopupCategory.Console => "popups.console", + PopupCategory.Info => "popups.info", _ => null }; - var wav = soundFile == null ? null : Util.LoadCiv3WAVFromDisk(soundFile); + var wav = soundFile == null ? null : AudioLoader.Load(soundFile); Isolate(); @@ -142,7 +142,7 @@ public override void _UnhandledInput(InputEvent @event) { } if (@event is InputEventMouseButton ev) { - // Catch right clicks over UI elements to stop awkward TileInfo renders + // Catch right clicks over UI elements to stop awkward TileInfo renders if (ev.ButtonIndex == MouseButton.Right) { if (IsOverUI()) { AcceptEvent(); diff --git a/C7/Util.cs b/C7/Util.cs index 427e200ce..e13fba45d 100644 --- a/C7/Util.cs +++ b/C7/Util.cs @@ -292,7 +292,7 @@ public static (FlicSheet, Flic) loadFlicSheet(string filePath) { } } - static public AudioStreamWav LoadWAVFromDisk(string path) { + public static AudioStreamWav LoadWAVFromDisk(string path) { FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Read); byte[] riffBytes = file.GetBuffer(4); @@ -309,7 +309,7 @@ static public AudioStreamWav LoadWAVFromDisk(string path) { bool formatFound = false; bool dataFound = false; - AudioStreamWav wav = new AudioStreamWav(); + AudioStreamWav wav = new(); while (!file.EofReached()) { byte[] chunkBytes = file.GetBuffer(4); @@ -373,6 +373,35 @@ static public AudioStreamWav LoadWAVFromDisk(string path) { return wav; } + public static AudioStreamMP3? LoadCiv3Mp3FromDisk(string path) { + try { + return LoadMp3FromDisk(Civ3MediaPath(path)); + } catch (Exception e) { + return null; + } + } + + public static AudioStreamMP3 LoadMp3FromDisk(string path) { + AudioStreamMP3 mp3 = AudioStreamMP3.LoadFromFile(path); + mp3.Loop = true; // TODO: beyond simple path loading: add looping as a property + return mp3; + } + + public static AudioStreamOggVorbis? LoadCiv3OggFromDisk(string path) { + try { + return LoadOggFromDisk(Civ3MediaPath(path)); + } catch (Exception e) { + return null; + } + } + + public static AudioStreamOggVorbis LoadOggFromDisk(string path) { + AudioStreamOggVorbis ogg = AudioStreamOggVorbis.LoadFromFile(path); + ogg.Loop = true; + return ogg; + } + + // This method is intended for use within overrides of Godot object _ValidateProperty method. // Its purpose is to prevent values of properties listed in validProperties from being saved as // part of the scene. It's useful when using [Tool] scripts to execute code in editor. It diff --git a/C7Engine/Lua/GameMode.cs b/C7Engine/Lua/GameMode.cs index fe6a871c5..79fa11243 100644 --- a/C7Engine/Lua/GameMode.cs +++ b/C7Engine/Lua/GameMode.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Collections.Generic; +using System.ComponentModel; using Serilog; using C7GameData.Save; using MoonSharp.Interpreter; @@ -27,6 +28,7 @@ public Config(string baseModeDir, List addonPaths = null) { internal SaveGame ruleset; public BehaviorEngine behaviors; public (Script, Table) textures; + public (Script, Table) audio; // Returns a deep copy of the ruleset // @@ -52,6 +54,7 @@ enum ScriptType { Textures, Ruleset, Behaviors, + Audio } private static ILogger log = Log.ForContext(); @@ -76,11 +79,13 @@ public GameMode Load() { Table behaviors = LoadWithAddons(ScriptType.Behaviors).Table; Table textures = LoadWithAddons(ScriptType.Textures).Table; + Table audio = LoadWithAddons(ScriptType.Audio).Table; return new() { ruleset = LoadRuleset(), behaviors = new(lua, behaviors), textures = (lua, textures), + audio = (lua, audio) }; } @@ -144,8 +149,9 @@ private DynValue LoadAddons(DynValue baseTable, ScriptType scriptType) { private string GetScriptPath(string addonDir, ScriptType scriptType) { string scriptFile = scriptType switch { ScriptType.Textures => "textures.lua", - ScriptType.Behaviors => "behaviors.lua", ScriptType.Ruleset => "ruleset.lua", + ScriptType.Behaviors => "behaviors.lua", + ScriptType.Audio => "audio.lua", _ => throw new InvalidOperationException("Unknown script type"), }; return Path.Combine(gameModesDir, addonDir, scriptFile);