From a495e627d52c806a09a4ab6879d570d0fd2f0c87 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Fri, 28 Aug 2026 00:09:35 +0100 Subject: [PATCH 1/5] Lua config for audio, AudioLoader pattern --- C7/Audio/AudioLoader.cs | 132 ++++++++++++++++++ C7/GlobalSingleton.cs | 3 + C7/Lua/civ3/audio.lua | 22 +++ C7/UIElements/MainMenu/MainMenu.cs | 9 +- C7/UIElements/MainMenu/MainMenuMusicPlayer.cs | 10 +- C7/UIElements/Popups/PopupOverlay.cs | 14 +- C7/Util.cs | 22 ++- C7Engine/Lua/GameMode.cs | 8 +- 8 files changed, 198 insertions(+), 22 deletions(-) create mode 100644 C7/Audio/AudioLoader.cs create mode 100644 C7/Lua/civ3/audio.lua diff --git a/C7/Audio/AudioLoader.cs b/C7/Audio/AudioLoader.cs new file mode 100644 index 000000000..68b0649f8 --- /dev/null +++ b/C7/Audio/AudioLoader.cs @@ -0,0 +1,132 @@ +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 = []; + private static Dictionary<(string configKey, object obj), AudioStream> objectMappingCache = []; + + static AudioLoader() { + // 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()) { + 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 a texture based on the config key. + /// The config key should be a string separated by dots, representing the path through the + /// configuration hierarchy (e.g., "icons.plus"). + public static AudioStream Load(string configKey) { + if (configKeyCache.TryGetValue(configKey, out AudioStream cachedTexture)) + return cachedTexture; + + object entry = GetEntryByPath(configKey); + if (entry == null) + throw new Exception($"Texture config not found for key: {configKey}"); + + AudioStream texture = LoadFromLuaObject(entry); + + configKeyCache[configKey] = texture; + + return texture; + } + + /// Returns an audio stream based on the config key and a C# object. + /// + /// This overload uses the "map_object_to_audio_stream" function in the + /// config entry to dynamically determine which audio stream to load + /// based on the provided object's properties. + /// + /// This method optionally allows to cache the resulting texture, + /// using (configKey, obj) as key. Note that caching shouldn't + /// be used for objects whose properties can change. + /// + /// Note that the type of the object passed to the method should + /// be registered as Moonsharp userdata. + public static AudioStream Load(string configKey, object obj, bool useCache = false) { + var cacheKey = (configKey, obj); + + if (useCache && objectMappingCache.TryGetValue(cacheKey, out AudioStream cachedTexture)) + return cachedTexture; + + object entry = GetEntryByPath(configKey); + if (entry is not Table table) + throw new Exception($"Table expected for key: {configKey}"); + + if (table["map_object_to_sprite"] is not Closure func) + throw new Exception("Custom mapping function expected"); + + object result = lua.SafeCall(func, table, DynValue.FromObject(lua, obj)).ToObject(); + + AudioStream audioStream = LoadFromLuaObject(result); + + if (useCache) + objectMappingCache[cacheKey] = audioStream; + + return audioStream; + } + + 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), + _ => 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(); + objectMappingCache.Clear(); + } +} diff --git a/C7/GlobalSingleton.cs b/C7/GlobalSingleton.cs index dbd4b801b..315866ea8 100644 --- a/C7/GlobalSingleton.cs +++ b/C7/GlobalSingleton.cs @@ -47,6 +47,9 @@ public void ActivateGameMode(GameMode.Config config) { 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 { 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/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..8087db84a 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,24 @@ 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) { + FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + AudioStreamMP3 mp3 = new(); + long fileSize = (long)file.GetLength(); //might blow up if it's > 2 GB, oh well + mp3.Data = file.GetBuffer(fileSize); + mp3.Loop = true; // TODO: beyond simple path loading: add looping as a property + return mp3; + } + + // 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); From eb4038050f2b4998f9761b814d06090f2b55487e Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Sun, 30 Aug 2026 00:52:19 +0100 Subject: [PATCH 2/5] Add Ogg Vorbis support, Lua override, first Audio asset --- C7/Assets | 2 +- C7/Audio/AudioLoader.cs | 59 +++++++++++++------------------------ C7/Lua/standalone/audio.lua | 18 +++++++++++ C7/Util.cs | 19 +++++++++--- 4 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 C7/Lua/standalone/audio.lua diff --git a/C7/Assets b/C7/Assets index 897564c51..a05393eaf 160000 --- a/C7/Assets +++ b/C7/Assets @@ -1 +1 @@ -Subproject commit 897564c51142d854875da9da98826d4d77b95446 +Subproject commit a05393eaf49fdb8431132a22e2b1691a1da49049 diff --git a/C7/Audio/AudioLoader.cs b/C7/Audio/AudioLoader.cs index 68b0649f8..a0905771f 100644 --- a/C7/Audio/AudioLoader.cs +++ b/C7/Audio/AudioLoader.cs @@ -12,14 +12,13 @@ public static class AudioLoader { private static Table audioConfig; private static Dictionary configKeyCache = []; - private static Dictionary<(string configKey, object obj), AudioStream> objectMappingCache = []; static AudioLoader() { // 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 + // 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); @@ -35,57 +34,39 @@ public static void SetConfig(Script lua, Table audioConfig) { AudioLoader.audioConfig = audioConfig; } - /// Returns a texture based on the config key. + /// 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., "icons.plus"). + /// configuration hierarchy (e.g., "menu.main_menu_1"). public static AudioStream Load(string configKey) { - if (configKeyCache.TryGetValue(configKey, out AudioStream cachedTexture)) - return cachedTexture; + if (configKeyCache.TryGetValue(configKey, out AudioStream cachedAudio)) + return cachedAudio; object entry = GetEntryByPath(configKey); if (entry == null) - throw new Exception($"Texture config not found for key: {configKey}"); + throw new Exception($"Audio config not found for key: {configKey}"); - AudioStream texture = LoadFromLuaObject(entry); + object entry2 = GetEntryByModPath(configKey); - configKeyCache[configKey] = texture; + AudioStream audioStream = LoadFromLuaObject(entry2 ?? entry); - return texture; + configKeyCache[configKey] = audioStream; + + return audioStream; } - /// Returns an audio stream based on the config key and a C# object. - /// - /// This overload uses the "map_object_to_audio_stream" function in the - /// config entry to dynamically determine which audio stream to load - /// based on the provided object's properties. - /// - /// This method optionally allows to cache the resulting texture, - /// using (configKey, obj) as key. Note that caching shouldn't - /// be used for objects whose properties can change. - /// - /// Note that the type of the object passed to the method should - /// be registered as Moonsharp userdata. - public static AudioStream Load(string configKey, object obj, bool useCache = false) { - var cacheKey = (configKey, obj); - - if (useCache && objectMappingCache.TryGetValue(cacheKey, out AudioStream cachedTexture)) - return cachedTexture; + private static object GetEntryByModPath(string configKey) { + object current = audioConfig; - object entry = GetEntryByPath(configKey); - if (entry is not Table table) - throw new Exception($"Table expected for key: {configKey}"); + if (current is not Table table) + throw new Exception($"Root is not table"); if (table["map_object_to_sprite"] is not Closure func) - throw new Exception("Custom mapping function expected"); + return null; - object result = lua.SafeCall(func, table, DynValue.FromObject(lua, obj)).ToObject(); + var arg = DynValue.FromObject(lua, configKey); + object result = lua.SafeCall(func, arg).ToObject(); - AudioStream audioStream = LoadFromLuaObject(result); - - if (useCache) - objectMappingCache[cacheKey] = audioStream; - - return audioStream; + return result; } private static AudioStream LoadFromLuaObject(object entry) { @@ -106,6 +87,7 @@ private static AudioStream LoadFromPath(string path) { return ext switch { ".wav" => Util.LoadCiv3WAVFromDisk(path), ".mp3" => Util.LoadCiv3Mp3FromDisk(path), + ".ogg" => Util.LoadCiv3OggFromDisk(path), _ => throw new FormatException($"Unknown audio format: {path}"), }; } @@ -127,6 +109,5 @@ private static object GetEntryByPath(string configKey) { public static void ClearCache() { configKeyCache.Clear(); - objectMappingCache.Clear(); } } 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/Util.cs b/C7/Util.cs index 8087db84a..e13fba45d 100644 --- a/C7/Util.cs +++ b/C7/Util.cs @@ -382,14 +382,25 @@ public static AudioStreamWav LoadWAVFromDisk(string path) { } public static AudioStreamMP3 LoadMp3FromDisk(string path) { - FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Read); - AudioStreamMP3 mp3 = new(); - long fileSize = (long)file.GetLength(); //might blow up if it's > 2 GB, oh well - mp3.Data = file.GetBuffer(fileSize); + 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 From 6429d7e788f200066af5d90cb3b4391d082771a6 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Mon, 31 Aug 2026 10:44:11 +0100 Subject: [PATCH 3/5] Consolidate UI-side Lua type registrations --- C7/Audio/AudioLoader.cs | 4 ---- C7/GlobalSingleton.cs | 21 +++++++++++++++++++++ C7/Textures/TextureLoader.cs | 14 -------------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/C7/Audio/AudioLoader.cs b/C7/Audio/AudioLoader.cs index a0905771f..3d2dbb9b2 100644 --- a/C7/Audio/AudioLoader.cs +++ b/C7/Audio/AudioLoader.cs @@ -14,10 +14,6 @@ public static class AudioLoader { private static Dictionary configKeyCache = []; static AudioLoader() { - // We need to register the "Type" type to be able to inspect - // the types of C# objects in the Lua code - UserData.RegisterType(); - // Initialize 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/GlobalSingleton.cs b/C7/GlobalSingleton.cs index 315866ea8..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,6 +47,8 @@ public void ActivateGameMode(GameMode.Config config) { GameMode = GameMode.Load(GamePaths.GameModesDir, config); + LuaTypeRegistrations(); + var (script, textureConfig) = GameMode.textures; TextureLoader.SetConfig(script, textureConfig); @@ -59,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/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()) { From 93ac0d799dadbbf2536c5f7eaf5015bba90a1f60 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Mon, 31 Aug 2026 10:48:21 +0100 Subject: [PATCH 4/5] Add .oga suffix in AudioLoader switch --- C7/Audio/AudioLoader.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/C7/Audio/AudioLoader.cs b/C7/Audio/AudioLoader.cs index 3d2dbb9b2..f6f8b8cee 100644 --- a/C7/Audio/AudioLoader.cs +++ b/C7/Audio/AudioLoader.cs @@ -84,6 +84,7 @@ private static AudioStream LoadFromPath(string path) { ".wav" => Util.LoadCiv3WAVFromDisk(path), ".mp3" => Util.LoadCiv3Mp3FromDisk(path), ".ogg" => Util.LoadCiv3OggFromDisk(path), + ".oga" => Util.LoadCiv3OggFromDisk(path), _ => throw new FormatException($"Unknown audio format: {path}"), }; } From dc50d0b96755fe77e1f82e87550f3f077509d045 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Thu, 3 Sep 2026 22:39:04 +0100 Subject: [PATCH 5/5] Audio assets --- C7/Assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/C7/Assets b/C7/Assets index a05393eaf..716625cc6 160000 --- a/C7/Assets +++ b/C7/Assets @@ -1 +1 @@ -Subproject commit a05393eaf49fdb8431132a22e2b1691a1da49049 +Subproject commit 716625cc6c68e872f253c48efe7f934b77d9ba0c