Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions C7/Audio/AudioLoader.cs
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>();
Comment thread
ajhalme marked this conversation as resolved.
Outdated

// 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 {
Comment thread
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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:

audio.menu = {
  main_menu_1 = "/my/path/file.mp3"
}
Parts:       "menu.main_menu_1" --> ["menu", "main_menu_1"]
init:        current := audio
1st pass:    current["menu"] --> current := {  main_menu_1 = "/my/path/file.mp3" }
2nd pass:    current["main_menu_1"] --> current := "/my/path/file.mp3"
return:      "/my/path/file.mp3"

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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();
}
}
3 changes: 3 additions & 0 deletions C7/GlobalSingleton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions C7/Lua/civ3/audio.lua
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
18 changes: 18 additions & 0 deletions C7/Lua/standalone/audio.lua
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
9 changes: 5 additions & 4 deletions C7/UIElements/MainMenu/MainMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
10 changes: 2 additions & 8 deletions C7/UIElements/MainMenu/MainMenuMusicPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 7 additions & 7 deletions C7/UIElements/Popups/PopupOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AudioStreamPlayer>("PopupSound");
player.Stream = wav;
player.Stream = stream;
player.Play();
}

Expand All @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
33 changes: 31 additions & 2 deletions C7/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion C7Engine/Lua/GameMode.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using Serilog;
using C7GameData.Save;
using MoonSharp.Interpreter;
Expand All @@ -27,6 +28,7 @@ public Config(string baseModeDir, List<string> addonPaths = null) {
internal SaveGame ruleset;
public BehaviorEngine behaviors;
public (Script, Table) textures;
public (Script, Table) audio;

// Returns a deep copy of the ruleset
//
Expand All @@ -52,6 +54,7 @@ enum ScriptType {
Textures,
Ruleset,
Behaviors,
Audio
}

private static ILogger log = Log.ForContext<GameModeLoader>();
Expand All @@ -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)
};
}

Expand Down Expand Up @@ -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);
Expand Down
Loading