-
Notifications
You must be signed in to change notification settings - Fork 64
Towards moddable audio #984
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
a495e62
eb40380
6429d7e
93ac0d7
dc50d0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| + − | Art/3checkboxes-USE.png | |
| + − | Audio/Music/Icarus/Icarus_alt.ogg | |
| +3 −0 | Audio/Music/Icarus/README.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| 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<string, AudioStream> 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<Type>(); | ||
|
|
||
| // 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 { | ||
|
ajhalme marked this conversation as resolved.
|
||
| ".wav" => Util.LoadCiv3WAVFromDisk(path), | ||
| ".mp3" => Util.LoadCiv3Mp3FromDisk(path), | ||
| ".ogg" => 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; | ||
| } | ||
| } | ||
|
Comment on lines
+96
to
+102
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused, this will end up returning the last such table entry, correct? We would iterate backwards and return the first match in that case. But why would multiple substrings be found in the table anyway?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is identical to TextureLoader. Inlined it here because it's a compact static helper. I'm not sure I follow your interpretation. This function resolves the value from a nested Lua structure: The modding Lua code makes a second lookup to see if there's a direct key-based override for the structured key, using it as a flat key-value lookup. That is, there is no structure to parse in the modded code. We could make the moddable stuff structured as well, but there's no need for now. The reason to have structured Lua data in the first place is to enable not just simple values but full objects with properties to be retrieved by a single key.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, I missed that this is recursing into the table structure |
||
|
|
||
| return current; | ||
| } | ||
|
|
||
| public static void ClearCache() { | ||
| configKeyCache.Clear(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Uh oh!
There was an error while loading. Please reload this page.