diff --git a/assets/config_menu.rml b/assets/config_menu.rml
index 696852a..a857ff2 100644
--- a/assets/config_menu.rml
+++ b/assets/config_menu.rml
@@ -63,7 +63,7 @@
Sound
-
+
diff --git a/assets/config_menu/sound.rml b/assets/config_menu/sound.rml
index db0b161..846034b 100644
--- a/assets/config_menu/sound.rml
+++ b/assets/config_menu/sound.rml
@@ -4,7 +4,24 @@
diff --git a/include/recomp_sound.h b/include/recomp_sound.h
new file mode 100644
index 0000000..04408e0
--- /dev/null
+++ b/include/recomp_sound.h
@@ -0,0 +1,12 @@
+#ifndef __RECOMP_SOUND_H__
+#define __RECOMP_SOUND_H__
+
+namespace recomp {
+ void reset_sound_settings();
+ void set_bgm_volume(int volume);
+ int get_bgm_volume();
+ void set_low_health_beeps_enabled(bool enabled);
+ bool get_low_health_beeps_enabled();
+}
+
+#endif
diff --git a/patches/sound.h b/patches/sound.h
new file mode 100644
index 0000000..0e05637
--- /dev/null
+++ b/patches/sound.h
@@ -0,0 +1,9 @@
+#ifndef __PATCH_AUDIO_H__
+#define __PATCH_AUDIO_H__
+
+#include "patch_helpers.h"
+
+DECLARE_FUNC(float, recomp_get_bgm_volume);
+DECLARE_FUNC(u32, recomp_get_low_health_beeps_enabled);
+
+#endif
diff --git a/patches/sound_patches.c b/patches/sound_patches.c
new file mode 100644
index 0000000..dd4c7ea
--- /dev/null
+++ b/patches/sound_patches.c
@@ -0,0 +1,363 @@
+#include "patches.h"
+#include "overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope.h"
+#include "sound.h"
+
+void AudioSeq_ProcessSeqCmd(u32 cmd);
+void AudioThread_QueueCmd(u32 opArgs, void** data);
+
+// Direct audio command (skips the queueing system)
+#define SEQCMD_SET_SEQPLAYER_VOLUME_NOW(seqPlayerIndex, duration, volume) \
+ AudioSeq_ProcessSeqCmd((SEQCMD_OP_SET_SEQPLAYER_VOLUME << 28) | ((u8)(seqPlayerIndex) << 24) | \
+ ((u8)(duration) << 16) | ((u8)((volume)*127.0f)));
+
+bool is_bgm_player(u8 player_index) {
+ return player_index == SEQ_PLAYER_BGM_MAIN || player_index == SEQ_PLAYER_BGM_SUB;
+}
+
+/**
+ * Update different commands and requests for active sequences
+ */
+void AudioSeq_UpdateActiveSequences(void) {
+ u32 tempoCmd;
+ u16 tempoPrev;
+ u16 seqId;
+ u16 channelMask;
+ u16 tempoTarget;
+ u8 setupOp;
+ u8 targetSeqPlayerIndex;
+ u8 setupVal2;
+ u8 setupVal1;
+ u8 tempoOp;
+ s32 pad[2];
+ u32 retMsg;
+ f32 volume;
+ u8 tempoTimer;
+ u8 seqPlayerIndex;
+ u8 j;
+ u8 channelIndex;
+
+ for (seqPlayerIndex = 0; seqPlayerIndex < SEQ_PLAYER_MAX; seqPlayerIndex++) {
+
+ // The seqPlayer has finished initializing and is currently playing the active sequences
+ if (gActiveSeqs[seqPlayerIndex].isSeqPlayerInit && gAudioCtx.seqPlayers[seqPlayerIndex].enabled) {
+ gActiveSeqs[seqPlayerIndex].isSeqPlayerInit = false;
+ }
+
+ // The seqPlayer is no longer playing the active sequences
+ if ((AudioSeq_GetActiveSeqId(seqPlayerIndex) != NA_BGM_DISABLED) &&
+ !gAudioCtx.seqPlayers[seqPlayerIndex].enabled && (!gActiveSeqs[seqPlayerIndex].isSeqPlayerInit)) {
+ gActiveSeqs[seqPlayerIndex].seqId = NA_BGM_DISABLED;
+ }
+
+ // Check if the requested sequences is waiting for fonts to load
+ if (gActiveSeqs[seqPlayerIndex].isWaitingForFonts) {
+ switch ((s32)AudioThread_GetExternalLoadQueueMsg(&retMsg)) {
+ case SEQ_PLAYER_BGM_MAIN + 1:
+ case SEQ_PLAYER_FANFARE + 1:
+ case SEQ_PLAYER_SFX + 1:
+ case SEQ_PLAYER_BGM_SUB + 1:
+ case SEQ_PLAYER_AMBIENCE + 1:
+ // The fonts have been loaded successfully.
+ gActiveSeqs[seqPlayerIndex].isWaitingForFonts = false;
+ // Queue the same command that was stored previously, but without the 0x8000
+ AudioSeq_ProcessSeqCmd(gActiveSeqs[seqPlayerIndex].startAsyncSeqCmd);
+ break;
+ case 0xFF:
+ // There was an error in loading the fonts
+ gActiveSeqs[seqPlayerIndex].isWaitingForFonts = false;
+ break;
+ }
+ }
+
+ // Update global volume
+ if (gActiveSeqs[seqPlayerIndex].fadeVolUpdate) {
+ volume = 1.0f;
+ for (j = 0; j < VOL_SCALE_INDEX_MAX; j++) {
+ volume *= (gActiveSeqs[seqPlayerIndex].volScales[j] / 127.0f);
+ }
+
+ SEQCMD_SET_SEQPLAYER_VOLUME((u8)(seqPlayerIndex + (SEQCMD_ASYNC_ACTIVE >> 24)),
+ gActiveSeqs[seqPlayerIndex].volFadeTimer, (u8)(volume * 127.0f));
+ gActiveSeqs[seqPlayerIndex].fadeVolUpdate = false;
+ }
+
+ if (gActiveSeqs[seqPlayerIndex].volTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].volTimer--;
+
+ if (gActiveSeqs[seqPlayerIndex].volTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].volCur -= gActiveSeqs[seqPlayerIndex].volStep;
+ } else {
+ gActiveSeqs[seqPlayerIndex].volCur = gActiveSeqs[seqPlayerIndex].volTarget;
+ }
+
+ }
+ // @recomp Send a volume scale command regardless of whether volTimer is active and scale it for background music players.
+ f32 cur_volume = gActiveSeqs[seqPlayerIndex].volCur;
+ if (is_bgm_player(seqPlayerIndex)) {
+ cur_volume *= recomp_get_bgm_volume();
+ }
+ AUDIOCMD_SEQPLAYER_FADE_VOLUME_SCALE(seqPlayerIndex, cur_volume);
+
+ // Process tempo
+ if (gActiveSeqs[seqPlayerIndex].tempoCmd != 0) {
+ tempoCmd = gActiveSeqs[seqPlayerIndex].tempoCmd;
+ tempoTimer = (tempoCmd & 0xFF0000) >> 15;
+ tempoTarget = tempoCmd & 0xFFF;
+ if (tempoTimer == 0) {
+ tempoTimer++;
+ }
+
+ // Process tempo commands
+ if (gAudioCtx.seqPlayers[seqPlayerIndex].enabled) {
+ tempoPrev = gAudioCtx.seqPlayers[seqPlayerIndex].tempo / TATUMS_PER_BEAT;
+ tempoOp = (tempoCmd & 0xF000) >> 12;
+ switch (tempoOp) {
+ case SEQCMD_SUB_OP_TEMPO_SPEED_UP:
+ // Speed up tempo by `tempoTarget` amount
+ tempoTarget += tempoPrev;
+ break;
+
+ case SEQCMD_SUB_OP_TEMPO_SLOW_DOWN:
+ // Slow down tempo by `tempoTarget` amount
+ if (tempoTarget < tempoPrev) {
+ tempoTarget = tempoPrev - tempoTarget;
+ }
+ break;
+
+ case SEQCMD_SUB_OP_TEMPO_SCALE:
+ // Scale tempo by a multiplicative factor
+ tempoTarget = tempoPrev * (tempoTarget / 100.0f);
+ break;
+
+ case SEQCMD_SUB_OP_TEMPO_RESET:
+ // Reset tempo to original tempo
+ tempoTarget = (gActiveSeqs[seqPlayerIndex].tempoOriginal != 0)
+ ? gActiveSeqs[seqPlayerIndex].tempoOriginal
+ : tempoPrev;
+ break;
+
+ default: // `SEQCMD_SUB_OP_TEMPO_SET`
+ // `tempoTarget` is the new tempo
+ break;
+ }
+
+ if (gActiveSeqs[seqPlayerIndex].tempoOriginal == 0) {
+ gActiveSeqs[seqPlayerIndex].tempoOriginal = tempoPrev;
+ }
+
+ gActiveSeqs[seqPlayerIndex].tempoTarget = tempoTarget;
+ gActiveSeqs[seqPlayerIndex].tempoCur = gAudioCtx.seqPlayers[seqPlayerIndex].tempo / 0x30;
+ gActiveSeqs[seqPlayerIndex].tempoStep =
+ (gActiveSeqs[seqPlayerIndex].tempoCur - gActiveSeqs[seqPlayerIndex].tempoTarget) / tempoTimer;
+ gActiveSeqs[seqPlayerIndex].tempoTimer = tempoTimer;
+ gActiveSeqs[seqPlayerIndex].tempoCmd = 0;
+ }
+ }
+
+ // Step tempo to target
+ if (gActiveSeqs[seqPlayerIndex].tempoTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].tempoTimer--;
+ if (gActiveSeqs[seqPlayerIndex].tempoTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].tempoCur -= gActiveSeqs[seqPlayerIndex].tempoStep;
+ } else {
+ gActiveSeqs[seqPlayerIndex].tempoCur = gActiveSeqs[seqPlayerIndex].tempoTarget;
+ }
+
+ AUDIOCMD_SEQPLAYER_SET_TEMPO(seqPlayerIndex, gActiveSeqs[seqPlayerIndex].tempoCur);
+ }
+
+ // Update channel volumes
+ if (gActiveSeqs[seqPlayerIndex].volChannelFlags != 0) {
+ for (channelIndex = 0; channelIndex < SEQ_NUM_CHANNELS; channelIndex++) {
+ if (gActiveSeqs[seqPlayerIndex].channelData[channelIndex].volTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].volTimer--;
+ if (gActiveSeqs[seqPlayerIndex].channelData[channelIndex].volTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].volCur -=
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].volStep;
+ } else {
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].volCur =
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].volTarget;
+ gActiveSeqs[seqPlayerIndex].volChannelFlags ^= (1 << channelIndex);
+ }
+
+ AUDIOCMD_CHANNEL_SET_VOL_SCALE(seqPlayerIndex, channelIndex,
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].volCur);
+ }
+ }
+ }
+
+ // Update frequencies
+ if (gActiveSeqs[seqPlayerIndex].freqScaleChannelFlags != 0) {
+ for (channelIndex = 0; channelIndex < SEQ_NUM_CHANNELS; channelIndex++) {
+ if (gActiveSeqs[seqPlayerIndex].channelData[channelIndex].freqScaleTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].freqScaleTimer--;
+ if (gActiveSeqs[seqPlayerIndex].channelData[channelIndex].freqScaleTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].freqScaleCur -=
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].freqScaleStep;
+ } else {
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].freqScaleCur =
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].freqScaleTarget;
+ gActiveSeqs[seqPlayerIndex].freqScaleChannelFlags ^= (1 << channelIndex);
+ }
+
+ AUDIOCMD_CHANNEL_SET_FREQ_SCALE(seqPlayerIndex, channelIndex,
+ gActiveSeqs[seqPlayerIndex].channelData[channelIndex].freqScaleCur);
+ }
+ }
+ }
+
+ // Process setup commands
+ if (gActiveSeqs[seqPlayerIndex].setupCmdNum != 0) {
+ // If there is a SeqCmd to reset the audio heap queued, then drop all setup commands
+ if (!AudioSeq_IsSeqCmdNotQueued(SEQCMD_OP_RESET_AUDIO_HEAP << 28, SEQCMD_OP_MASK)) {
+ gActiveSeqs[seqPlayerIndex].setupCmdNum = 0;
+ break;
+ }
+
+ // Only process setup commands once the timer reaches zero
+ if (gActiveSeqs[seqPlayerIndex].setupCmdTimer != 0) {
+ gActiveSeqs[seqPlayerIndex].setupCmdTimer--;
+ continue;
+ }
+
+ // Only process setup commands if `seqPlayerIndex` if no longer playing
+ // i.e. the `seqPlayer` is no longer enabled
+ if (gAudioCtx.seqPlayers[seqPlayerIndex].enabled) {
+ continue;
+ }
+
+ for (j = 0; j < gActiveSeqs[seqPlayerIndex].setupCmdNum; j++) {
+ setupOp = (gActiveSeqs[seqPlayerIndex].setupCmd[j] & 0xF00000) >> 20;
+ targetSeqPlayerIndex = (gActiveSeqs[seqPlayerIndex].setupCmd[j] & 0xF0000) >> 16;
+ setupVal2 = (gActiveSeqs[seqPlayerIndex].setupCmd[j] & 0xFF00) >> 8;
+ setupVal1 = gActiveSeqs[seqPlayerIndex].setupCmd[j] & 0xFF;
+
+ switch (setupOp) {
+ case SEQCMD_SUB_OP_SETUP_RESTORE_SEQPLAYER_VOLUME:
+ // Restore `targetSeqPlayerIndex` volume back to normal levels
+ AudioSeq_SetVolumeScale(targetSeqPlayerIndex, VOL_SCALE_INDEX_FANFARE, 0x7F, setupVal1);
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_RESTORE_SEQPLAYER_VOLUME_IF_QUEUED:
+ // Restore `targetSeqPlayerIndex` volume back to normal levels,
+ // but only if the number of sequence queue requests from `sSeqRequests`
+ // exactly matches the argument to the command
+ if (setupVal1 == sNumSeqRequests[seqPlayerIndex]) {
+ AudioSeq_SetVolumeScale(targetSeqPlayerIndex, VOL_SCALE_INDEX_FANFARE, 0x7F, setupVal2);
+ }
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_SEQ_UNQUEUE:
+ // Unqueue `seqPlayerIndex` from sSeqRequests
+ //! @bug this command does not work as intended as unqueueing
+ //! the sequence relies on `gActiveSeqs[seqPlayerIndex].seqId`
+ //! However, `gActiveSeqs[seqPlayerIndex].seqId` is reset before the sequence on
+ //! `seqPlayerIndex` is requested to stop, i.e. before the sequence is disabled and setup
+ //! commands (including this command) can run. A simple fix would have been to unqueue based on
+ //! `gActiveSeqs[seqPlayerIndex].prevSeqId` instead
+ SEQCMD_UNQUEUE_SEQUENCE((u8)(seqPlayerIndex + (SEQCMD_ASYNC_ACTIVE >> 24)), 0,
+ gActiveSeqs[seqPlayerIndex].seqId);
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_RESTART_SEQ:
+ // Restart the currently active sequence on `targetSeqPlayerIndex` with full volume.
+ // Sequence on `targetSeqPlayerIndex` must still be active to play (can be muted)
+ SEQCMD_PLAY_SEQUENCE((u8)(targetSeqPlayerIndex + (SEQCMD_ASYNC_ACTIVE >> 24)), 1,
+ gActiveSeqs[targetSeqPlayerIndex].seqId);
+ gActiveSeqs[targetSeqPlayerIndex].fadeVolUpdate = true;
+ gActiveSeqs[targetSeqPlayerIndex].volScales[1] = 0x7F;
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_TEMPO_SCALE:
+ // Scale tempo by a multiplicative factor
+ SEQCMD_SCALE_TEMPO((u8)(targetSeqPlayerIndex + (SEQCMD_ASYNC_ACTIVE >> 24)), setupVal2,
+ setupVal1);
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_TEMPO_RESET:
+ // Reset tempo to previous tempo
+ SEQCMD_RESET_TEMPO((u8)(targetSeqPlayerIndex + (SEQCMD_ASYNC_ACTIVE >> 24)), setupVal1);
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_PLAY_SEQ:
+ // Play the requested sequence
+ // Uses the fade timer set by `SEQCMD_SUB_OP_SETUP_SET_FADE_TIMER`
+ seqId = gActiveSeqs[seqPlayerIndex].setupCmd[j] & 0xFFFF;
+ SEQCMD_PLAY_SEQUENCE((u8)(targetSeqPlayerIndex + (SEQCMD_ASYNC_ACTIVE >> 24)),
+ gActiveSeqs[targetSeqPlayerIndex].setupFadeTimer, seqId);
+ AudioSeq_SetVolumeScale(targetSeqPlayerIndex, VOL_SCALE_INDEX_FANFARE, 0x7F, 0);
+ gActiveSeqs[targetSeqPlayerIndex].setupFadeTimer = 0;
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_SET_FADE_TIMER:
+ // A command specifically to support `SEQCMD_SUB_OP_SETUP_PLAY_SEQ`
+ // Sets the fade timer for the sequence requested in `SEQCMD_SUB_OP_SETUP_PLAY_SEQ`
+ gActiveSeqs[seqPlayerIndex].setupFadeTimer = setupVal2;
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_RESTORE_SEQPLAYER_VOLUME_WITH_SCALE_INDEX:
+ // Restore the volume back to default levels
+ // Allows a `scaleIndex` to be specified.
+ AudioSeq_SetVolumeScale(targetSeqPlayerIndex, setupVal2, 0x7F, setupVal1);
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_POP_PERSISTENT_CACHE:
+ // Discard audio data by popping one more audio caches from the audio heap
+ if (setupVal1 & (1 << SEQUENCE_TABLE)) {
+ AUDIOCMD_GLOBAL_POP_PERSISTENT_CACHE(SEQUENCE_TABLE);
+ }
+ if (setupVal1 & (1 << FONT_TABLE)) {
+ AUDIOCMD_GLOBAL_POP_PERSISTENT_CACHE(FONT_TABLE);
+ }
+ if (setupVal1 & (1 << SAMPLE_TABLE)) {
+ AUDIOCMD_GLOBAL_POP_PERSISTENT_CACHE(SAMPLE_TABLE);
+ }
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_SET_CHANNEL_DISABLE_MASK:
+ // Disable (or reenable) specific channels of `targetSeqPlayerIndex`
+ channelMask = gActiveSeqs[seqPlayerIndex].setupCmd[j] & 0xFFFF;
+ SEQCMD_SET_CHANNEL_DISABLE_MASK((u8)(targetSeqPlayerIndex + (SEQCMD_ASYNC_ACTIVE >> 24)),
+ channelMask);
+ break;
+
+ case SEQCMD_SUB_OP_SETUP_SET_SEQPLAYER_FREQ:
+ // Scale all channels of `targetSeqPlayerIndex`
+ SEQCMD_SET_SEQPLAYER_FREQ((u8)(targetSeqPlayerIndex + (SEQCMD_ASYNC_ACTIVE >> 24)), setupVal2,
+ setupVal1 * 10);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ gActiveSeqs[seqPlayerIndex].setupCmdNum = 0;
+ }
+ }
+}
+
+// @recomp Patched to add the ability to turn off low health beeps.
+void LifeMeter_UpdateSizeAndBeep(PlayState* play) {
+ InterfaceContext* interfaceCtx = &play->interfaceCtx;
+
+ if (interfaceCtx->lifeSizeChangeDirection != 0) {
+ interfaceCtx->lifeSizeChange--;
+ if (interfaceCtx->lifeSizeChange <= 0) {
+ interfaceCtx->lifeSizeChange = 0;
+ interfaceCtx->lifeSizeChangeDirection = 0;
+ // @recomp Additional check for whether low health beeps are enabled.
+ if (recomp_get_low_health_beeps_enabled() && !Player_InCsMode(play) && (play->pauseCtx.state == PAUSE_STATE_OFF) &&
+ (play->pauseCtx.debugEditor == DEBUG_EDITOR_NONE) && LifeMeter_IsCritical() && !Play_InCsMode(play)) {
+ Audio_PlaySfx(NA_SE_SY_HITPOINT_ALARM);
+ }
+ }
+ } else {
+ interfaceCtx->lifeSizeChange++;
+ if ((s32)interfaceCtx->lifeSizeChange >= 10) {
+ interfaceCtx->lifeSizeChange = 10;
+ interfaceCtx->lifeSizeChangeDirection = 1;
+ }
+ }
+}
+
diff --git a/patches/syms.ld b/patches/syms.ld
index d2d5b4e..7e5c6a6 100644
--- a/patches/syms.ld
+++ b/patches/syms.ld
@@ -16,3 +16,5 @@ recomp_get_pending_warp = 0x8F000020;
recomp_powf = 0x8F000024;
recomp_get_target_framerate = 0x8F000028;
recomp_get_targeting_mode = 0x8F00002C;
+recomp_get_bgm_volume = 0x8F000030;
+recomp_get_low_health_beeps_enabled = 0x8F000034;
diff --git a/src/game/config.cpp b/src/game/config.cpp
index c66e858..b4d54e6 100644
--- a/src/game/config.cpp
+++ b/src/game/config.cpp
@@ -1,5 +1,6 @@
#include "recomp_config.h"
#include "recomp_input.h"
+#include "recomp_sound.h"
#include "../../ultramodern/config.hpp"
#include
#include
@@ -14,6 +15,7 @@
constexpr std::u8string_view graphics_filename = u8"graphics.json";
constexpr std::u8string_view controls_filename = u8"controls.json";
+constexpr std::u8string_view sound_filename = u8"sound.json";
constexpr auto res_default = ultramodern::Resolution::Auto;
constexpr auto wm_default = ultramodern::WindowMode::Windowed;
@@ -24,13 +26,26 @@ constexpr int rr_manual_default = 60;
constexpr bool developer_mode_default = false;
template
-void from_or_default(const json& j, const std::string& key, T& out, T default_value) {
+T from_or_default(const json& j, const std::string& key, T default_value) {
+ T ret;
auto find_it = j.find(key);
if (find_it != j.end()) {
- find_it->get_to(out);
+ find_it->get_to(ret);
}
else {
- out = default_value;
+ ret = default_value;
+ }
+
+ return ret;
+}
+
+template
+void call_if_key_exists(void (*func)(T), const json& j, const std::string& key) {
+ auto find_it = j.find(key);
+ if (find_it != j.end()) {
+ T val;
+ find_it->get_to(val);
+ func(val);
}
}
@@ -48,13 +63,13 @@ namespace ultramodern {
}
void from_json(const json& j, GraphicsConfig& config) {
- from_or_default(j, "res_option", config.res_option, res_default);
- from_or_default(j, "wm_option", config.wm_option, wm_default);
- from_or_default(j, "ar_option", config.ar_option, ar_default);
- from_or_default(j, "msaa_option", config.msaa_option, msaa_default);
- from_or_default(j, "rr_option", config.rr_option, rr_default);
- from_or_default(j, "rr_manual_value", config.rr_manual_value, rr_manual_default);
- from_or_default(j, "developer_mode", config.developer_mode, developer_mode_default);
+ config.res_option = from_or_default(j, "res_option", res_default);
+ config.wm_option = from_or_default(j, "wm_option", wm_default);
+ config.ar_option = from_or_default(j, "ar_option", ar_default);
+ config.msaa_option = from_or_default(j, "msaa_option", msaa_default);
+ config.rr_option = from_or_default(j, "rr_option", rr_default);
+ config.rr_manual_value = from_or_default(j, "rr_manual_value", rr_manual_default);
+ config.developer_mode = from_or_default(j, "developer_mode", developer_mode_default);
}
}
@@ -227,13 +242,8 @@ void load_controls_config(const std::filesystem::path& path) {
config_file >> config_json;
- recomp::TargetingMode targeting_mode;
- from_or_default(config_json["options"], "targeting_mode", targeting_mode, recomp::TargetingMode::Switch);
- recomp::set_targeting_mode(targeting_mode);
-
- int rumble_strength;
- from_or_default(config_json["options"], "rumble_strength", rumble_strength, 25);
- recomp::set_rumble_strength(rumble_strength);
+ recomp::set_targeting_mode(from_or_default(config_json["options"], "targeting_mode", recomp::TargetingMode::Switch));
+ recomp::set_rumble_strength(from_or_default(config_json["options"], "rumble_strength", 25));
if (!load_input_device_from_json(config_json, recomp::InputDevice::Keyboard, "keyboard")) {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
@@ -244,10 +254,33 @@ void load_controls_config(const std::filesystem::path& path) {
}
}
+void save_sound_config(const std::filesystem::path& path) {
+ nlohmann::json config_json{};
+
+ config_json["bgm_volume"] = recomp::get_bgm_volume();
+ config_json["low_health_beeps"] = recomp::get_low_health_beeps_enabled();
+
+ std::ofstream config_file{path};
+ config_file << std::setw(4) << config_json;
+}
+
+void load_sound_config(const std::filesystem::path& path) {
+ std::ifstream config_file{path};
+ nlohmann::json config_json{};
+
+ config_file >> config_json;
+
+
+ recomp::reset_sound_settings();
+ call_if_key_exists(recomp::set_bgm_volume, config_json, "bgm_volume");
+ call_if_key_exists(recomp::set_low_health_beeps_enabled, config_json, "set_low_health_beeps_enabled");
+}
+
void recomp::load_config() {
std::filesystem::path recomp_dir = recomp::get_app_folder_path();
std::filesystem::path graphics_path = recomp_dir / graphics_filename;
std::filesystem::path controls_path = recomp_dir / controls_filename;
+ std::filesystem::path sound_path = recomp_dir / sound_filename;
if (std::filesystem::exists(graphics_path)) {
load_graphics_config(graphics_path);
@@ -264,6 +297,14 @@ void recomp::load_config() {
recomp::reset_input_bindings();
save_controls_config(controls_path);
}
+
+ if (std::filesystem::exists(sound_path)) {
+ load_sound_config(sound_path);
+ }
+ else {
+ recomp::reset_sound_settings();
+ save_sound_config(sound_path);
+ }
}
void recomp::save_config() {
@@ -277,4 +318,5 @@ void recomp::save_config() {
save_graphics_config(recomp_dir / graphics_filename);
save_controls_config(recomp_dir / controls_filename);
+ save_sound_config(recomp_dir / sound_filename);
}
diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp
index dbb88b5..0a43364 100644
--- a/src/game/recomp_api.cpp
+++ b/src/game/recomp_api.cpp
@@ -3,9 +3,11 @@
#include "recomp.h"
#include "recomp_input.h"
#include "recomp_ui.h"
+#include "recomp_sound.h"
#include "recomp_helpers.h"
#include "../patches/input.h"
#include "../patches/graphics.h"
+#include "../patches/sound.h"
#include "../ultramodern/ultramodern.hpp"
#include "../ultramodern/config.hpp"
@@ -66,3 +68,12 @@ extern "C" void recomp_get_aspect_ratio(uint8_t* rdram, recomp_context* ctx) {
extern "C" void recomp_get_targeting_mode(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast(recomp::get_targeting_mode()));
}
+
+
+extern "C" void recomp_get_bgm_volume(uint8_t* rdram, recomp_context* ctx) {
+ _return(ctx, recomp::get_bgm_volume() / 100.0f);
+}
+
+extern "C" void recomp_get_low_health_beeps_enabled(uint8_t* rdram, recomp_context* ctx) {
+ _return(ctx, static_cast(recomp::get_low_health_beeps_enabled()));
+}
diff --git a/src/game/scene_table.cpp b/src/game/scene_table.cpp
index d091770..4301788 100644
--- a/src/game/scene_table.cpp
+++ b/src/game/scene_table.cpp
@@ -154,7 +154,7 @@ std::vector recomp::game_warps {
"From Laundry Pool",
"From East Clock Town (South entrance)",
"Clock Tower balcony",
- "From Song of Soaring",
+ "Owl Statue",
"First song of time cutscene"
}
},
@@ -168,17 +168,17 @@ std::vector recomp::game_warps {
{ "Swamp", {
{
12, "Southern Swamp (After Woodfall Temple)", {
- "-swamp road",
- "-boat house",
- "-woodfall",
- "-lower deku palace",
- "-upper deku palace",
- "-hags potion shop",
- "-boat cruise",
- "-woods of mystery",
- "-swamp spider house",
- "-ikana canyon",
- "-owl statue",
+ "From Road",
+ "In Front of Boat House",
+ "Froom Woodfall",
+ "From Lower Deku Palace",
+ "From Upper Deku Palace",
+ "From Magic Hags' Potion Shop",
+ "Boat Ride",
+ "From Woods of Mistery",
+ "From Swamp Spider House",
+ "From Ikanya Canyon",
+ "Owl Statue",
}
},
{
@@ -212,67 +212,67 @@ std::vector recomp::game_warps {
80, "Deku Palace", {
"From Southern Swamp",
"After getting caught",
- "-deku king chamber",
- "-deku king chamber (upper)",
- "-deku shrine",
- "From Southern Swamp (Alternate)",
- "-jp grotto left, first room",
- "-jp grotto left, second room",
- "-jp grotto right, second room",
+ "From Deku King Chamber",
+ "From Upper Deku King Chamber",
+ "From Deku Shrine",
+ "From Southern Swamp (Upper tunnel)",
+ "From Left Grotto (Japanese)",
+ "From Left Grotto Second Room (Japanese)",
+ "From Right Grotto Second Room (Japanese)",
"From Bean Seller Grotto",
- "-jp grotto right, first room",
+ "From Right Grotto First Room (Japanese)",
}
},
{
118, "Deku Palace Royal Chamber", {
- "-deku palace",
- "-deku palace (upper)",
- "-monkey released",
- "-front of king",
+ "From Deku Palace",
+ "From Upper Deku Palace",
+ "After Releasing Monkey",
+ "In Front of the King",
}
},
{
122, "Road to Southern Swamp", {
- "-termina field",
- "-southern swamp",
- "-swamp shooting gallery",
+ "From Termina Field",
+ "From Southern Swamp",
+ "From Swamp Shooting Gallery",
}
},
{
132, "Southern Swamp (Before Woodfall Temple)", {
- "-road to southern swamp",
- "-boat house",
- "-woodfall",
- "-deku palace",
- "-deku palace (shortcut)",
- "-hags potion shop",
- "-boat ride",
- "-woods of mystery",
- "-swamp spider house",
- "-ikana canyon",
- "-owl statue",
+ "From Road to Southern Swamp",
+ "In Front of Boat House",
+ "From Woodfall",
+ "From Deku Palace",
+ "From Deku Palace (Shortcut)",
+ "From Hags' Potion Shop",
+ "Boat Ride",
+ "From Woods of Mistery",
+ "From Swamp Spider House",
+ "From Ikana Canyon",
+ "Owl Statue",
}
},
{
134, "Woodfall", {
- "-southern swamp",
- "-unknown",
- "-fairy fountain",
- "-unknown",
- "-owl statue",
+ "From Southern Swamp",
+ "In Mid-Air",
+ "-From Fairy Mountain",
+ "In Mid-Air (alternate)",
+ "Owl Statue",
}
},
{
158, "Deku Shrine", {
- "-deku palace",
- "-deku palace"
+ "From Deku Palace",
+ "From Deku Palace"
}
},
{
168, "Swamp Tourist Center", {
"Entrance",
- "-koume",
- "-tingle's dad",
+ "Talking to Koume",
+ "Talking to Tingle's Dad",
}
},
{
@@ -319,117 +319,117 @@ std::vector recomp::game_warps {
},
{
138, "Goron Village (After Snowhead Temple)", {
- "-path to goron village (spring)",
- "-unknown",
- "-goron shrine",
- "-lens of truth",
- "-void out",
+ "From Path to Goron Village",
+ "In Mid-Air",
+ "From Goron Shrine",
+ "Over the Void",
+ "In Front of Invisible Platforms",
}
},
{
148, "Goron Village (Before Snowhead Temple)", {
- "-path to goron village (winter)",
- "-deku flower",
- "-goron shrine",
- "-lens of truth",
- "-void out",
+ "From Path to Goron Village",
+ "In Front of Deku Flower",
+ "From Goron Shrine",
+ "From Lens of Truth",
+ "In Front of Invisible Platforms",
}
},
{
150, "Goron Graveyard", {
- "-mountain village",
- "-receiving goron mask",
+ "-From Mountain Village",
+ "-After Receiving Goron Mask",
}
},
{
154, "Mountain Village (Before Snowhead Temple)", {
- "-after snowhead",
- "-mountain smithy",
- "-path to goron village (winter)",
- "-goron graveyard",
- "-path to snowhead",
- "-on ice",
- "-path to mountain village",
- "-unknown",
- "-owl statue",
+ "In Front of Mountain Smithy",
+ "Mountain Smithy",
+ "From Path to Goron Village",
+ "From Goron Graveyard",
+ "From Path to Snowhead",
+ "On the lake",
+ "From Path to Mountain Village",
+ "On the Lake (alternate)",
+ "Owl Statue",
}
},
{
174, "Mountain Village (After Snowhead Temple)", {
- "-after snowhead",
- "-mountain smithy",
- "-path to goron village (spring)",
- "-goron graveyard",
- "-path to snowhead",
- "-behind waterfall",
- "-path to mountain village",
- "-after snowhead (cutscene)",
- "-owl statue",
+ "Next to Lake",
+ "From Mountain Smithy",
+ "From Path to Goron Village",
+ "From Goron Graveyard",
+ "From Path to Snowhead",
+ "Behind Waterfall",
+ "From Path to Mountain Village",
+ "Next to Lake (After Snowhead Cutscene)",
+ "Owl Statue",
}
},
{
178, "Snowhead", {
- "-path to snowhead",
- "-snowhead temple",
- "-fairy fountain",
- "-owl statue",
+ "From Path to Snowhead",
+ "From Snowhead Temple",
+ "From Fairy Fountain",
+ "Owl Statue",
}
},
{
180, "Road to Goron Village (Before Snowhead Temple)", {
- "-mountain village (winter)",
- "-goron village (winter)",
- "-goron racetrack",
+ "From Mountain Village",
+ "From Goron Village",
+ "From Goron Racetrack",
}
},
{
182, "Road to Goron Village (After Snowhead Temple)", {
- "-mountain village (spring)",
- "-goron village (spring)",
- "-goron racetrack",
+ "From Mountain Village",
+ "From Goron Village",
+ "From Goron Racetrack",
}
},
{
208, "Goron Racetrack", {
- "-path to mountain village",
- "-race start",
- "-race end",
+ "From Path to Mountain Village",
+ "Race Start",
+ "Race End",
}
}
}},
{ "Great Bay", {
{
34, "Pirates' Fortress (Outdoors)", {
- "-exterior pirates fortress",
- "-lower hookshot room",
- "-upper hookshot room",
- "-silver rupee room",
- "-silver rupee room exit",
- "-barrel room",
- "-barrel room exit",
- "-twin barrel room",
- "-twin barrel room exit",
- "-oob near twin barrel",
- "-telescope",
- "-oob hookshot room",
- "-balcony",
- "-upper hookshot room",
+ "From Exterior Pirate Fortress",
+ "From Lower Hookshoot Room",
+ "From Upper Hookshoot Room",
+ "From Silver Rupee Room",
+ "From Silver Rupee Room (alternate)",
+ "From Room with Barrels",
+ "From Room with Barrels (alternate)",
+ "From Room with Barrels and Bridge",
+ "From Room with Barrels and Bridge (alternate)",
+ "Out of bounds",
+ "Telescope",
+ "Out of bounds (alternate)",
+ "From Balcony in Exterior Pirate Fortress",
+ "From Upper Hookshoot Room",
}
},
{
64, "Pirates' Fortress (Indoors)", {
- "-hookshot room",
- "-hookshot room upper",
- "-100 rupee room",
- "-100 rupee room (egg)",
- "-barrel room",
- "-barrel room (egg)",
- "-twin barrel room",
- "-twin barrel room (egg)",
- "-telescope",
- "-outside, underwater",
- "-outside, telescope",
- "-unknown",
+ "Hookshoot Room",
+ "Upper Hookshoot Room",
+ "Silver Rupee Room",
+ "100 Rupee Room (Next to Egg)",
+ "Barrel Room",
+ "Barrel Room (Next to Egg)",
+ "Room with Barrels and Bridge",
+ "Room with Barrels and Bridge (next to egg)",
+ "Telescope",
+ "Hidden Entrance ",
+ "Telescope",
+ "Unloaded Room",
}
},
{
@@ -450,58 +450,58 @@ std::vector recomp::game_warps {
},
{
96, "Zora Hall", {
- "-zora cape",
- "-zora cape (turtle)",
- "-zora shop",
- "-lulu's room",
- "-evan's room",
- "-japa's room",
- "-mikau & tijo's room",
- "-stage",
- "-after rehearsal",
+ "From Zora Cape",
+ "From Zora Cape with Turtle",
+ "From Zora Shop",
+ "From Lulu's Room",
+ "From Evan's Room",
+ "From Japa's Room",
+ "From Mikau's & Tijo's Room",
+ "Stage",
+ "Stage (After Rehearsal)",
}
},
{
104, "Great Bay Coast", {
- "-termina field",
+ "From Termina Field",
"-zora cape",
- "-void respawn",
- "-pinnacle rock",
- "-fisherman hut",
- "-pirates fortress",
- "-void resapwn (murky water)",
- "-marine lab",
- "-oceanside spider house",
- "-during zora mask",
- "-after zora mask",
- "-owl statue",
- "-thrown out",
- "-after jumping game",
+ "From Zora Cape",
+ "From Pinnacle Rock",
+ "From Fisherman's Hut",
+ "From Pirates' Fortress",
+ "Next to Chuchu",
+ "From Marine Lab",
+ "From Oceanside Spider House",
+ "Beach (Zora Mask Cutscene)",
+ "Beach (After Zora Mask Cutscene)",
+ "Owl Statue",
+ "Thrown Out Pirates' Fortress",
+ "Island (After jumping game)",
}
},
{
106, "Zora Cape", {
- "-great bay coast",
- "-zora hall",
- "-zora hall (turtle)",
- "-void respawn",
- "-waterfall",
- "-fairy fountain",
- "-owl statue",
- "-great bay temple",
- "-after great bay temple",
- "-unknown",
+ "From Great Bay Coast",
+ "From Zora Hall",
+ "From Zora Hall with Turtle",
+ "Next to Zora Game Site",
+ "From Waterfall Rapids",
+ "From Fairy Fountain",
+ "From Owl Statue",
+ "From Great Bay Temple",
+ "After Beating Great Bay Temple",
+ "After Beating Great Bay Temple",
}
},
{
112, "Pirates' Fortress (Entrance)", {
- "-great bay coast",
- "-pirates fortress",
- "-underwater passage",
- "-underwater jet",
- "-kicked out",
- "-hookshot platform",
- "-passage door",
+ "From Great Bay Coast",
+ "From Pirates' Fortress",
+ "From Secret Entrance",
+ "From Underwater Jet",
+ "Kicked out",
+ "From Hookshot Platform in Pirates' Fortress",
+ "From Telescope Room",
}
},
{
@@ -517,53 +517,53 @@ std::vector recomp::game_warps {
},
{
142, "Waterfall Rapids", {
- "-zora cape",
- "-race start",
- "-race end",
- "-game won",
+ "From Zora Cape",
+ "Race Start",
+ "Race End",
+ "Race Won",
}
},
{
146, "Zora Hall (Room)", {
- "-mikau from zora hall",
- "-japas from zora hall",
- "-lulu from zora hall",
- "-evan from zora hall",
- "-japa after jam session",
- "-zora shop from zora hall",
- "-evan after composing song",
+ "Mikau's Room",
+ "Japas' Room",
+ "Lulu's Room",
+ "Evan's Room",
+ "Japa's Room (after jam session)",
+ "Zora's Shop",
+ "Evan's Room (after composing song)",
}
},
{
184, "Gyorg Arena", {
- "-great bay temple",
- "-falling cutscene",
+ "Entrance",
+ "Falling Cutscene",
}
},
{
- 190, "-great bay (cutscene)", {
- "zora cape",
+ 190, "Great Bay (Pirate and Turtle Cutscene)", {
+ "From Zora Cape",
}
}
}},
{ "Ikana", {
{
32, "Ikana Canyon", {
- "-ikana road",
- "-ghost hut",
- "-music box house",
- "-stone tower",
- "-owl statue",
- "-beneath the well",
- "-sakon's hideout",
- "-after stone tower",
- "-ikana castle",
- "-after house opens",
- "-song of storms cave (house open)",
- "-fairy fountain",
- "-secret shrine",
- "-from song of storms cave",
- "-song of storms cave (house closed) ",
+ "From Ikana Road",
+ "From Ghost Hut",
+ "From Music Box House",
+ "From Stone Tower",
+ "Owl Statue",
+ "From Beneath the Well",
+ "From Sakon's Hideout",
+ "After Beating Stone Tower Temple",
+ "From Ikana Castle",
+ "House Opening Cutscene",
+ "Spring Water Cave (played Song of Storms)",
+ "From Fairy Fountain",
+ "From Secret Shrine",
+ "From Spring Water Cave",
+ "Spring Water Cave",
}
},
{
@@ -609,18 +609,18 @@ std::vector recomp::game_warps {
},
{
128, "Ikana Graveyard", {
- "-road to ikana",
- "-grave 1",
- "-grave 2",
- "-grave 3",
- "-dampe's house",
- "-after keeta defeated",
+ "Road to Ikana",
+ "From Grave 1 ",
+ "From Grave 2",
+ "From Grave 3",
+ "From Dampe's House",
+ "Keeta Defeated Cutscene",
}
},
{
144, "Beneath the Well", {
- "-ikana canyon",
- "-ikana castle",
+ "From Ikana Canyon",
+ "From Ikana Castle",
}
},
{
@@ -631,15 +631,15 @@ std::vector recomp::game_warps {
{
156, "Spirit House", {
"Entrance",
- "-after minigame",
- "-beat minigame",
+ "Minigame start",
+ "After minigame",
}
},
{
160, "Road to Ikana", {
- "-termina field",
- "-ikana canyon",
- "-ikana graveyard",
+ "From Termina Field",
+ "From Ikana Canyon",
+ "From Ikana Graveyard",
}
},
{
@@ -654,16 +654,16 @@ std::vector recomp::game_warps {
},
{
170, "Stone Tower", {
- "-ikana canyon",
- "-unknown",
- "-stone tower temple",
- "-owl statue",
+ "From Ikana Canyon",
+ "In Front of Temple",
+ "From Stone Tower Temple",
+ "Owl Statue",
}
},
{
172, "Stone Tower (Inverted)", {
- "-after inverting",
- "-stone tower temple",
+ "In Front of Temple (Inverting Cutscene)",
+ "From Stone Tower Temple",
}
},
{
@@ -682,22 +682,21 @@ std::vector recomp::game_warps {
},
{
84, "Termina Field", {
- "-west clock town",
- "-road to southern swamp",
- "-great bay coast",
- "-path to mountain village",
- "-road to ikana",
- "-milk road",
- "-south clock town",
- "-east clock town",
- "-north clock town",
- "-observatory",
- "-observatory (telescope)",
- "-near ikana",
- "-moon crash",
- "-cremia hug",
- "-skullkid cutscene",
- "-west clock town",
+ "From West Clock Town",
+ "From Road to Southern Swapm",
+ "From Great Bay Coast",
+ "From Path to Mountain Village",
+ "From Road to Ikana",
+ "From Milk Road",
+ "From South Clock Town",
+ "From East Clock Town",
+ "From North Clock Town",
+ "From Observatory",
+ "Use Telescope",
+ "Near Ikana",
+ "Moon Crash Cutscene (Game Over)",
+ "Next to Ikana (After Cremia's Hug)",
+ "Next to Road to Southern Swamp (After Skull Kid Cutscene)"
}
}
}},
@@ -712,11 +711,11 @@ std::vector recomp::game_warps {
62, "Milk Road", {
"From Termina Field",
"From Romani Ranch",
- "-gorman track (track exit)",
- "-gorman track (main exit)",
+ "From Gorman's Track (Track Exit)",
+ "From Gorman's Track (Main Exit)",
"At Owl Statue",
- "5?",
- "6?",
+ "Behind Giant Rock",
+ "Next to Owl Statue",
}
},
{
@@ -737,25 +736,24 @@ std::vector recomp::game_warps {
},
{
124, "Doggy Racetrack", {
- "-romani ranch",
- "-after race",
+ "From Romani Ranch",
+ "Next to Track (After Race)",
}
},
{
126, "Cucco Shack", {
- "-romani ranch",
- "-after bunny hood",
+ "From Romani Ranch",
+ "Talking to Grog (Getting Bunny Hood)",
}
},
{
206, "Gorman Track", {
- "-milk road",
- "-unknown",
- "-beat minigame",
- "-milk road behind fence",
- "-milk road fence cutscene",
- "-unknown",
- "-start minigame",
+ "From Milk Road",
+ "Next to Gorman",
+ "Next to Gorman (After Beating Race)",
+ "From Milk Road (Behind Fence)",
+ "From Milk Road (After Fence Cutscene)",
+ "In the Middle of the Track"
}
}
}},
@@ -815,12 +813,12 @@ std::vector recomp::game_warps {
}
},
{
- 46, "-before clock town", {
- "-falling from cliff",
- "-inside clock tower",
- "-transformed to deku",
- "-void respawn",
- "-song of time flashback",
+ 46, "Intro Areas", {
+ "Falling from Cliff Cutscene",
+ "Before Entering Clock Tower",
+ "After Being Transformed into Deku",
+ "Before Entering Clock Tower (Void Respawn)",
+ "South Clock Town (After First Song of Time)",
}
},
{
diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp
index 2a6202f..60f55a4 100644
--- a/src/ui/ui_config.cpp
+++ b/src/ui/ui_config.cpp
@@ -1,5 +1,6 @@
#include "recomp_ui.h"
#include "recomp_input.h"
+#include "recomp_sound.h"
#include "recomp_config.h"
#include "recomp_debug.h"
#include "../../ultramodern/config.hpp"
@@ -10,6 +11,7 @@ ultramodern::GraphicsConfig new_options;
Rml::DataModelHandle graphics_model_handle;
Rml::DataModelHandle controls_model_handle;
Rml::DataModelHandle control_options_model_handle;
+Rml::DataModelHandle sound_options_model_handle;
// True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
bool configuring_controller = false;
@@ -45,6 +47,21 @@ void bind_option(Rml::DataModelConstructor& constructor, const std::string& name
);
};
+template
+void bind_atomic(Rml::DataModelConstructor& constructor, Rml::DataModelHandle handle, const char* name, std::atomic* atomic_val) {
+ constructor.BindFunc(name,
+ [atomic_val](Rml::Variant& out) {
+ out = atomic_val->load();
+ printf("out: %s\n", out.Get().c_str());
+ },
+ [atomic_val, handle, name](const Rml::Variant& in) mutable {
+ printf("in: %s\n", in.Get().c_str());
+ atomic_val->store(in.Get());
+ handle.DirtyVariable(name);
+ }
+ );
+}
+
static int scanned_binding_index = -1;
static int scanned_input_index = -1;
static int focused_input_index = -1;
@@ -100,6 +117,49 @@ void recomp::set_targeting_mode(recomp::TargetingMode mode) {
}
}
+struct SoundOptionsContext {
+ std::atomic bgm_volume;
+ std::atomic low_health_beeps_enabled; // RmlUi doesn't seem to like "true"/"false" strings for setting variants so an int is used here instead.
+ void reset() {
+ bgm_volume = 100;
+ low_health_beeps_enabled = (int)true;
+ }
+ SoundOptionsContext() {
+ reset();
+ }
+};
+
+SoundOptionsContext sound_options_context;
+
+void recomp::reset_sound_settings() {
+ sound_options_context.reset();
+ if (sound_options_model_handle) {
+ sound_options_model_handle.DirtyAllVariables();
+ }
+}
+
+void recomp::set_bgm_volume(int volume) {
+ sound_options_context.bgm_volume.store(volume);
+ if (sound_options_model_handle) {
+ sound_options_model_handle.DirtyVariable("bgm_volume");
+ }
+}
+
+int recomp::get_bgm_volume() {
+ return sound_options_context.bgm_volume.load();
+}
+
+void recomp::set_low_health_beeps_enabled(bool enabled) {
+ sound_options_context.low_health_beeps_enabled.store((int)enabled);
+ if (sound_options_model_handle) {
+ sound_options_model_handle.DirtyVariable("low_health_beeps_enabled");
+ }
+}
+
+bool recomp::get_low_health_beeps_enabled() {
+ return (bool)sound_options_context.low_health_beeps_enabled.load();
+}
+
struct DebugContext {
Rml::DataModelHandle model_handle;
std::vector area_names;
@@ -386,6 +446,18 @@ public:
control_options_model_handle = constructor.GetModelHandle();
}
+
+ void make_sound_options_bindings(Rml::Context* context) {
+ Rml::DataModelConstructor constructor = context->CreateDataModel("sound_options_model");
+ if (!constructor) {
+ throw std::runtime_error("Failed to make RmlUi data model for the sound options menu");
+ }
+
+ sound_options_model_handle = constructor.GetModelHandle();
+
+ bind_atomic(constructor, sound_options_model_handle, "bgm_volume", &sound_options_context.bgm_volume);
+ bind_atomic(constructor, sound_options_model_handle, "low_health_beeps_enabled", &sound_options_context.low_health_beeps_enabled);
+ }
void make_debug_bindings(Rml::Context* context) {
Rml::DataModelConstructor constructor = context->CreateDataModel("debug_model");
@@ -413,6 +485,7 @@ public:
make_graphics_bindings(context);
make_controls_bindings(context);
make_control_options_bindings(context);
+ make_sound_options_bindings(context);
make_debug_bindings(context);
}
};
diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp
index 62d6b4b..28bf0d9 100644
--- a/src/ui/ui_renderer.cpp
+++ b/src/ui/ui_renderer.cpp
@@ -1273,6 +1273,7 @@ void recomp::set_config_submenu(recomp::ConfigSubmenu submenu) {
void recomp::destroy_ui() {
std::lock_guard lock {ui_context_mutex};
+ Rml::Debugger::Shutdown();
ui_context->rml.font_interface.reset();
Rml::Shutdown();
ui_context->rml.unload();