Added background music volume and low health beep settings, imported warp names

This commit is contained in:
Mr-Wiseguy 2024-03-10 16:40:41 -04:00
parent 0ceeeb04ea
commit eeeabba64d
11 changed files with 788 additions and 260 deletions

View File

@ -63,7 +63,7 @@
<div>Sound</div>
<div class="tab__indicator"></div>
</tab>
<panel class="config" >
<panel class="config" data-model="sound_options_model">
<template src="config-menu__sound" />
</panel>
<tab class="tab">

View File

@ -4,7 +4,24 @@
<body>
<form class="config__form">
<div class="config__wrapper">
Sound options!!!!!
<div class="config__row">
<div class="config-option">
<label class="config-option__title">Background Music Volume</label>
<div class="config-option__range-wrapper config-option__list">
<label class="config-option__range-label">{{bgm_volume}}</label>
<input class="nav-vert" id="bgm_volume_input" type="range" min="0" max="100" style="flex: 1; margin: 0dp;" data-value="bgm_volume"/>
</div>
</div>
<div class="config-option">
<label class="config-option__title">Low Health Beeps</label>
<div class="config-option__list config-option__list">
<input type="radio" name="lhb" data-checked="low_health_beeps_enabled" value="1" id="lhb_on"/>
<label class="config-option__tab-label" for="lhb_on">On</label>
<input type="radio" name="lhb" data-checked="low_health_beeps_enabled" value="0" id="lhb_off"/>
<label class="config-option__tab-label" for="lhb_off">Off</label>
</div>
</div>
</div>
</div>
</form>
</body>

12
include/recomp_sound.h Normal file
View File

@ -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

9
patches/sound.h Normal file
View File

@ -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

363
patches/sound_patches.c Normal file
View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -1,5 +1,6 @@
#include "recomp_config.h"
#include "recomp_input.h"
#include "recomp_sound.h"
#include "../../ultramodern/config.hpp"
#include <filesystem>
#include <fstream>
@ -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 <typename T>
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 <typename T>
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);
}

View File

@ -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<int>(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<u32>(recomp::get_low_health_beeps_enabled()));
}

View File

@ -154,7 +154,7 @@ std::vector<recomp::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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::AreaWarps> 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)",
}
},
{

View File

@ -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 <typename T>
void bind_atomic(Rml::DataModelConstructor& constructor, Rml::DataModelHandle handle, const char* name, std::atomic<T>* atomic_val) {
constructor.BindFunc(name,
[atomic_val](Rml::Variant& out) {
out = atomic_val->load();
printf("out: %s\n", out.Get<std::string>().c_str());
},
[atomic_val, handle, name](const Rml::Variant& in) mutable {
printf("in: %s\n", in.Get<std::string>().c_str());
atomic_val->store(in.Get<T>());
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<int> bgm_volume;
std::atomic<int> 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<std::string> 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);
}
};

View File

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