Merge branch 'main' into improvement/toggle_owl_statue_markers

This commit is contained in:
MelonSpeedruns
2026-05-19 10:10:11 -04:00
committed by GitHub
111 changed files with 2463 additions and 855 deletions
+5
View File
@@ -53,6 +53,11 @@ static std::string FormatToString(const char* msg, va_list list) {
size *= 2;
}
}
while (!str.empty() && str[str.size()-1] == '\n') {
str.pop_back();
}
return str;
}
+12 -3
View File
@@ -191,13 +191,17 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}
bool hasJewelRod = false;
for (int slot = 0; slot < 24 && !hasJewelRod; ++slot) {
bool hasAncientDoc = false;
for (int slot = 0; slot < 24; ++slot) {
const u8 item = dComIfGs_getItem(slot, false);
if (item == dItemNo_JEWEL_ROD_e || item == dItemNo_JEWEL_BEE_ROD_e || item == dItemNo_JEWEL_WORM_ROD_e) {
hasJewelRod = true;
}
if (item == dItemNo_ANCIENT_DOCUMENT_e || item == dItemNo_ANCIENT_DOCUMENT2_e || item == dItemNo_AIR_LETTER_e) {
hasAncientDoc = true;
}
}
if (!hasJewelRod) {
if (!hasJewelRod || !hasAncientDoc) {
return;
}
@@ -212,7 +216,6 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
dItemNo_KANTERA_e,
dItemNo_PACHINKO_e,
dItemNo_HAWK_EYE_e,
dItemNo_ANCIENT_DOCUMENT_e,
dItemNo_HORSE_FLUTE_e,
};
for (u8 required : requiredWheelItems) {
@@ -692,6 +695,12 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
return;
}
// prevent stuff like https://github.com/TwilitRealm/dusklight/issues/949
if (link->getDemoMode() != 0) {
inJump = false;
return;
}
if (!inJump) {
if (link->mProcID == daAlink_c::PROC_CUT_JUMP) {
inJump = true;
+96
View File
@@ -0,0 +1,96 @@
#include "dusk/action_bindings.h"
#include "aurora/lib/input.hpp"
#include "dusk/settings.h"
#include "dusk/ui/ui.hpp"
namespace dusk {
static std::array<std::array<ActionBindPressData, static_cast<int>(ActionBinds::COUNT)>, PAD_CHANMAX> actionPressData{};
ActionBindsMap& getActionBinds() {
static ActionBindsMap actionBinds = {
{ActionBinds::FIRST_PERSON_CAMERA, {&getSettings().actionBindings.firstPersonCamera, "First Person Camera"}},
{ActionBinds::CALL_MIDNA, {&getSettings().actionBindings.callMidna, "Call Midna"}},
{ActionBinds::OPEN_DUSKLIGHT_MENU, {&getSettings().actionBindings.openDusklightMenu, "Open Dusklight Menu"}},
{ActionBinds::TURBO_SPEED_BUTTON, {&getSettings().actionBindings.turboSpeedButton, "Turbo Speed Button"}},
};
return actionBinds;
}
bool isActionBound(ActionBinds action, u32 port) {
auto& actionBinds = getActionBinds();
// Check to make sure action is properly bound
if (!actionBinds.contains(action)) {
return false;
}
return getActionBindButton(action, port) != PAD_NATIVE_BUTTON_INVALID;
}
void updateActionBindings() {
for (u32 port = 0; port < PAD_CHANMAX; ++port) {
// Move the current press to the previous frame
for (auto& pressData : actionPressData[port]) {
pressData.pressedPrevFrame = pressData.pressedCurFrame;
pressData.pressedCurFrame = false;
}
// Update current frame with whether action button is pressed
for (auto& [action, boundAction] : getActionBinds()) {
// If the action isn't bound, or if documents are visible and the action isn't
// opening the dusklight menu, don't update. Otherwise, we may accidentally
// perform actions while the dusklight menu is open.
if (!isActionBound(action, port) ||
(ui::any_document_visible() && action != ActionBinds::OPEN_DUSKLIGHT_MENU)) {
continue;
}
int button = boundAction.configVars->at(port);
// If keyboard is active for this port
u32 count = 0;
if (PADGetKeyButtonBindings(port, &count) != nullptr) {
int numKeys = 0;
const bool* kbState = SDL_GetKeyboardState(&numKeys);
if (kbState[button]) {
actionPressData[port][static_cast<int>(action)].pressedCurFrame = true;
}
} else {
// If controller is active
auto controller = aurora::input::get_controller_for_player(port);
if (controller) {
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(button))) {
actionPressData[port][static_cast<int>(action)].pressedCurFrame = true;
}
}
}
}
}
}
bool getActionBindTrig(ActionBinds action, u32 port) {
return isActionBound(action, port) &&
actionPressData[port][static_cast<int>(action)].pressedCurFrame &&
!actionPressData[port][static_cast<int>(action)].pressedPrevFrame;
}
bool getActionBindHold(ActionBinds action, u32 port) {
return isActionBound(action, port) &&
actionPressData[port][static_cast<int>(action)].pressedCurFrame &&
actionPressData[port][static_cast<int>(action)].pressedPrevFrame;
}
bool getActionBindHoldAnyPort(ActionBinds action) {
for (u32 port = 0; port < PAD_CHANMAX; ++port) {
if (getActionBindHold(action, port)) {
return true;
}
}
return false;
}
int getActionBindButton(ActionBinds action, u32 port) {
return (*getActionBinds()[action].configVars)[port];
}
}
+31 -6
View File
@@ -2,6 +2,7 @@
#include "dusk/ui/ui.hpp"
#include "imgui/ImGuiConsole.hpp"
bool shouldAutoSave = false;
u8 mSaveBuffer[QUEST_LOG_SIZE * 3];
u8 mAutoSaveProc = 0;
int autoSaveWriteState = 0;
@@ -13,9 +14,23 @@ static AutoSaveFuncs AutoSaveFuncsProc[] = {
void noAutoSave() {}
bool canAutoSave() {
daAlink_c* player = (daAlink_c*)daAlink_getAlinkActorClass();
if (player == nullptr) {
return false;
}
if (player->checkCargoCarry() || player->checkCanoeRide()) {
return false;
}
return dusk::getSettings().game.autoSave && shouldAutoSave && mAutoSaveProc == 0 &&
strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0 &&
strcmp(dComIfGp_getStartStageName(), "F_SP112") != 0;
}
void triggerAutoSave() {
if (dusk::getSettings().game.autoSave && mAutoSaveProc == 0 &&
strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0)
if (canAutoSave())
{
mAutoSaveProc = 1;
}
@@ -25,8 +40,12 @@ void updateAutoSave() {
(AutoSaveFuncsProc[mAutoSaveProc])();
}
void writeAutoSave() {
int stageNo = dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo());
bool writeAutoSave() {
stage_stag_info_class* stagInfo = dComIfGp_getStageStagInfo();
if (stagInfo == nullptr) {
return false;
}
int stageNo = dStage_stagInfo_GetSaveTbl(stagInfo);
dComIfGs_putSave(stageNo);
dComIfGs_setMemoryToCard(mSaveBuffer, dComIfGs_getDataNum());
@@ -39,6 +58,7 @@ void writeAutoSave() {
}
g_mDoMemCd_control.save(mSaveBuffer, sizeof(mSaveBuffer), 0);
return true;
}
void autoSaving() {
@@ -47,8 +67,9 @@ void autoSaving() {
if (cardState == 2) {
mAutoSaveProc = 1;
} else if (cardState == 1) {
writeAutoSave();
mAutoSaveProc = 3;
if (writeAutoSave()) {
mAutoSaveProc = 3;
}
}
}
}
@@ -89,4 +110,8 @@ void endAutoSave() {
.duration = std::chrono::milliseconds(1500),
});
mAutoSaveProc = 0;
}
void toggleAutoSave(bool enabled) {
shouldAutoSave = enabled;
}
+30 -2
View File
@@ -11,6 +11,7 @@
#include <string>
#include "dusk/main.h"
#include "dusk/action_bindings.h"
using namespace dusk::config;
@@ -55,12 +56,29 @@ static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
template<ConfigValue T>
void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
if constexpr (std::is_enum_v<T>) {
if (jsonValue.is_boolean()) {
using Underlying = std::underlying_type_t<T>;
const bool b = jsonValue.get<bool>();
Underlying raw;
if constexpr (std::is_same_v<T, dusk::FrameInterpMode>) {
raw = b ? static_cast<Underlying>(2) : static_cast<Underlying>(0);
} else {
raw = b ? static_cast<Underlying>(1) : static_cast<Underlying>(0);
}
cVar.setValue(sanitizeEnumValue(cVar, static_cast<T>(raw)), false);
return;
}
}
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<T>()), false);
}
template<ConfigValue T>
nlohmann::json ConfigImpl<T>::dumpToJson(const ConfigVar<T>& cVar) {
return cVar.getValue();
return cVar.getValueForSave();
}
template<ConfigValue T> requires std::is_integral_v<T> && std::is_signed_v<T>
@@ -157,6 +175,8 @@ namespace dusk::config {
template class ConfigImpl<dusk::DiscVerificationState>;
template class ConfigImpl<dusk::GameLanguage>;
template class ConfigImpl<dusk::GyroMode>;
template class ConfigImpl<dusk::FrameInterpMode>;
template class ConfigImpl<dusk::Resampler>;
}
void dusk::config::Register(ConfigVarBase& configVar) {
@@ -248,7 +268,8 @@ void dusk::config::Save() {
json j;
for (const auto& pair : RegisteredConfigVars) {
if (pair.second->getLayer() == ConfigVarLayer::Value) {
const auto layer = pair.second->getLayer();
if (layer == ConfigVarLayer::Value || layer == ConfigVarLayer::Speedrun) {
j[pair.first] = pair.second->getImpl()->dumpToJson(*pair.second);
}
}
@@ -256,6 +277,13 @@ void dusk::config::Save() {
io::FileStream::WriteAllText(reinterpret_cast<const char*>(configJsonPath.c_str()), j.dump(4));
}
void dusk::config::ClearAllActionBindings(int port) {
for (auto& actionBinding : getActionBinds() | std::views::values) {
actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID);
}
Save();
}
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
const auto configVar = RegisteredConfigVars.find(name);
if (configVar != RegisteredConfigVars.end()) {
+1 -1
View File
@@ -61,7 +61,7 @@ std::string release_name() {
}
std::filesystem::path sentry_database_path() {
return dusk::ConfigPath / "sentry";
return dusk::CachePath / "sentry";
}
std::filesystem::path log_attachment_path() {
+222 -93
View File
@@ -6,6 +6,7 @@
#include "dusk/main.h"
#include <array>
#include <chrono>
#include <filesystem>
#include <optional>
#include <ranges>
@@ -30,6 +31,21 @@ constexpr auto kLocationDescriptorName = "data_location.json";
constexpr auto kPipelineCacheName = "pipeline_cache.db";
constexpr auto kInitialPipelineCacheName = "initial_pipeline_cache.db";
constexpr std::array<std::string_view, 4> kUserDataDirectories = {
"texture_replacements",
"USA",
"EUR",
"JAP",
};
constexpr std::array<std::string_view, 6> kUserDataFiles = {
"achievements.json",
"config.json",
"controller_ports.dat",
"imgui.ini",
"keyboard_bindings.dat",
"states.json",
};
enum class LocationMode {
Default,
Portable,
@@ -62,6 +78,7 @@ struct MigrationStats {
std::optional<std::filesystem::path> sConfiguredDataPath;
std::optional<std::filesystem::path> sActiveDescriptorPath;
std::optional<std::filesystem::path> sActivePrefPath;
std::filesystem::path path_from_utf8(std::string_view value) {
return std::filesystem::path{
@@ -70,19 +87,22 @@ std::filesystem::path path_from_utf8(std::string_view value) {
};
}
std::filesystem::path get_legacy_path() {
if (std::string_view{LegacyAppName}.empty()) {
std::filesystem::path legacy_path_for_pref_path(const std::filesystem::path& prefPath) {
if (std::string_view{LegacyAppName}.empty() || prefPath.empty()) {
return {};
}
char* prefPath = SDL_GetPrefPath(OrgName, LegacyAppName);
if (!prefPath) {
Log.fatal("Unable to get PrefPath: {}", SDL_GetError());
auto normalizedPrefPath = prefPath;
if (normalizedPrefPath.filename().empty()) {
normalizedPrefPath = normalizedPrefPath.parent_path();
}
std::filesystem::path result{reinterpret_cast<const char8_t*>(prefPath)};
SDL_free(prefPath);
return result;
const auto parentPath = normalizedPrefPath.parent_path();
if (parentPath.empty()) {
return {};
}
return parentPath / LegacyAppName;
}
std::filesystem::path get_pref_path() {
@@ -96,6 +116,13 @@ std::filesystem::path get_pref_path() {
return result;
}
std::filesystem::path active_pref_path() {
if (sActivePrefPath) {
return *sActivePrefPath;
}
return get_pref_path();
}
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
const auto* basePath = SDL_GetBasePath();
if (!basePath) {
@@ -249,6 +276,69 @@ std::filesystem::path absolute_path(const std::filesystem::path& path) {
return absolute.lexically_normal();
}
std::filesystem::path rename_legacy_pref_path(
const std::filesystem::path& legacyPath, const std::filesystem::path& prefPath) {
if (legacyPath.empty() || prefPath.empty() ||
normalized_path(legacyPath) == normalized_path(prefPath))
{
return prefPath;
}
std::error_code ec;
if (!std::filesystem::exists(legacyPath, ec)) {
if (ec) {
Log.warn("Failed to inspect legacy data directory '{}': {}",
io::fs_path_to_string(legacyPath), ec.message());
}
return prefPath;
}
const bool prefExists = std::filesystem::exists(prefPath, ec);
if (ec) {
Log.warn("Failed to inspect data directory '{}': {}", io::fs_path_to_string(prefPath),
ec.message());
return prefPath;
}
if (prefExists) {
if (!std::filesystem::is_directory(prefPath, ec) ||
!std::filesystem::is_empty(prefPath, ec))
{
if (ec) {
Log.warn("Failed to inspect data directory '{}': {}",
io::fs_path_to_string(prefPath), ec.message());
} else {
Log.info("Skipping legacy data directory rename because '{}' is not empty",
io::fs_path_to_string(prefPath));
}
return prefPath;
}
std::filesystem::remove(prefPath, ec);
if (ec) {
Log.warn("Failed to remove empty data directory '{}' before legacy rename: {}",
io::fs_path_to_string(prefPath), ec.message());
return prefPath;
}
}
std::filesystem::rename(legacyPath, prefPath, ec);
if (ec) {
Log.warn("Failed to rename legacy data directory '{}' to '{}': {}",
io::fs_path_to_string(legacyPath), io::fs_path_to_string(prefPath), ec.message());
ec.clear();
if (!std::filesystem::exists(prefPath, ec) && !ec) {
Log.info("Using legacy data directory '{}' because the new data directory is absent",
io::fs_path_to_string(legacyPath));
return legacyPath;
}
return prefPath;
}
Log.info("Renamed legacy data directory '{}' to '{}'", io::fs_path_to_string(legacyPath),
io::fs_path_to_string(prefPath));
return prefPath;
}
bool is_same_or_inside(const std::filesystem::path& root, const std::filesystem::path& path) {
const auto normalizedRoot = normalized_path(root);
const auto normalizedPath = normalized_path(path);
@@ -283,85 +373,46 @@ bool should_skip_migration_path(const std::filesystem::path& path,
return false;
}
bool has_location_descriptor(const std::filesystem::path& path) {
std::error_code ec;
return std::filesystem::exists(path / kLocationDescriptorName, ec);
bool matches_name(std::string_view name, const auto& names) {
return std::ranges::find(names, name) != names.end();
}
bool remove_empty_destination_for_rename(const std::filesystem::path& path) {
std::error_code ec;
const bool exists = std::filesystem::exists(path, ec);
if (ec) {
Log.debug("Could not inspect migration destination '{}': {}", io::fs_path_to_string(path),
ec.message());
bool should_migrate_user_data_path(
const std::filesystem::path& sourcePath, const std::filesystem::path& from) {
const auto relativePath = sourcePath.lexically_relative(from);
if (relativePath.empty() || relativePath.is_absolute()) {
return false;
}
if (!exists) {
auto it = relativePath.begin();
if (it == relativePath.end() || *it == "..") {
return false;
}
const auto first = io::fs_path_to_string(*it);
if (matches_name(first, kUserDataDirectories)) {
return true;
}
const bool canRemove = std::filesystem::is_directory(path, ec) &&
std::filesystem::is_empty(path, ec) && !has_location_descriptor(path);
if (ec || !canRemove) {
if (ec) {
Log.debug("Could not inspect migration destination '{}': {}",
io::fs_path_to_string(path), ec.message());
}
++it;
if (it != relativePath.end()) {
return false;
}
std::filesystem::remove(path, ec);
if (ec) {
Log.debug("Could not remove empty migration destination '{}': {}",
io::fs_path_to_string(path), ec.message());
return false;
const auto filename = io::fs_path_to_string(relativePath.filename());
if (matches_name(filename, kUserDataFiles)) {
return true;
}
return true;
}
bool try_rename_directory_migration(
const std::filesystem::path& from, const std::filesystem::path& to) {
std::error_code ec;
if (!std::filesystem::is_directory(from, ec)) {
return false;
}
if (ec) {
Log.debug("Could not inspect migration source '{}': {}", io::fs_path_to_string(from),
ec.message());
return false;
}
if (has_location_descriptor(from)) {
return false;
}
if (!remove_empty_destination_for_rename(to)) {
return false;
}
std::filesystem::create_directories(to.parent_path(), ec);
if (ec) {
Log.debug("Could not create migration destination parent '{}': {}",
io::fs_path_to_string(to.parent_path()), ec.message());
return false;
}
std::filesystem::rename(from, to, ec);
if (ec) {
Log.debug("Could not rename data directory '{}' to '{}': {}", io::fs_path_to_string(from),
io::fs_path_to_string(to), ec.message());
return false;
}
Log.info("Renamed data directory '{}' to '{}'", io::fs_path_to_string(from),
io::fs_path_to_string(to));
return true;
return relativePath.extension() == ".controller" || relativePath.extension() == ".gci" ||
(filename.starts_with("MemoryCard") && filename.ends_with(".raw"));
}
std::filesystem::path current_data_path() {
if (!ConfigPath.empty()) {
return ConfigPath;
}
const auto prefPath = get_pref_path();
const auto prefPath = active_pref_path();
const auto descriptor = read_location_descriptor(prefPath);
if (descriptor) {
sActiveDescriptorPath = descriptor->path;
@@ -427,7 +478,7 @@ bool write_location_descriptor(LocationMode mode, const std::filesystem::path& t
json["previousPath"] = io::fs_path_to_string(descriptor.previousPath);
}
const auto prefPath = get_pref_path();
const auto prefPath = active_pref_path();
for (const auto& path : descriptor_write_paths(prefPath)) {
if (write_descriptor_json(path, json)) {
sActiveDescriptorPath = path;
@@ -439,6 +490,61 @@ bool write_location_descriptor(LocationMode mode, const std::filesystem::path& t
return false;
}
void set_error(std::string* errorOut, std::string error) {
if (errorOut != nullptr) {
*errorOut = std::move(error);
}
}
bool validate_writable_data_path(const std::filesystem::path& path, std::string* errorOut) {
if (path.empty()) {
set_error(errorOut, "Choose a folder.");
return false;
}
std::error_code ec;
std::filesystem::create_directories(path, ec);
if (ec) {
set_error(errorOut, fmt::format("{} could not create the selected folder.", AppName));
Log.warn("Failed to create custom data folder '{}': {}", io::fs_path_to_string(path),
ec.message());
return false;
}
if (!std::filesystem::is_directory(path, ec)) {
set_error(errorOut, "The selected path is not a folder.");
if (ec) {
Log.warn("Failed to inspect custom data folder '{}': {}", io::fs_path_to_string(path),
ec.message());
}
return false;
}
const auto probePath = path / fmt::format(".write-probe-{}.tmp",
std::chrono::steady_clock::now().time_since_epoch().count());
try {
io::FileStream::WriteAllText(probePath, "dusk");
} catch (const std::exception& e) {
set_error(errorOut, fmt::format("{} could not write to the selected folder.", AppName));
Log.warn("Failed write probe for custom data folder '{}': {}", io::fs_path_to_string(path),
e.what());
return false;
}
std::filesystem::remove(probePath, ec);
if (ec) {
set_error(
errorOut, fmt::format("{} could write to the selected folder, but could not remove "
"the test file it created.",
AppName));
Log.warn("Failed to remove custom data folder write probe '{}': {}",
io::fs_path_to_string(probePath), ec.message());
return false;
}
return true;
}
std::uintmax_t remove_empty_directories(const std::filesystem::path& root, bool includeRoot) {
std::error_code ec;
std::vector<std::filesystem::path> directories;
@@ -647,14 +753,6 @@ void migrate_directory(const std::filesystem::path& from, const std::filesystem:
return;
}
if (try_rename_directory_migration(from, to)) {
return;
}
if (try_rename_directory_migration(from, to)) {
return;
}
std::filesystem::create_directories(to, ec);
if (ec) {
++stats.failures;
@@ -700,6 +798,16 @@ void migrate_directory(const std::filesystem::path& from, const std::filesystem:
continue;
}
if (!should_migrate_user_data_path(sourcePath, from)) {
++stats.skippedUnsupportedEntries;
if (std::filesystem::is_directory(status)) {
it.disable_recursion_pending();
}
ec.clear();
it.increment(ec);
continue;
}
const auto relativePath = sourcePath.lexically_relative(from);
if (relativePath.empty() || relativePath.is_absolute()) {
++stats.failures;
@@ -761,8 +869,6 @@ void migrate_data(const std::filesystem::path& prefPath, const std::filesystem::
const LocationDescriptor* descriptor) {
if (descriptor && !descriptor->previousPath.empty()) {
migrate_directory(descriptor->previousPath, dataPath, prefPath);
} else if (const auto legacyPath = get_legacy_path(); !legacyPath.empty()) {
migrate_directory(legacyPath, dataPath, prefPath);
}
}
@@ -899,17 +1005,25 @@ bool open_data_path() {
#endif
}
bool set_custom_data_path(const std::filesystem::path& path) {
if (path.empty()) {
Log.warn("Ignoring empty custom data path");
bool set_custom_data_path(const std::filesystem::path& path, std::string* errorOut) {
if (!validate_writable_data_path(path, errorOut)) {
return false;
}
return write_location_descriptor(LocationMode::Custom, path);
if (!write_location_descriptor(LocationMode::Custom, path)) {
set_error(errorOut, fmt::format("{} could not save the data folder setting.", AppName));
return false;
}
return true;
}
bool set_custom_data_path(const char* path) {
return set_custom_data_path(path_from_utf8(path));
bool set_custom_data_path(const char* path, std::string* errorOut) {
if (path == nullptr) {
set_error(errorOut, "Choose a folder.");
return false;
}
return set_custom_data_path(path_from_utf8(path), errorOut);
}
bool set_portable_data_path() {
@@ -917,12 +1031,12 @@ bool set_portable_data_path() {
}
bool reset_data_path() {
const auto prefPath = get_pref_path();
const auto prefPath = active_pref_path();
return write_location_descriptor(LocationMode::Default, default_data_path(prefPath));
}
bool is_default_data_path() {
const auto prefPath = get_pref_path();
const auto prefPath = active_pref_path();
return normalized_path(configured_data_path()) == normalized_path(default_data_path(prefPath));
}
@@ -931,7 +1045,7 @@ std::filesystem::path configured_data_path() {
return *sConfiguredDataPath;
}
const auto prefPath = get_pref_path();
const auto prefPath = active_pref_path();
const auto descriptor = read_location_descriptor(prefPath);
if (descriptor) {
sActiveDescriptorPath = descriptor->path;
@@ -941,6 +1055,13 @@ std::filesystem::path configured_data_path() {
return *sConfiguredDataPath;
}
std::filesystem::path cache_path() {
if (!CachePath.empty()) {
return CachePath;
}
return active_pref_path();
}
bool is_data_path_restart_pending() {
if (ConfigPath.empty()) {
return false;
@@ -949,8 +1070,12 @@ bool is_data_path_restart_pending() {
return normalized_path(ConfigPath) != normalized_path(configured_data_path());
}
std::filesystem::path initialize_data() {
const auto prefPath = get_pref_path();
Paths initialize_data() {
const auto preferredPrefPath = get_pref_path();
const auto prefPath =
rename_legacy_pref_path(legacy_path_for_pref_path(preferredPrefPath), preferredPrefPath);
sActivePrefPath = prefPath;
const auto descriptor = read_location_descriptor(prefPath);
if (descriptor) {
sActiveDescriptorPath = descriptor->path;
@@ -963,9 +1088,13 @@ std::filesystem::path initialize_data() {
migrate_data(prefPath, dataPath, descriptor ? &descriptor->descriptor : nullptr);
ensure_data_directory(dataPath);
ensure_initial_pipeline_cache(dataPath);
ensure_data_directory(prefPath);
ensure_initial_pipeline_cache(prefPath);
return dataPath;
return Paths{
.userPath = dataPath,
.cachePath = prefPath,
};
}
} // namespace dusk::data
+11 -4
View File
@@ -1,6 +1,7 @@
#pragma once
#include <filesystem>
#include <string>
#if defined(__APPLE__)
#include <TargetConditionals.h>
@@ -14,7 +15,7 @@
#define DUSK_CAN_OPEN_DATA_FOLDER 0
#endif
#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
#if (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || defined(__ANDROID__)
#define DUSK_CAN_CHANGE_DATA_FOLDER 0
#else
#define DUSK_CAN_CHANGE_DATA_FOLDER 1
@@ -22,11 +23,17 @@
namespace dusk::data {
std::filesystem::path initialize_data();
struct Paths {
std::filesystem::path userPath;
std::filesystem::path cachePath;
};
Paths initialize_data();
std::filesystem::path configured_data_path();
std::filesystem::path cache_path();
bool open_data_path();
bool set_custom_data_path(const char* path);
bool set_custom_data_path(const std::filesystem::path& path);
bool set_custom_data_path(const char* path, std::string* errorOut);
bool set_custom_data_path(const std::filesystem::path& path, std::string* errorOut);
bool set_portable_data_path();
bool reset_data_path();
bool is_default_data_path();
+13 -8
View File
@@ -21,6 +21,7 @@ bool g_sync_presentation = false;
float g_step = 0.0f;
bool g_is_sim_frame = false;
bool g_ui_tick_pending = false;
uint64_t g_sim_tick_seq = 0;
Recording g_current_recording;
Recording g_previous_recording;
@@ -66,19 +67,18 @@ void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) {
}
inline void lerp_matrix(Mtx out, const Mtx lhs, const Mtx rhs, float step) {
const float old_weight = 1.0f - step;
for (size_t row = 0; row < 3; ++row) {
for (size_t col = 0; col < 4; ++col) {
out[row][col] = lhs[row][col] * old_weight + rhs[row][col] * step;
const float l = lhs[row][col];
out[row][col] = l + (rhs[row][col] - l) * step;
}
}
}
inline void lerp_xyz(cXyz* out, const cXyz& lhs, const cXyz& rhs, float step) {
const float old_weight = 1.0f - step;
out->x = lhs.x * old_weight + rhs.x * step;
out->y = lhs.y * old_weight + rhs.y * step;
out->z = lhs.z * old_weight + rhs.z * step;
out->x = lhs.x + (rhs.x - lhs.x) * step;
out->y = lhs.y + (rhs.y - lhs.y) * step;
out->z = lhs.z + (rhs.z - lhs.z) * step;
}
static s16 lerp_bank(s16 a, s16 b, f32 t) {
@@ -135,10 +135,15 @@ void begin_sim_tick() {
s_interpolationCallBackWork.clear();
s_cam_prev = std::move(s_cam_curr);
++g_sim_tick_seq;
}
void begin_frame(bool enabled, bool is_sim_frame, float step) {
g_enabled = enabled;
uint64_t sim_tick_seq() {
return g_sim_tick_seq;
}
void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step) {
g_enabled = mode != FrameInterpMode::Off;
g_is_sim_frame = is_sim_frame;
g_step = std::clamp(step, 0.0f, 1.0f);
}
+3 -1
View File
@@ -4,6 +4,7 @@
#include <chrono>
#include <cmath>
#include <unordered_map>
#include <dusk/frame_interpolation.h>
namespace dusk::game_clock {
@@ -45,7 +46,8 @@ MainLoopPacer advance_main_loop() {
MainLoopPacer out{};
out.presentation_dt_seconds = presentation_dt;
const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation &&
const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation.getValue() !=
dusk::FrameInterpMode::Off &&
!dusk::getTransientSettings().skipFrameRateLimit;
out.is_interpolating = should_interpolate;
out.sim_pace = sim_pace();
+11 -4
View File
@@ -13,6 +13,7 @@
#include "ImGuiEngine.hpp"
#include "JSystem/JUtility/JUTGamePad.h"
#include "SDL3/SDL_mouse.h"
#include "dusk/action_bindings.h"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/config.hpp"
#include "dusk/data.hpp"
@@ -239,7 +240,8 @@ namespace dusk {
}
void ImGuiConsole::UpdateSettings() {
getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && ImGui::IsKeyDown(ImGuiKey_Tab);
getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind &&
(ImGui::IsKeyDown(ImGuiKey_Tab) || getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON));
if (dusk::frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) {
getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive;
@@ -260,6 +262,12 @@ namespace dusk {
config::Save();
}
if (getSettings().game.enableResetKeybind && ImGui::GetIO().KeyCtrl &&
ImGui::IsKeyReleased(ImGuiKey_R) && !fpcM_SearchByName(fpcNm_LOGO_SCENE_e))
{
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
}
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
if (getSettings().backend.enableAdvancedSettings) {
m_isHidden = !m_isHidden;
@@ -314,8 +322,8 @@ namespace dusk {
}
ImGui::PushFont(ImGuiEngine::fontLarge);
ImGuiTextCenter("Failed to initialize any graphics backend.");
ImGuiTextCenter("\nYour system may be misconfigured, or your hardware may not support the required versions of any of the available backends.");
ImGuiTextCenter("\nA clean reinstall of Dusklight may help. For further assistance, please visit #tech-support on the Twilit Realm Discord server.");
ImGuiTextCenter("\nDusklight requires Vulkan 1.1+, or Direct X 12.0.");
ImGuiTextCenter("\nTry updating your Operating System and GPU drivers.");
const auto& style = ImGui::GetStyle();
const auto retrySize = ImGui::CalcTextSize("Retry (Auto backend)");
const auto quitSize = ImGui::CalcTextSize("Quit");
@@ -360,7 +368,6 @@ namespace dusk {
m_menuTools.ShowProcessManager();
m_menuTools.ShowHeapOverlay();
m_menuTools.ShowStubLog();
m_menuTools.ShowMapLoader();
m_menuTools.ShowBloomWindow();
m_menuTools.ShowPlayerInfo();
m_menuTools.ShowAudioDebug();
+5 -4
View File
@@ -3,12 +3,13 @@
#include "imgui.h"
#include <imgui_internal.h>
#include "ImGuiConsole.hpp"
#include "dusk/settings.h"
#include <dolphin/pad.h>
namespace dusk {
void ImGuiMenuTools::ShowInputViewer() {
if (!m_showInputViewer) {
if (!getSettings().game.showInputViewer) {
return;
}
@@ -259,10 +260,10 @@ namespace dusk {
size.y = 130 * scale;
ImGui::Dummy(size);
if (PADHasSensor(PAD_1, PAD_SENSOR_GYRO) == TRUE) {
if (getSettings().game.showInputViewerGyro)
{
ImGui::Separator();
ImGui::Checkbox("Gyro Values", &m_showInputViewerGyro);
if (m_showInputViewerGyro) {
{
ImGui::TextUnformatted("Gyro");
constexpr float kBarScale = 4.0f;
-149
View File
@@ -1,149 +0,0 @@
#include "d/d_com_inf_game.h"
#include "imgui.h"
#include <imgui_internal.h>
#include "ImGuiConsole.hpp"
#include "ImGuiMenuTools.hpp"
#include "dusk/map_loader_definitions.h"
#include "fmt/format.h"
namespace dusk {
void ImGuiMenuTools::ShowMapLoader() {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader))
{
return;
}
ImGuiWindowFlags windowFlags = ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
// ImGui::SetNextWindowBgAlpha(0.65f);
if (!ImGui::Begin("Map Loader", &m_showMapLoader, windowFlags)) {
ImGui::End();
return;
}
ImGui::Checkbox("Show Internal Names", &m_mapLoaderInfo.showInternalNames);
const char* previewRegion = "None";
if (m_mapLoaderInfo.regionIdx != -1) {
previewRegion = gameRegions[m_mapLoaderInfo.regionIdx].regionName;
}
if (ImGui::BeginCombo("Select Region", previewRegion)) {
int idx = 0;
for (const auto& region : gameRegions) {
if (ImGui::Selectable(region.regionName)) {
if (m_mapLoaderInfo.regionIdx != idx) {
m_mapLoaderInfo.mapIdx = 0;
m_mapLoaderInfo.roomNoIdx = 0;
m_mapLoaderInfo.pointNoIdx = 0;
}
m_mapLoaderInfo.regionIdx = idx;
}
idx++;
}
ImGui::EndCombo();
}
if (m_mapLoaderInfo.regionIdx != -1) {
const auto& region = gameRegions[m_mapLoaderInfo.regionIdx];
std::string previewMap = "None";
if (m_mapLoaderInfo.mapIdx != -1) {
const auto& map = region.maps[m_mapLoaderInfo.mapIdx];
previewMap = m_mapLoaderInfo.showInternalNames ? fmt::format("{} ({})", map.mapName, map.mapFile) : map.mapName;
}
if (ImGui::BeginCombo("Select Map", previewMap.data())) {
int prevMapIdx = m_mapLoaderInfo.mapIdx;
for (int i = 0; i < region.maps.size(); ++i) {
const auto& map = region.maps[i];
std::string label = m_mapLoaderInfo.showInternalNames ? fmt::format("{} ({})", map.mapName, map.mapFile) : map.mapName;
if (ImGui::Selectable(label.data())) {
m_mapLoaderInfo.mapIdx = i;
}
}
ImGui::EndCombo();
if (m_mapLoaderInfo.mapIdx != prevMapIdx) {
m_mapLoaderInfo.roomNoIdx = 0;
m_mapLoaderInfo.pointNoIdx = 0;
}
}
} else {
ImGui::Text("No region selected.");
}
if (m_mapLoaderInfo.regionIdx != -1 && m_mapLoaderInfo.mapIdx != -1) {
const auto& region = gameRegions[m_mapLoaderInfo.regionIdx];
const auto& map = region.maps[m_mapLoaderInfo.mapIdx];
const auto& room = map.mapRooms[m_mapLoaderInfo.roomNoIdx];
if (map.mapRooms.size() > 1) {
ImGui::Text("Selected Room: %2d", room.roomNo);
ImGui::SameLine();
if (ImGui::Button("-###RoomNoIdxDec")) {
m_mapLoaderInfo.roomNoIdx--;
if (m_mapLoaderInfo.roomNoIdx < 0) {
m_mapLoaderInfo.roomNoIdx = map.mapRooms.size() - 1;
}
m_mapLoaderInfo.pointNoIdx = 0;
}
ImGui::SameLine();
if (ImGui::Button("+###RoomNoIdxInc")) {
m_mapLoaderInfo.roomNoIdx++;
if (m_mapLoaderInfo.roomNoIdx >= map.mapRooms.size()) {
m_mapLoaderInfo.roomNoIdx = 0;
}
m_mapLoaderInfo.pointNoIdx = 0;
}
}
constexpr int MAX_LAYER = 14;
ImGui::Text("Selected Layer: %3d", m_mapLoaderInfo.layer);
ImGui::SameLine();
if (ImGui::Button("-###layerDec")) {
m_mapLoaderInfo.layer--;
if (m_mapLoaderInfo.layer < -1) {
m_mapLoaderInfo.layer = MAX_LAYER;
}
}
ImGui::SameLine();
if (ImGui::Button("+###layerInc")) {
m_mapLoaderInfo.layer++;
if (m_mapLoaderInfo.layer > MAX_LAYER) {
m_mapLoaderInfo.layer = -1;
}
}
if (room.roomPoints.size() > 1) {
ImGui::Text("Selected Point: %3d", room.roomPoints[m_mapLoaderInfo.pointNoIdx]);
ImGui::SameLine();
if (ImGui::Button("-###PointNoIdxDec")) {
m_mapLoaderInfo.pointNoIdx--;
if (m_mapLoaderInfo.pointNoIdx < 0) {
m_mapLoaderInfo.pointNoIdx = room.roomPoints.size() - 1;
}
}
ImGui::SameLine();
if (ImGui::Button("+###PointNoIdxInc")) {
m_mapLoaderInfo.pointNoIdx++;
if (m_mapLoaderInfo.pointNoIdx >= room.roomPoints.size()) {
m_mapLoaderInfo.pointNoIdx = 0;
}
}
}
if (ImGui::Button("Warp")) {
dComIfGp_setNextStage(map.mapFile, room.roomPoints[m_mapLoaderInfo.pointNoIdx], room.roomNo, m_mapLoaderInfo.layer);
}
}
ImGui::End();
}
} // namespace dusk
-4
View File
@@ -40,7 +40,6 @@ namespace dusk {
ImGui::BeginDisabled(getSettings().game.speedrunMode);
ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor);
ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader);
ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare);
ImGui::EndDisabled();
@@ -49,9 +48,6 @@ namespace dusk {
ImGui::EndDisabled();
}
ImGui::Separator();
ImGui::Checkbox("Show Input Viewer", &m_showInputViewer);
#if DUSK_CAN_OPEN_DATA_FOLDER
ImGui::Separator();
if (ImGui::MenuItem("Open Data Folder")) {
-16
View File
@@ -21,7 +21,6 @@ namespace dusk {
void ShowProcessManager();
void ShowHeapOverlay();
void ShowStubLog();
void ShowMapLoader();
void ShowBloomWindow();
void ShowPlayerInfo();
void ShowAudioDebug();
@@ -43,22 +42,9 @@ namespace dusk {
bool m_showStubLog = false;
bool m_showMapLoader = false;
bool m_showBloomWindow = false;
bool m_showAudioDebug = false;
struct {
int mapIdx = -1;
int regionIdx = -1;
int roomNoIdx = 0;
int pointNoIdx = 0;
int roomNo = -1;
int pointNo = -1;
int spawnId = 0;
int layer = -1;
bool showInternalNames = false;
} m_mapLoaderInfo;
bool m_showPlayerInfo = false;
int m_playerInfoOverlayCorner = 1; // top-right
@@ -69,8 +55,6 @@ namespace dusk {
bool m_showStateShare = false;
ImGuiStateShare m_stateShare;
bool m_showInputViewer = false;
bool m_showInputViewerGyro = false;
bool m_showActorSpawner = false;
int m_inputOverlayCorner = 3;
std::string m_controllerName;
+1 -1
View File
@@ -713,7 +713,7 @@ namespace dusk {
transformLevel++;
}
}
if (ImGui::SliderInt("Transform Level", &transformLevel, 0, 3)) {
if (ImGui::SliderInt("Transform Level", &transformLevel, 0, 4)) {
u8 newFlags = 0;
for (int i = 0; i < transformLevel; i++) {
newFlags |= (1 << i);
+3
View File
@@ -18,6 +18,7 @@
#include <unordered_set>
#include <zstd.h>
#include <dusk/autosave.h>
namespace dusk {
@@ -135,6 +136,8 @@ bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::s
return false;
}
toggleAutoSave(false);
StateSharePacket pkt;
memcpy(&pkt, raw.data(), sizeof(pkt));
pkt.stageName[7] = '\0';
+12 -10
View File
@@ -17,6 +17,8 @@ using namespace dusk::io;
#else
#define MODE(val) val
#endif
#define _SH_DENYNO 0
#define _SH_DENYWR 0
#endif
static FILE* ThrowIfNotOpen(const FileStream& file) {
@@ -31,19 +33,19 @@ static FILE* ThrowIfNotOpen(const FileStream& file) {
throw std::system_error(std::make_error_code(static_cast<std::errc>(code)));
}
static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode) {
static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, int shareFlag) {
FILE* file;
int err;
errno = 0;
#if _WIN32
static_assert(std::is_same_v<std::filesystem::path::value_type, wchar_t>);
err = _wfopen_s(&file, path.c_str(), mode);
file = _wfsopen(path.c_str(), mode, shareFlag);
#else
errno = 0;
static_assert(std::is_same_v<std::filesystem::path::value_type, char>);
file = fopen(path.c_str(), mode);
err = errno;
#endif
err = errno;
if (!file) {
ThrowForError(err);
@@ -52,8 +54,8 @@ static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode)
return file;
}
static FILE* OpenCore(const char* path, const MODE_TYPE* mode) {
return OpenCore(reinterpret_cast<const char8_t*>(path), mode);
static FILE* OpenCore(const char* path, const MODE_TYPE* mode, int shareFlag) {
return OpenCore(reinterpret_cast<const char8_t*>(path), mode, shareFlag);
}
FileStream::FileStream() noexcept : file(nullptr) {
@@ -76,19 +78,19 @@ FileStream::~FileStream() {
}
FileStream FileStream::OpenRead(const char* utf8Path) {
return FileStream(OpenCore(utf8Path, MODE("rb")));
return FileStream(OpenCore(utf8Path, MODE("rb"), _SH_DENYWR));
}
FileStream FileStream::OpenRead(const std::filesystem::path& path) {
return FileStream(OpenCore(path, MODE("rb")));
return FileStream(OpenCore(path, MODE("rb"), _SH_DENYWR));
}
FileStream FileStream::Create(const char* utf8Path) {
return FileStream(OpenCore(utf8Path, MODE("wb")));
return FileStream(OpenCore(utf8Path, MODE("wb"), _SH_DENYWR));
}
FileStream FileStream::Create(const std::filesystem::path& path) {
return FileStream(OpenCore(path, MODE("wb")));
return FileStream(OpenCore(path, MODE("wb"), _SH_DENYWR));
}
std::vector<u8> FileStream::ReadFull() {
+65 -2
View File
@@ -10,10 +10,11 @@ UserSettings g_userSettings = {
.lockAspectRatio {"video.lockAspectRatio", false},
.enableFpsOverlay {"game.enableFpsOverlay", false},
.fpsOverlayCorner {"game.fpsOverlayCorner", 0},
.maxFrameRate {"video.maxFrameRate", 240},
},
.audio = {
.masterVolume {"audio.masterVolume", 80},
.masterVolume {"audio.masterVolume", 60},
.mainMusicVolume {"audio.mainMusicVolume", 100},
.subMusicVolume {"audio.subMusicVolume", 100},
.soundEffectsVolume {"audio.soundEffectsVolume", 100},
@@ -52,14 +53,17 @@ UserSettings g_userSettings = {
.enableLinkDollRotation {"game.enableLinkDollRotation", false},
.enableAchievementToasts {"game.enableAchievementToasts", true},
.enableControllerToasts {"game.enableControllerToasts", true},
.enableDiscordPresence {"game.enableDiscordPresence", true},
// Graphics
.bloomMode {"game.bloomMode", BloomMode::Dusk},
.bloomMultiplier {"game.bloomMultiplier", 1.0f},
.disableWaterRefraction {"game.disableWaterRefraction", false},
.enableFrameInterpolation {"game.enableFrameInterpolation", false},
.enableTextureReplacements {"game.enableTextureReplacements", true},
.enableFrameInterpolation {"game.enableFrameInterpolation", FrameInterpMode::Off},
.internalResolutionScale {"game.internalResolutionScale", 0},
.shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1},
.resampler {"game.resampler", Resampler::Bilinear},
.enableDepthOfField {"game.enableDepthOfField", true},
.enableMapBackground {"game.enableMapBackground", true},
.disableCutscenePillarboxing {"game.disableCutscenePillarboxing", false},
@@ -92,6 +96,7 @@ UserSettings g_userSettings = {
// Cheats
.infiniteHearts {"game.infiniteHearts", false},
.infiniteArrows {"game.infiniteArrows", false},
.infiniteSeeds {"game.infiniteSeeds", false},
.infiniteBombs {"game.infiniteBombs", false},
.infiniteOil {"game.infiniteOil", false},
.infiniteOxygen {"game.infiniteOxygen", false},
@@ -105,12 +110,14 @@ UserSettings g_userSettings = {
.fastRoll {"game.fastRoll", false},
.fastSpinner {"game.fastSpinner", false},
.freeMagicArmor {"game.freeMagicArmor", false},
.invincibleEnemies {"game.invincibleEnemies", false},
// Technical
.restoreWiiGlitches {"game.restoreWiiGlitches", false},
// Controls
.enableTurboKeybind {"game.enableTurboKeybind", false},
.enableResetKeybind {"game.enableResetKeybind", false},
// Tools
.speedrunMode {"game.speedrunMode", false},
@@ -118,6 +125,8 @@ UserSettings g_userSettings = {
.showSpeedrunRTATimer {"game.showSpeedrunRTATimer", true},
.recordingMode {"game.recordingMode", false},
.removeQuestMapMarkers {"game.removeQuestMapMarkers", false},
.showInputViewer {"game.showInputViewer", false},
.showInputViewerGyro {"game.showInputViewerGyro", false}
},
.backend = {
@@ -130,6 +139,34 @@ UserSettings g_userSettings = {
.checkForUpdates {"backend.checkForUpdates", true},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)},
.enableAdvancedSettings {"backend.enableAdvancedSettings", false},
},
// Not sure if there's a better way to declare this
.actionBindings = {
.firstPersonCamera {
ActionBindConfigVar{"actionBindings.firstPersonCamera_port0", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.firstPersonCamera_port1", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.firstPersonCamera_port2", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.firstPersonCamera_port3", PAD_NATIVE_BUTTON_INVALID},
},
.callMidna {
ActionBindConfigVar{"actionBindings.callMidna_port0", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.callMidna_port1", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.callMidna_port2", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.callMidna_port3", PAD_NATIVE_BUTTON_INVALID},
},
.openDusklightMenu {
ActionBindConfigVar{"actionBindings.openDusklightMenu_port0", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.openDusklightMenu_port1", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.openDusklightMenu_port2", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.openDusklightMenu_port3", PAD_NATIVE_BUTTON_INVALID},
},
.turboSpeedButton {
ActionBindConfigVar{"actionBindings.turboButton_port0", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.turboButton_port1", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.turboButton_port2", PAD_NATIVE_BUTTON_INVALID},
ActionBindConfigVar{"actionBindings.turboButton_port3", PAD_NATIVE_BUTTON_INVALID},
},
}
};
@@ -144,6 +181,7 @@ void registerSettings() {
Register(g_userSettings.video.lockAspectRatio);
Register(g_userSettings.video.enableFpsOverlay);
Register(g_userSettings.video.fpsOverlayCorner);
Register(g_userSettings.video.maxFrameRate);
// Audio
Register(g_userSettings.audio.masterVolume);
@@ -181,10 +219,13 @@ void registerSettings() {
Register(g_userSettings.game.freeCameraSensitivity);
Register(g_userSettings.game.minimalHUD);
Register(g_userSettings.game.pauseOnFocusLost);
Register(g_userSettings.game.enableDiscordPresence);
Register(g_userSettings.game.bloomMode);
Register(g_userSettings.game.bloomMultiplier);
Register(g_userSettings.game.disableWaterRefraction);
Register(g_userSettings.game.enableTextureReplacements);
Register(g_userSettings.game.internalResolutionScale);
Register(g_userSettings.game.resampler);
Register(g_userSettings.game.shadowResolutionMultiplier);
Register(g_userSettings.game.enableDepthOfField);
Register(g_userSettings.game.enableMapBackground);
@@ -201,14 +242,18 @@ void registerSettings() {
Register(g_userSettings.game.noLowHpSound);
Register(g_userSettings.game.midnasLamentNonStop);
Register(g_userSettings.game.enableTurboKeybind);
Register(g_userSettings.game.enableResetKeybind);
Register(g_userSettings.game.speedrunMode);
Register(g_userSettings.game.liveSplitEnabled);
Register(g_userSettings.game.showSpeedrunRTATimer);
Register(g_userSettings.game.recordingMode);
Register(g_userSettings.game.removeQuestMapMarkers);
Register(g_userSettings.game.showInputViewer);
Register(g_userSettings.game.showInputViewerGyro);
Register(g_userSettings.game.fastSpinner);
Register(g_userSettings.game.infiniteHearts);
Register(g_userSettings.game.infiniteArrows);
Register(g_userSettings.game.infiniteSeeds);
Register(g_userSettings.game.infiniteBombs);
Register(g_userSettings.game.infiniteOil);
Register(g_userSettings.game.infiniteOxygen);
@@ -217,6 +262,7 @@ void registerSettings() {
Register(g_userSettings.game.moonJump);
Register(g_userSettings.game.superClawshot);
Register(g_userSettings.game.alwaysGreatspin);
Register(g_userSettings.game.invincibleEnemies);
Register(g_userSettings.game.enableFrameInterpolation);
Register(g_userSettings.game.gyroMode);
Register(g_userSettings.game.enableGyroAim);
@@ -242,6 +288,23 @@ void registerSettings() {
Register(g_userSettings.backend.checkForUpdates);
Register(g_userSettings.backend.cardFileType);
Register(g_userSettings.backend.enableAdvancedSettings);
Register(g_userSettings.actionBindings.firstPersonCamera[0]);
Register(g_userSettings.actionBindings.firstPersonCamera[1]);
Register(g_userSettings.actionBindings.firstPersonCamera[2]);
Register(g_userSettings.actionBindings.firstPersonCamera[3]);
Register(g_userSettings.actionBindings.callMidna[0]);
Register(g_userSettings.actionBindings.callMidna[1]);
Register(g_userSettings.actionBindings.callMidna[2]);
Register(g_userSettings.actionBindings.callMidna[3]);
Register(g_userSettings.actionBindings.openDusklightMenu[0]);
Register(g_userSettings.actionBindings.openDusklightMenu[1]);
Register(g_userSettings.actionBindings.openDusklightMenu[2]);
Register(g_userSettings.actionBindings.openDusklightMenu[3]);
Register(g_userSettings.actionBindings.turboSpeedButton[0]);
Register(g_userSettings.actionBindings.turboSpeedButton[1]);
Register(g_userSettings.actionBindings.turboSpeedButton[2]);
Register(g_userSettings.actionBindings.turboSpeedButton[3]);
}
// Transient settings
+1
View File
@@ -20,6 +20,7 @@ void resetForSpeedrunMode() {
getSettings().game.infiniteHearts.setSpeedrunValue(false);
getSettings().game.infiniteArrows.setSpeedrunValue(false);
getSettings().game.infiniteSeeds.setSpeedrunValue(false);
getSettings().game.infiniteBombs.setSpeedrunValue(false);
getSettings().game.infiniteOil.setSpeedrunValue(false);
getSettings().game.infiniteOxygen.setSpeedrunValue(false);
+247 -111
View File
@@ -15,6 +15,9 @@
#include <utility>
#include <vector>
#include "dusk/action_bindings.h"
#include "dusk/config.hpp"
namespace dusk::ui {
namespace {
@@ -34,7 +37,7 @@ Rml::String current_controller_name(int port) {
Rml::String controller_index_name(u32 index) {
const char* name = PADGetNameForControllerIndex(index);
if (name == nullptr) {
return fmt::format("Controller {}", index + 1);
return fmt::format("Device {}", index + 1);
}
return name;
}
@@ -108,68 +111,6 @@ const std::vector<ButtonNames> kGamepadButtonNames = {
};
// clang-format on
Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) {
if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) {
return "Not bound";
}
auto button = static_cast<SDL_GamepadButton>(buttonUntyped);
if (gamepad != nullptr) {
switch (SDL_GetGamepadButtonLabel(gamepad, button)) {
case SDL_GAMEPAD_BUTTON_LABEL_A:
return "A";
case SDL_GAMEPAD_BUTTON_LABEL_B:
return "B";
case SDL_GAMEPAD_BUTTON_LABEL_X:
return "X";
case SDL_GAMEPAD_BUTTON_LABEL_Y:
return "Y";
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
return "Cross";
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
return "Circle";
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
return "Triangle";
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
return "Square";
default:
break;
}
}
const SDL_GamepadType type =
gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN;
for (const auto& buttonNames : kGamepadButtonNames) {
if (buttonNames.button != button) {
continue;
}
for (const auto& name : buttonNames.names) {
if (name.type == type) {
return name.name;
}
}
}
switch (button) {
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
return "D-pad left";
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
return "D-pad right";
case SDL_GAMEPAD_BUTTON_DPAD_UP:
return "D-pad up";
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
return "D-pad down";
default:
break;
}
if (const char* name = PADGetNativeButtonName(buttonUntyped)) {
return name;
}
return "Unknown";
}
Rml::String native_axis_name(const PADAxisMapping& mapping, SDL_Gamepad* gamepad) {
if (mapping.nativeAxis.nativeAxis != -1) {
Rml::String value = PADGetNativeAxisName(mapping.nativeAxis);
@@ -183,7 +124,7 @@ Rml::String native_axis_name(const PADAxisMapping& mapping, SDL_Gamepad* gamepad
return native_button_name(gamepad, static_cast<u32>(mapping.nativeButton));
}
return "Not bound";
return "Not Bound";
}
bool is_dpad_button(PADButton button) {
@@ -221,7 +162,7 @@ bool keyboard_escape_pressed() {
Rml::String keyboard_key_name(s32 scancode) {
if (scancode == PAD_KEY_INVALID) {
return "Not bound";
return "Not Bound";
}
switch (scancode) {
case PAD_KEY_MOUSE_LEFT:
@@ -362,11 +303,12 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
});
};
addPageButton(Page::Controller, "Controller", [port] { return current_controller_name(port); }, [] { return false; });
addPageButton(Page::Controller, "Device", [port] { return current_controller_name(port); }, [] { return false; });
addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); }, [] { return false; });
addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); }, [] { return false; });
addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); }, [] { return false; });
addPageButton(Page::Rumble, "Rumble", [] { return Rml::String(">"); }, [port] { return !PADSupportsRumbleIntensity(static_cast<u32>(port)); });
addPageButton(Page::Actions, "Custom Action Bindings", [] {return Rml::String(">"); }, [] { return false; });
leftPane.add_section("Options");
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
@@ -407,7 +349,14 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
rightPane, [](Pane& pane) {
pane.add_text("Treat analog trigger movement as digital L and R button input.");
});
leftPane.register_control(leftPane.add_button("Restore Default Controls").on_pressed([this, port] {
mDoAud_seStartMenu(kSoundClick);
PADRestoreDefaultMapping(port);
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_text("Restores all binding configurations for the currently selected device to their defaults.");
});
render_page(rightPane, port, mPage);
}
@@ -423,11 +372,12 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
[port] { return PADGetIndexForPort(port) < 0 && !keyboard_active(port); },
})
.on_pressed([this, port] {
mDoAud_seStartMenu(kSoundItemChange);
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
PADClearPort(port);
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
PADSerializeMappings();
ClearAllActionBindings(port);
});
pane.add_button({
@@ -435,16 +385,17 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
.isSelected = [port] { return keyboard_active(port); },
})
.on_pressed([this, port] {
mDoAud_seStartMenu(kSoundItemChange);
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
PADClearPort(port);
PADSetKeyboardActive(static_cast<u32>(port), TRUE);
PADSerializeMappings();
ClearAllActionBindings(port);
});
const u32 controllerCount = PADCount();
if (controllerCount == 0) {
pane.add_text("No controllers detected");
pane.add_text("No Device Detected");
break;
}
@@ -456,11 +407,12 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
[port, i] { return PADGetIndexForPort(port) == static_cast<s32>(i); },
})
.on_pressed([this, port, i] {
mDoAud_seStartMenu(kSoundItemChange);
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
PADSetPortForIndex(i, port);
PADSerializeMappings();
ClearAllActionBindings(port);
});
}
break;
@@ -480,17 +432,18 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
PADKeyButtonBinding* bindings =
PADGetKeyButtonBindings(static_cast<u32>(port), &count);
if (bindings == nullptr) {
return Rml::String("Not bound");
return Rml::String("Not Bound");
}
for (u32 i = 0; i < PAD_BUTTON_COUNT; ++i) {
if (bindings[i].padButton == button) {
return keyboard_key_name(bindings[i].scancode);
}
}
return Rml::String("Not bound");
return Rml::String("Not Bound");
},
})
.on_pressed([this, port, button] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
@@ -517,7 +470,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
u32 buttonCount = 0;
PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount);
if (mappings == nullptr) {
pane.add_text("No controller selected");
pane.add_text("No Device Selected");
break;
}
@@ -541,6 +494,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
},
})
.on_pressed([this, port, &mapping] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
@@ -567,6 +521,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
},
})
.on_pressed([this, port, &mapping] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
@@ -590,17 +545,18 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
PADKeyButtonBinding* bindings =
PADGetKeyButtonBindings(static_cast<u32>(port), &count);
if (bindings == nullptr) {
return Rml::String("Not bound");
return Rml::String("Not Bound");
}
for (u32 i = 0; i < PAD_BUTTON_COUNT; ++i) {
if (bindings[i].padButton == button) {
return keyboard_key_name(bindings[i].scancode);
}
}
return Rml::String("Not bound");
return Rml::String("Not Bound");
},
})
.on_pressed([this, port, button] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
@@ -621,17 +577,18 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
PADKeyAxisBinding* bindings =
PADGetKeyAxisBindings(static_cast<u32>(port), &count);
if (bindings == nullptr) {
return Rml::String("Not bound");
return Rml::String("Not Bound");
}
for (u32 i = 0; i < PAD_AXIS_COUNT; ++i) {
if (bindings[i].padAxis == axis) {
return keyboard_key_name(bindings[i].scancode);
}
}
return Rml::String("Not bound");
return Rml::String("Not Bound");
},
})
.on_pressed([this, port, axis] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
@@ -654,7 +611,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
u32 buttonCount = 0;
PADButtonMapping* buttons = PADGetButtonMappings(port, &buttonCount);
if (axes == nullptr && buttons == nullptr) {
pane.add_text("No controller selected");
pane.add_text("No Device Selected");
break;
}
@@ -678,6 +635,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
},
})
.on_pressed([this, port, &mapping] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
@@ -686,30 +644,33 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
}
}
pane.add_section("Digital");
if (buttons != nullptr) {
for (u32 i = 0; i < buttonCount; ++i) {
PADButtonMapping& mapping = buttons[i];
if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) {
continue;
if (getSettings().backend.enableAdvancedSettings) {
pane.add_section("Digital");
if (buttons != nullptr) {
for (u32 i = 0; i < buttonCount; ++i) {
PADButtonMapping& mapping = buttons[i];
if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) {
continue;
}
pane.add_select_button({
.key = PADGetButtonName(mapping.padButton),
.getValue =
[this, &mapping, gamepad] {
if (mPendingButtonMapping == &mapping) {
return pending_button_label();
}
return native_button_name(
gamepad, mapping.nativeButton);
},
})
.on_pressed([this, port, &mapping] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingButtonMapping = &mapping;
});
}
pane.add_select_button({
.key = PADGetButtonName(mapping.padButton),
.getValue =
[this, &mapping, gamepad] {
if (mPendingButtonMapping == &mapping) {
return pending_button_label();
}
return native_button_name(
gamepad, mapping.nativeButton);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingButtonMapping = &mapping;
});
}
}
@@ -761,17 +722,18 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
PADKeyAxisBinding* bindings =
PADGetKeyAxisBindings(static_cast<u32>(port), &count);
if (bindings == nullptr) {
return Rml::String("Not bound");
return Rml::String("Not Bound");
}
for (u32 i = 0; i < PAD_AXIS_COUNT; ++i) {
if (bindings[i].padAxis == axis) {
return keyboard_key_name(bindings[i].scancode);
}
}
return Rml::String("Not bound");
return Rml::String("Not Bound");
},
})
.on_pressed([this, port, axis] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
@@ -796,7 +758,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
u32 axisCount = 0;
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
if (axes == nullptr) {
pane.add_text("No controller selected");
pane.add_text("No Device Selected");
break;
}
@@ -817,6 +779,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
},
})
.on_pressed([this, port, &mapping] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
@@ -946,6 +909,79 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
pane.add_text("Configure your desired rumble intensities, then run a test to check how they feel.");
break;
}
case Page::Actions: {
if (keyboard_active(port)) {
auto addActionBinding = [&](auto actionBind, const std::string& key) {
pane.add_select_button(
{
.key = key,
.getValue =
[this, actionBind] {
if (mPendingActionBinding == actionBind) {
return pending_key_label();
}
return keyboard_key_name(actionBind->getValue());
},
})
.on_pressed([this, port, actionBind] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingActionBinding = actionBind;
});
};
pane.add_section("Custom Action Bindings");
pane.add_text("A key bound to any action here will REPLACE the default control for"
" that action. Only bind buttons here that aren't used anywhere else.");
for (auto& [configVars, actionName] : getActionBinds() | std::views::values) {
addActionBinding(&configVars->at(port), actionName);
}
break;
}
u32 buttonCount = 0;
PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount);
if (mappings == nullptr) {
pane.add_text("No Device Selected");
break;
}
SDL_Gamepad* gamepad = gamepad_for_port(port);
pane.add_section("Custom Action Bindings");
pane.add_text("A button bound to any action here will REPLACE the default control for"
" that action. Only bind buttons here that aren't used anywhere else. The glyphs"
" shown for in game actions will not change. This is not recommended for "
" regular Gamecube controllers.");
auto addActionBinding = [&](auto actionBind, const std::string& key) {
pane.add_select_button({
.key = key,
.getValue =
[this, gamepad, actionBind] {
if (mPendingActionBinding == actionBind) {
return pending_button_label();
}
return native_button_name(
gamepad, actionBind->getValue());
},
})
.on_pressed([this, port, actionBind] {
mDoAud_seStartMenu(kSoundClick);
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingActionBinding = actionBind;
});
};
for (auto& [configVars, actionName] : getActionBinds() | std::views::values) {
addActionBinding(&configVars->at(port), actionName);
}
break;
}
}
}
@@ -997,6 +1033,12 @@ void ControllerConfigWindow::poll_pending_binding() {
const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort);
if (nativeButton != -1) {
const int completedPort = mPendingPort;
if (mPendingButtonMapping->nativeButton == static_cast<u32>(nativeButton) &&
(mPendingButtonMapping->padButton != PAD_BUTTON_A &&
mPendingButtonMapping->padButton != PAD_BUTTON_B)) {
unmap_pending_binding();
return;
}
mPendingButtonMapping->nativeButton = static_cast<u32>(nativeButton);
finish_pending_binding(completedPort);
}
@@ -1007,6 +1049,10 @@ void ControllerConfigWindow::poll_pending_binding() {
const PADSignedNativeAxis nativeAxis = PADGetNativeAxisPulled(mPendingPort);
if (nativeAxis.nativeAxis != -1) {
const int completedPort = mPendingPort;
if (mPendingAxisMapping->nativeAxis.nativeAxis == nativeAxis.nativeAxis) {
unmap_pending_binding();
return;
}
mPendingAxisMapping->nativeAxis = nativeAxis;
mPendingAxisMapping->nativeButton = -1;
finish_pending_binding(completedPort);
@@ -1020,12 +1066,36 @@ void ControllerConfigWindow::poll_pending_binding() {
mPendingAxisMapping->nativeButton = nativeButton;
finish_pending_binding(completedPort);
}
return;
}
if (mPendingActionBinding != nullptr) {
int button{};
if (keyboard_active(mPendingPort)) {
button = keyboard_key_pressed();
} else {
button = PADGetNativeButtonPressed(mPendingPort);
}
if (button != -1) {
const int completedPort = mPendingPort;
if (mPendingActionBinding->getValue() == button) {
unmap_pending_binding();
return;
}
mPendingActionBinding->setValue(button);
config::Save();
finish_pending_binding(completedPort);
}
return;
}
}
void ControllerConfigWindow::finish_pending_binding(int completedPort) {
mDoAud_seStartMenu(kSoundBindingChanged);
mPendingButtonMapping = nullptr;
mPendingAxisMapping = nullptr;
mPendingActionBinding = nullptr;
mPendingPort = -1;
mPendingBindingArmed = false;
mSuppressNavigationUntilNeutral = true;
@@ -1035,7 +1105,7 @@ void ControllerConfigWindow::finish_pending_binding(int completedPort) {
void ControllerConfigWindow::unmap_pending_binding() {
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr &&
mPendingKeyButton < 0 && mPendingKeyAxis < 0)
mPendingActionBinding == nullptr && mPendingKeyButton < 0 && mPendingKeyAxis < 0)
{
return;
}
@@ -1048,6 +1118,9 @@ void ControllerConfigWindow::unmap_pending_binding() {
mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
mPendingAxisMapping->nativeButton = -1;
finish_pending_binding(completedPort);
} else if (mPendingActionBinding != nullptr) {
mPendingActionBinding->setValue(PAD_NATIVE_BUTTON_INVALID);
finish_pending_binding(completedPort);
} else if (mPendingKeyButton >= 0) {
PADSetKeyButtonBinding(static_cast<u32>(completedPort),
{PAD_KEY_INVALID, static_cast<PADButton>(mPendingKeyButton)});
@@ -1061,7 +1134,7 @@ void ControllerConfigWindow::unmap_pending_binding() {
bool ControllerConfigWindow::capture_active() const {
return mPendingButtonMapping != nullptr || mPendingAxisMapping != nullptr ||
mPendingKeyButton >= 0 || mPendingKeyAxis >= 0;
mPendingActionBinding != nullptr || mPendingKeyButton >= 0 || mPendingKeyAxis >= 0;
}
bool ControllerConfigWindow::pending_input_neutral() const {
@@ -1072,21 +1145,22 @@ bool ControllerConfigWindow::pending_input_neutral() const {
}
Rml::String ControllerConfigWindow::pending_button_label() const {
return mPendingBindingArmed ? "Press a button..." : "Waiting...";
return mPendingBindingArmed ? "Press a Key or Button..." : "Waiting...";
}
Rml::String ControllerConfigWindow::pending_axis_label() const {
return mPendingBindingArmed ? "Move axis or press a button..." : "Waiting...";
return mPendingBindingArmed ? "Move Axis or press a Key or Button..." : "Waiting...";
}
void ControllerConfigWindow::cancel_pending_binding() {
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr &&
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr && mPendingActionBinding == nullptr &&
!mSuppressNavigationUntilNeutral && mPendingKeyButton < 0 && mPendingKeyAxis < 0)
{
return;
}
mPendingButtonMapping = nullptr;
mPendingAxisMapping = nullptr;
mPendingActionBinding = nullptr;
mPendingKeyButton = -1;
mPendingKeyAxis = -1;
mPendingPort = -1;
@@ -1104,7 +1178,7 @@ void ControllerConfigWindow::finish_pending_key_binding() {
}
Rml::String ControllerConfigWindow::pending_key_label() const {
return mPendingBindingArmed ? "Press a key or mouse button..." : "Waiting...";
return mPendingBindingArmed ? "Press a Key or Mouse Button..." : "Waiting...";
}
void ControllerConfigWindow::stop_rumble_test() {
@@ -1118,4 +1192,66 @@ void ControllerConfigWindow::stop_rumble_test() {
mRumbleTestPort = -1;
}
Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) {
if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) {
return "Not Bound";
}
auto button = static_cast<SDL_GamepadButton>(buttonUntyped);
if (gamepad != nullptr) {
switch (SDL_GetGamepadButtonLabel(gamepad, button)) {
case SDL_GAMEPAD_BUTTON_LABEL_A:
return "A";
case SDL_GAMEPAD_BUTTON_LABEL_B:
return "B";
case SDL_GAMEPAD_BUTTON_LABEL_X:
return "X";
case SDL_GAMEPAD_BUTTON_LABEL_Y:
return "Y";
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
return "Cross";
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
return "Circle";
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
return "Triangle";
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
return "Square";
default:
break;
}
}
const SDL_GamepadType type =
gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN;
for (const auto& buttonNames : kGamepadButtonNames) {
if (buttonNames.button != button) {
continue;
}
for (const auto& name : buttonNames.names) {
if (name.type == type) {
return name.name;
}
}
}
switch (button) {
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
return "D-pad left";
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
return "D-pad right";
case SDL_GAMEPAD_BUTTON_DPAD_UP:
return "D-pad up";
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
return "D-pad down";
default:
break;
}
if (const char* name = PADGetNativeButtonName(buttonUntyped)) {
return name;
}
return "Unknown";
}
} // namespace dusk::ui
+5
View File
@@ -1,6 +1,7 @@
#pragma once
#include "window.hpp"
#include "dusk/config_var.hpp"
#include <pad.h>
@@ -20,6 +21,7 @@ private:
Triggers,
Sticks,
Rumble,
Actions,
};
void build_port_tab(Rml::Element* content, int port);
@@ -50,6 +52,9 @@ private:
int mPendingKeyAxis = -1;
bool mRumbleTestActive = false;
int mRumbleTestPort = -1;
ActionBindConfigVar* mPendingActionBinding = nullptr;
};
Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped);
} // namespace dusk::ui
+39 -40
View File
@@ -26,6 +26,43 @@
#include <vector>
namespace dusk::ui {
Rml::String stage_option_label(const MapEntry& map, bool showInternalNames) {
return showInternalNames ? fmt::format("{} ({})", map.mapName, map.mapFile) : map.mapName;
}
Rml::String stage_label_for_file(const Rml::String& stageFile, bool showInternalNames) {
for (const auto& region : gameRegions) {
for (const auto& map : region.maps) {
if (stageFile == map.mapFile) {
return stage_option_label(map, showInternalNames);
}
}
}
return stageFile;
}
void populate_stage_picker(Pane& pane, std::function<Rml::String()> getStageFile,
std::function<void(const char*)> setStageFile, bool showInternalNames) {
pane.clear();
for (const auto& region : gameRegions) {
pane.add_section(region.regionName);
for (const auto& map : region.maps) {
pane.add_button({
.text = stage_option_label(map, showInternalNames),
.isSelected =
[getStageFile, stageFile = map.mapFile] {
return getStageFile() == stageFile;
},
})
.on_pressed([setStageFile, stageFile = map.mapFile] {
mDoAud_seStartMenu(kSoundItemChange);
setStageFile(stageFile);
});
}
}
}
namespace {
bool has_save_data() {
@@ -155,44 +192,6 @@ bool parse_vec3(const Rml::String& value, float& x, float& y, float& z) {
return *cursor == '\0';
}
Rml::String stage_option_label(const MapEntry& map) {
// TODO: option to show internal name?
// return fmt::format("{} ({})", map.mapName, map.mapFile);
return map.mapName;
}
Rml::String stage_label_for_file(const Rml::String& stageFile) {
for (const auto& region : gameRegions) {
for (const auto& map : region.maps) {
if (stageFile == map.mapFile) {
return stage_option_label(map);
}
}
}
return stageFile;
}
void populate_stage_picker(Pane& pane, std::function<Rml::String()> getStageFile,
std::function<void(const char*)> setStageFile) {
pane.clear();
for (const auto& region : gameRegions) {
pane.add_section(region.regionName);
for (const auto& map : region.maps) {
pane.add_button({
.text = stage_option_label(map),
.isSelected =
[getStageFile, stageFile = map.mapFile] {
return getStageFile() == stageFile;
},
})
.on_pressed([setStageFile, stageFile = map.mapFile] {
mDoAud_seStartMenu(kSoundItemChange);
setStageFile(stageFile);
});
}
}
}
Rml::String get_player_name() {
if (!has_save_data()) {
return "";
@@ -1502,14 +1501,14 @@ EditorWindow::EditorWindow() {
.getValue =
[] {
return std::popcount(static_cast<unsigned>(
get_player_status_b()->mTransformLevelFlag & 0x7));
get_player_status_b()->mTransformLevelFlag & 0xF));
},
.setValue =
[](int value) {
get_player_status_b()->mTransformLevelFlag =
static_cast<u8>((1u << value) - 1u);
},
.max = 3,
.max = 4,
}),
rightPane, {});
leftPane.register_control(
+11
View File
@@ -1,8 +1,19 @@
#pragma once
#include "window.hpp"
struct MapEntry;
namespace dusk::ui {
class Pane;
Rml::String stage_option_label(const MapEntry& map, bool showInternalNames = false);
Rml::String stage_label_for_file(const Rml::String& stageFile, bool showInternalNames = false);
void populate_stage_picker(Pane& pane, std::function<Rml::String()> getStageFile,
std::function<void(const char*)> setStageFile,
bool showInternalNames = false);
class EditorWindow : public Window {
public:
EditorWindow();
+29 -2
View File
@@ -3,6 +3,7 @@
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <aurora/aurora.h>
#include <dolphin/gx/GXAurora.h>
#include <dolphin/vi.h>
#include <fmt/format.h>
@@ -43,6 +44,8 @@ int get_value(GraphicsOption option) {
return getSettings().game.internalResolutionScale.getValue();
case GraphicsOption::ShadowResolution:
return getSettings().game.shadowResolutionMultiplier.getValue();
case GraphicsOption::Resampler:
return static_cast<int>(getSettings().game.resampler.getValue());
case GraphicsOption::BloomMode:
return static_cast<int>(getSettings().game.bloomMode.getValue());
case GraphicsOption::BloomMultiplier:
@@ -62,6 +65,22 @@ void set_value(GraphicsOption option, int value) {
case GraphicsOption::ShadowResolution:
getSettings().game.shadowResolutionMultiplier.setValue(value);
break;
case GraphicsOption::Resampler: {
const auto sampler = static_cast<Resampler>(std::clamp(value,
static_cast<int>(Resampler::Bilinear),
static_cast<int>(Resampler::Area)));
getSettings().game.resampler.setValue(sampler);
switch (sampler) {
case Resampler::Area:
aurora_set_resampler(SAMPLER_AREA);
break;
case Resampler::Bilinear:
default:
aurora_set_resampler(SAMPLER_BILINEAR);
break;
}
break;
}
case GraphicsOption::BloomMode:
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
value, static_cast<int>(BloomMode::Off), static_cast<int>(BloomMode::Dusk))));
@@ -177,6 +196,14 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
}
case GraphicsOption::ShadowResolution:
return fmt::format("{}×", value);
case GraphicsOption::Resampler:
switch (static_cast<Resampler>(value)) {
case Resampler::Bilinear:
return "Bilinear";
case Resampler::Area:
return "Area";
}
break;
case GraphicsOption::BloomMode:
switch (static_cast<BloomMode>(value)) {
case BloomMode::Off:
@@ -184,7 +211,7 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
case BloomMode::Classic:
return "Classic";
case BloomMode::Dusk:
return "Dusk";
return "Dusklight";
}
break;
case GraphicsOption::BloomMultiplier:
@@ -211,7 +238,7 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch)
SteppedCarousel::Props{
.min = mValueMin,
.max = mValueMax,
.step = 1,
.step = props.step,
.getValue = [this] { return get_value(mOption); },
.onChange = [this](int value) { set_value(mOption, value); },
.formatValue =
+2
View File
@@ -42,6 +42,7 @@ private:
enum class GraphicsOption {
InternalResolution,
ShadowResolution,
Resampler,
BloomMode,
BloomMultiplier,
};
@@ -55,6 +56,7 @@ struct GraphicsTunerProps {
int valueMin = 0;
int valueMax = 0;
int defaultValue = 0;
int step = 1;
};
class GraphicsTuner : public Document {
+41 -16
View File
@@ -12,6 +12,8 @@
#include <algorithm>
#include <array>
#include "dusk/action_bindings.h"
namespace dusk::ui::input {
namespace {
@@ -203,6 +205,9 @@ Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexc
case SDL_GAMEPAD_BUTTON_SOUTH:
return Rml::Input::KI_RETURN;
case SDL_GAMEPAD_BUTTON_BACK:
if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0)) {
return Rml::Input::KI_UNKNOWN;
}
return Rml::Input::KI_F1;
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
return Rml::Input::KI_NEXT;
@@ -216,6 +221,9 @@ Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexc
Rml::Input::KeyIdentifier map_raw_button_alias(SDL_GamepadButton button) noexcept {
switch (button) {
case SDL_GAMEPAD_BUTTON_BACK:
if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0)) {
return Rml::Input::KI_UNKNOWN;
}
return Rml::Input::KI_F1;
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
return Rml::Input::KI_NEXT;
@@ -318,12 +326,20 @@ bool find_event_pad_button(
Rml::Input::KeyIdentifier map_gamepad_button(const SDL_GamepadButtonEvent& event) noexcept {
const auto nativeButton = static_cast<SDL_GamepadButton>(event.button);
if (nativeButton == SDL_GAMEPAD_BUTTON_BACK) {
u32 port = 0;
bool foundEventPort = find_event_port(event.which, port);
if (foundEventPort) {
int openMenuButton = getActionBindButton(ActionBinds::OPEN_DUSKLIGHT_MENU, port);
if (openMenuButton != PAD_NATIVE_BUTTON_INVALID && openMenuButton == nativeButton) {
return Rml::Input::KI_F1;
}
}
if (nativeButton == SDL_GAMEPAD_BUTTON_BACK && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port)) {
return Rml::Input::KI_F1;
}
u32 port = 0;
if (!find_event_port(event.which, port)) {
if (!foundEventPort) {
return map_raw_gamepad_button(nativeButton);
}
@@ -438,10 +454,18 @@ bool touch_moved_too_far(
return delta.SquaredMagnitude() > threshold * threshold;
}
void dispatch_menu_key(Rml::Context& context) noexcept {
void emit_key_press(Rml::Context& context, Rml::Input::KeyIdentifier key) noexcept {
context.ProcessMouseLeave();
context.ProcessKeyDown(Rml::Input::KI_F1, 0);
context.ProcessKeyUp(Rml::Input::KI_F1, 0);
context.ProcessKeyDown(key, 0);
}
void emit_key_tap(Rml::Context& context, Rml::Input::KeyIdentifier key) noexcept {
emit_key_press(context, key);
context.ProcessKeyUp(key, 0);
}
void dispatch_menu_key(Rml::Context& context) noexcept {
emit_key_tap(context, Rml::Input::KI_F1);
}
bool handle_touch_menu_tap(Rml::Context& context, const SDL_Event& event) noexcept {
@@ -611,7 +635,9 @@ void process_axis_direction(
if (repeat->held) {
if (released) {
if (!repeat->pending) {
if (repeat->pending) {
emit_key_tap(context, repeat->key);
} else {
context.ProcessKeyUp(repeat->key, 0);
}
set_pad_button_held(port, heldPadButton, false);
@@ -631,7 +657,7 @@ void process_axis_direction(
if (chorded) {
consume_menu_chord(port, context);
}
const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign);
const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign);
if (key == Rml::Input::KI_UNKNOWN) {
return;
}
@@ -642,8 +668,7 @@ void process_axis_direction(
}
begin_gamepad_key(*repeat, key);
context.ProcessMouseLeave();
context.ProcessKeyDown(key, 0);
emit_key_press(context, key);
}
} // namespace
@@ -719,7 +744,7 @@ void handle_event(const SDL_Event& event) noexcept {
if (chorded) {
consume_menu_chord(port, *context);
}
const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton);
const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton);
if (key != Rml::Input::KI_UNKNOWN) {
bool deferred = false;
if (repeat != nullptr) {
@@ -731,8 +756,7 @@ void handle_event(const SDL_Event& event) noexcept {
}
}
if (!deferred) {
context->ProcessMouseLeave();
context->ProcessKeyDown(key, 0);
emit_key_press(*context, key);
}
}
} else {
@@ -744,7 +768,9 @@ void handle_event(const SDL_Event& event) noexcept {
if (repeat != nullptr) {
*repeat = {};
}
if (!wasPending) {
if (wasPending) {
emit_key_tap(*context, key);
} else {
context->ProcessKeyUp(key, 0);
}
}
@@ -771,8 +797,7 @@ void update_input() noexcept {
repeat.pressedAt = now;
repeat.nextRepeatAt =
repeat.repeatable ? now + kGamepadRepeatInitialDelay : 0.0;
context->ProcessMouseLeave();
context->ProcessKeyDown(repeat.key, 0);
emit_key_press(*context, repeat.key);
continue;
}
+2 -3
View File
@@ -18,6 +18,7 @@
#include "modal.hpp"
#include "settings.hpp"
#include "ui.hpp"
#include "warp.hpp"
#include "window.hpp"
#include <chrono>
@@ -51,11 +52,9 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
.autoSelect = false,
});
mTabBar->add_tab("Settings", [this] { push(std::make_unique<SettingsWindow>()); });
// mTabBar->add_tab("Warp", [] {
// // TODO
// });
if (getSettings().backend.enableAdvancedSettings) {
mTabBar->add_tab("Warp", [this] { push(std::make_unique<WarpWindow>()); });
mTabBar->add_tab("Editor", [this] { push(std::make_unique<EditorWindow>()); });
}
+18 -5
View File
@@ -2,6 +2,8 @@
#include "aurora/lib/logging.hpp"
#include "dusk/achievements.h"
#include "dusk/action_bindings.h"
#include "controller_config.hpp"
#include "dusk/livesplit.h"
#include "dusk/speedrun.h"
#include "fmt/format.h"
@@ -101,13 +103,13 @@ Rml::Element* create_controller_warning(Rml::Element* parent) {
auto* heading = append(elem, "heading");
auto* title = append(heading, "span");
title->SetInnerRML("No controller assigned");
title->SetInnerRML("No Device Assigned");
auto* icon = append(heading, "icon");
icon->SetClass("warning", true);
auto* message = append(elem, "message");
auto* content = append(message, "span");
content->SetInnerRML("Configure controller port 1 in Settings.");
content->SetInnerRML("Configure <b>Port 1</b> in Settings.");
return elem;
}
@@ -145,19 +147,29 @@ Rml::String back_button_name() {
#if defined(TARGET_ANDROID) || (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST)
constexpr auto kMenuNotificationPrefix = "3-finger tap or";
#else
constexpr auto kMenuNotificationPrefix = "Press F1 or";
constexpr auto kMenuNotificationPrefix = "Press <b>F1</b> or";
#endif
Rml::Element* create_menu_notification(Rml::Element* parent) {
auto* elem = append(parent, "toast");
elem->SetClass("menu-notification", true);
// Get name of button for action binding if the action is bound
Rml::String padButton{};
SDL_Gamepad* gamepad = gamepad_for_port(PAD_CHAN0);
if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0) && gamepad != nullptr) {
padButton = native_button_name(gamepad,
getActionBindButton(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0));
} else {
padButton = back_button_name();
}
auto* message = append(elem, "message");
auto* row = append(message, "row");
append(row, "span")->SetInnerRML(kMenuNotificationPrefix);
auto* icon = append(row, "icon");
icon->SetClass("controller", true);
append(row, "span")->SetInnerRML(escape(back_button_name()));
append(row, "span")->SetInnerRML("<b>" + escape(padButton) + "</b>");
append(row, "span")->SetInnerRML("to open menu");
return elem;
@@ -342,8 +354,9 @@ void Overlay::update() {
}
}
u32 count = 0;
const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 &&
PADGetKeyButtonBindings(PAD_CHAN0, nullptr) == nullptr &&
PADGetKeyButtonBindings(PAD_CHAN0, &count) == nullptr &&
dynamic_cast<Window*>(top_document()) == nullptr &&
dynamic_cast<WindowSmall*>(top_document()) == nullptr;
if (showControllerWarning && mControllerWarning == nullptr) {
+2 -2
View File
@@ -40,7 +40,7 @@ void applyPresetDusk() {
s.game.enableQuickTransform.setValue(true);
s.game.instantSaves.setValue(true);
s.game.midnasLamentNonStop.setValue(true);
s.game.enableFrameInterpolation.setValue(true);
s.game.enableFrameInterpolation.setValue(FrameInterpMode::Unlimited);
s.game.sunsSong.setValue(true);
s.game.bloomMode.setValue(BloomMode::Dusk);
s.game.internalResolutionScale.setValue(0);
@@ -83,7 +83,7 @@ PresetWindow::PresetWindow() : WindowSmall("modal", "modal-dialog") {
"Enhancements disabled to match the GameCube version. "
"Good for speedrunning or simple nostalgia!",
applyPresetClassic},
{"Dusk",
{"Dusklight",
"Graphics & quality of life tweaks, including some from the Wii U version. "
"Our recommended way to play!",
applyPresetDusk},
+171 -22
View File
@@ -3,18 +3,22 @@
#include "aurora/gfx.h"
#include "bool_button.hpp"
#include "controller_config.hpp"
#include "dusk/app_info.hpp"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/config.hpp"
#include "dusk/hotkeys.h"
#include "dusk/data.hpp"
#include "dusk/file_select.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/io.hpp"
#include "dusk/livesplit.h"
#include "dusk/main.h"
#include "dusk/discord_presence.hpp"
#include "graphics_tuner.hpp"
#include "m_Do/m_Do_main.h"
#include "menu_bar.hpp"
#include "modal.hpp"
#include "number_button.hpp"
#include "menu_bar.hpp"
#include "pane.hpp"
@@ -23,6 +27,7 @@
#include <aurora/lib/window.hpp>
#include <SDL3/SDL_filesystem.h>
#include <fmt/format.h>
#if DUSK_ENABLE_SENTRY_NATIVE
#include "dusk/crash_reporting.h"
@@ -54,6 +59,12 @@ constexpr std::array kFpsOverlayCornerNames = {
"Bottom Right",
};
constexpr std::array kInterpolationModes = {
"Off",
"Capped",
"Unlimited",
};
constexpr std::array kGyroInputModeLabels = {
"Sensor",
"Mouse",
@@ -152,8 +163,8 @@ std::vector<AuroraBackend> available_backends() {
size_t backendCount = 0;
const AuroraBackend* raw = aurora_get_available_backends(&backendCount);
for (size_t i = 0; i < backendCount; ++i) {
// Do not expose NULL or D3D11
if (raw[i] != BACKEND_NULL && raw[i] != BACKEND_D3D11) {
// Do not expose NULL
if (raw[i] != BACKEND_NULL) {
backends.emplace_back(raw[i]);
}
}
@@ -182,6 +193,7 @@ void reset_for_speedrun_mode() {
getSettings().game.infiniteHearts.setSpeedrunValue(false);
getSettings().game.infiniteArrows.setSpeedrunValue(false);
getSettings().game.infiniteSeeds.setSpeedrunValue(false);
getSettings().game.infiniteBombs.setSpeedrunValue(false);
getSettings().game.infiniteOil.setSpeedrunValue(false);
getSettings().game.infiniteOxygen.setSpeedrunValue(false);
@@ -195,6 +207,7 @@ void reset_for_speedrun_mode() {
getSettings().game.fastRoll.setSpeedrunValue(false);
getSettings().game.fastSpinner.setSpeedrunValue(false);
getSettings().game.freeMagicArmor.setSpeedrunValue(false);
getSettings().game.invincibleEnemies.setSpeedrunValue(false);
getSettings().game.pauseOnFocusLost.setSpeedrunValue(false);
aurora_set_pause_on_focus_lost(false);
@@ -293,13 +306,49 @@ private:
Rml::String mCurrentRml;
};
void show_data_folder_error_modal(std::string_view message) {
auto dismiss = [](Modal& modal) {
mDoAud_seStartMenu(kSoundWindowClose);
modal.pop();
};
push_document(std::make_unique<Modal>(Modal::Props{
.title = "Data Folder Not Changed",
.bodyRml = escape(message),
.actions =
{
ModalAction{
.label = "OK",
.onPressed = dismiss,
},
},
.onDismiss = dismiss,
.icon = "warning",
}));
if (auto* doc = top_document()) {
doc->focus();
}
}
void data_folder_dialog_callback(void*, const char* path, const char* error) {
if (error != nullptr || path == nullptr) {
if (error != nullptr) {
show_data_folder_error_modal(error);
return;
}
if (data::set_custom_data_path(path)) {
mDoAud_seStartMenu(kSoundItemChange);
if (path == nullptr) {
return;
}
std::string dataPathError;
if (data::set_custom_data_path(path, &dataPathError)) {
mDoAud_seStartMenu(kSoundItemChange);
return;
}
if (dataPathError.empty()) {
dataPathError =
fmt::format("{} could not use the selected folder as its data folder.", AppName);
}
show_data_folder_error_modal(dataPathError);
}
const Rml::String kInternalResolutionHelpText =
@@ -308,13 +357,15 @@ const Rml::String kInternalResolutionHelpText =
const Rml::String kShadowResolutionHelpText =
"Configure the shadow-map resolution. Higher values improve shadow quality but increase GPU "
"and memory usage.";
const Rml::String kResamplerHelpText =
"Configure the sampling method used when scaling the internal resolution for final presentation.";
const Rml::String kBloomHelpText =
"Configure the post-processing bloom effect. Classic uses the original bloom pass; Dusklight uses "
"a higher-quality bloom pass.";
const Rml::String kBloomBrightnessHelpText =
"Configure bloom intensity. Higher values make bright areas glow more strongly.";
const Rml::String kUnlockFramerateHelpText =
"Uses inter-frame interpolation to enable higher frame rates.<br/><br/>May introduce minor "
"<br/>Uses inter-frame interpolation to enable higher frame rates.<br/><br/>May introduce minor "
"visual artifacts or animation glitches.";
int float_setting_percent(ConfigVar<float>& var) {
@@ -397,6 +448,31 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar<f
return button;
}
SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>& var,
Rml::String key, Rml::String helpText, int min, int max, int step = 5,
std::function<bool()> isDisabled = {}, std::string suffix = "") {
auto& button = leftPane.add_child<NumberButton>(NumberButton::Props{
.key = std::move(key),
.getValue = [&var] { return var; },
.setValue =
[&var, min, max](int value) {
var.setValue(std::clamp(value, min, max));
config::Save();
},
.isDisabled = std::move(isDisabled),
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
.min = min,
.max = max,
.step = step,
.suffix = suffix,
});
leftPane.register_control(button, rightPane, [helpText = std::move(helpText)](Pane& pane) {
pane.clear();
pane.add_text(helpText);
});
return button;
}
template <typename T>
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& var,
const GraphicsTunerProps& props, bool prelaunch) {
@@ -710,7 +786,6 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
pane.add_rml(
"<br/>Display the current framerate in a corner of the screen while playing.");
});
leftPane.add_section("Resolution");
graphics_tuner_control(*this, leftPane, rightPane,
getSettings().game.internalResolutionScale,
@@ -732,6 +807,15 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.valueMax = 8,
.defaultValue = 1,
}, mPrelaunch);
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.resampler,
GraphicsTunerProps{
.option = GraphicsOption::Resampler,
.title = "Output Resampling",
.helpText = kResamplerHelpText,
.valueMin = static_cast<int>(Resampler::Bilinear),
.valueMax = static_cast<int>(Resampler::Area),
.defaultValue = static_cast<int>(Resampler::Bilinear),
}, mPrelaunch);
leftPane.add_section("Post-Processing");
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
@@ -751,14 +835,49 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.valueMin = 0,
.valueMax = 100,
.defaultValue = 100,
.step = 10,
}, mPrelaunch);
leftPane.add_section("Rendering");
config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation,
config_bool_select(leftPane, rightPane, getSettings().game.enableTextureReplacements,
{
.key = "Unlock Framerate",
.helpText = kUnlockFramerateHelpText,
.key = "Use Texture Pack",
.helpText = "Enable installed texture replacements.",
.onChange = [](bool value) { aurora_set_texture_replacements_enabled(value); },
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Unlock Framerate",
.getValue =
[] {
return kInterpolationModes[static_cast<u8>(getSettings().game.enableFrameInterpolation.getValue())];
},
.isModified =
[] {
return getSettings().game.enableFrameInterpolation.getValue() !=
getSettings().game.enableFrameInterpolation.getDefaultValue();
},
}),
rightPane, [](Pane& pane) {
for (int i = 0; i < kInterpolationModes.size(); i++) {
pane.add_button({
.text = kInterpolationModes[i],
.isSelected =
[i] {
return getSettings().game.enableFrameInterpolation.getValue() == static_cast<FrameInterpMode>(i);
},
})
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().game.enableFrameInterpolation.setValue(static_cast<FrameInterpMode>(i));
config::Save();
});
}
pane.add_rml(kUnlockFramerateHelpText);
});
config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate,
"Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1,
[] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; });
config_bool_select(leftPane, rightPane, getSettings().game.enableDepthOfField,
{
.key = "Enable Depth of Field",
@@ -787,18 +906,18 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
});
};
leftPane.add_section("Controller");
leftPane.register_control(leftPane.add_button("Configure Controller").on_pressed([this] {
leftPane.add_section("Inputs");
leftPane.register_control(leftPane.add_button("Configure Inputs").on_pressed([this] {
push(std::make_unique<ControllerConfigWindow>(mPrelaunch));
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_text("Open controller binding configuration.");
pane.add_text("Open input binding configuration.");
});
config_bool_select(leftPane, rightPane, getSettings().game.allowBackgroundInput,
{
.key = "Allow Background Input",
.helpText = "Allow controller input even when the game window is not focused.",
.key = "Allow Background Inputs",
.helpText = "Allow inputs even when the game window is not focused.",
.onChange = [](bool value) { aurora_set_background_input(value); },
});
@@ -815,9 +934,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
"Free Camera Sensitivity", "Adjusts twin-stick camera sensitivity.", 50, 200, 5,
[] { return !getSettings().game.freeCamera; });
addOption("Invert First Person X Axis", getSettings().game.invertFirstPersonXAxis,
"Invert horizontal movement while aiming with items or first person camera. Applies to both stick and gyro aiming.");
"Invert horizontal movement while aiming with items or first person camera. Applies only to the control stick (the gyroscope can be inverted in Input settings).");
addOption("Invert First Person Y Axis", getSettings().game.invertFirstPersonYAxis,
"Invert vertical movement while aiming with items or first person camera. Applies to both stick and gyro aiming.");
"Invert vertical movement while aiming with items or first person camera. Applies only to the control stick (the gyroscope can be inverted in Input settings).");
leftPane.add_section("Gyro");
leftPane.register_control(
@@ -892,6 +1011,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
addOption("Turbo Key", getSettings().game.enableTurboKeybind,
"Hold Tab to increase game speed by up to 4x.",
[] { return getSettings().game.speedrunMode; });
addOption("Reset Key (" + Rml::String{hotkeys::DO_RESET} + ")",
getSettings().game.enableResetKeybind,
"Press " + Rml::String{hotkeys::DO_RESET} + " to reset the game.");
});
add_tab("Audio", [this](Rml::Element* content) {
@@ -908,7 +1030,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
[](int value) {
getSettings().audio.masterVolume.setValue(value);
config::Save();
audio::SetMasterVolume(value / 100.f);
audio::SetMasterVolume(audio::MasterVolumeToLinear(value / 100.0f));
},
.isModified =
[] {
@@ -1025,8 +1147,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
addOption("Faster Tears of Light", getSettings().game.fastTears,
"Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
addSpeedrunDisabledOption("Autosave", getSettings().game.autoSave,
"Autosaves the game when going to a new area, opening a dungeon door, "
"or getting a new item.");
"Autosaves the game when going to a new area or opening a dungeon door.");
addOption("Instant Saves", getSettings().game.instantSaves,
"Skips the delay when writing to the Memory Card.");
addOption("Hold B for Instant Text", getSettings().game.instantText,
@@ -1105,6 +1226,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
addCheat("Infinite Hearts", getSettings().game.infiniteHearts, "Keeps your health full.");
addCheat(
"Infinite Arrows", getSettings().game.infiniteArrows, "Keeps your arrow count full.");
addCheat("Infinite Seeds", getSettings().game.infiniteSeeds, "Keeps your slingshot pellets (seeds) full.");
addCheat("Infinite Bombs", getSettings().game.infiniteBombs, "Keeps all bomb bags full.");
addCheat("Infinite Oil", getSettings().game.infiniteOil, "Keeps your lantern oil full.");
addCheat("Infinite Oxygen", getSettings().game.infiniteOxygen,
@@ -1122,7 +1244,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
addCheat("Always Greatspin", getSettings().game.alwaysGreatspin,
"Allows the Great Spin attack without requiring full health.");
addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots,
"Speeds up movement while wearing the Iron Boots.");
"Speeds up movement while heavy, including wearing the Iron Boots, holding the Ball and Chain, wearing Magic Armor without rupees, etc.");
addCheat("Can Transform Anywhere", getSettings().game.canTransformAnywhere,
"Allows transforming even if NPCs are looking.");
addCheat("Fast Roll", getSettings().game.fastRoll,
@@ -1131,6 +1253,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
"Speeds up Spinner movement while holding R.");
addCheat("Free Magic Armor", getSettings().game.freeMagicArmor,
"Lets the magic armor work without consuming rupees.");
addCheat("Invincible Enemies", getSettings().game.invincibleEnemies,
"Prevents enemies from taking damage.");
});
add_tab("Interface", [this](Rml::Element* content) {
@@ -1202,7 +1326,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
});
pane.add_button(
{
.text = "Controller",
.text = "Missing Device",
.isSelected =
[] { return getSettings().game.enableControllerToasts.getValue(); },
})
@@ -1251,6 +1375,20 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.helpText = "Checks GitHub releases for a new Dusklight version on startup.<br/><br/>"
"No personal information is transmitted or collected.",
});
#ifdef DUSK_DISCORD
config_bool_select(leftPane, rightPane, getSettings().game.enableDiscordPresence,
{
.key = "Enable Discord Rich Presence",
.helpText = "Enable Dusklight to integrate with Discord Rich Presence. This allows Discord to show your status in-game.",
.onChange = [](bool enabled) {
if (enabled) {
dusk::discord::initialize();
} else {
dusk::discord::shutdown();
}
},
});
#endif
config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings,
{
.key = "Enable Advanced Settings",
@@ -1269,6 +1407,17 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
},
.isDisabled = [] { return getSettings().game.speedrunMode; },
});
config_bool_select(leftPane, rightPane, getSettings().game.showInputViewer,
{
.key = "Show Input Viewer",
.helpText = "Display a controller input overlay while playing.",
});
config_bool_select(leftPane, rightPane, getSettings().game.showInputViewerGyro,
{
.key = "Show Gyro Input Viewer",
.helpText = "Show gyro sensor values in the input viewer.",
.isDisabled = [] { return !getSettings().game.showInputViewer; },
});
leftPane.add_section("Game");
config_bool_select(leftPane, rightPane, getSettings().game.hideTvSettingsScreen,
+2 -2
View File
@@ -130,7 +130,7 @@ void handle_event(const SDL_Event& event) noexcept {
if (getSettings().game.enableControllerToasts) {
const char* name = SDL_GetGamepadName(gamepad);
Rml::String content = fmt::format("<span>{}</span>", name ? name : "[Unknown]");
Rml::String title = "Controller connected";
Rml::String title = "Device Connected";
if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
title = fmt::format(
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>", title,
@@ -163,7 +163,7 @@ void handle_event(const SDL_Event& event) noexcept {
const char* name = SDL_GetGamepadNameForID(event.gdevice.which);
push_toast({
.type = "controller",
.title = "Controller disconnected",
.title = "Device Disconnected",
.content = name ? name : "[Unknown]",
.duration = std::chrono::seconds(4),
});
+4
View File
@@ -26,6 +26,8 @@ struct Toast {
constexpr u32 kSoundClick = Z2SE_SY_CURSOR_OK;
// "Play" button clicked/pressed
constexpr u32 kSoundPlay = Z2SE_SY_ITEM_COMBINE_ON;
// Input binding changed
constexpr u32 kSoundBindingChanged = Z2SE_SY_ITEM_SET_X;
// Menu button pressed (open/close menu bar or hide/show the active window)
constexpr u32 kSoundMenuOpen = Z2SE_SY_MENU_SUB_IN;
@@ -49,6 +51,8 @@ constexpr u32 kSoundItemDisable = Z2SE_SUBJ_VIEW_OUT;
// Achievement unlocked
constexpr u32 kSoundAchievementUnlock = Z2SE_NAVI_FLY;
// Warning prompt
constexpr u32 kSoundWarning = Z2SE_SY_COW_GET_IN;
struct Insets {
float top = 0.0f;
+334
View File
@@ -0,0 +1,334 @@
#include "warp.hpp"
#include "editor.hpp"
#include "pane.hpp"
#include "dusk/map_loader_definitions.h"
#include "fmt/format.h"
namespace dusk::ui {
namespace {
constexpr int kMinLayer = -1;
constexpr int kMaxLayer = 14;
struct WarpSelectionState {
int regionIdx = 0;
int mapIdx = 0;
int roomIdx = 0;
int pointIdx = 0;
int layer = -1;
bool showInternalNames = false;
};
WarpSelectionState& selection_state() {
static WarpSelectionState state;
return state;
}
const RegionEntry* selected_region(const WarpSelectionState& state) {
if (state.regionIdx < 0 || state.regionIdx >= static_cast<int>(gameRegions.size())) {
return nullptr;
}
return &gameRegions[state.regionIdx];
}
const MapEntry* selected_map(const WarpSelectionState& state) {
const auto* region = selected_region(state);
if (region == nullptr || state.mapIdx < 0 || state.mapIdx >= static_cast<int>(region->maps.size())) {
return nullptr;
}
return &region->maps[state.mapIdx];
}
const RoomEntry* selected_room(const WarpSelectionState& state) {
const auto* map = selected_map(state);
if (map == nullptr || state.roomIdx < 0 || state.roomIdx >= static_cast<int>(map->mapRooms.size())) {
return nullptr;
}
return &map->mapRooms[state.roomIdx];
}
const s16* selected_point(const WarpSelectionState& state) {
const auto* room = selected_room(state);
if (room == nullptr || state.pointIdx < 0 ||
state.pointIdx >= static_cast<int>(room->roomPoints.size()))
{
return nullptr;
}
return &room->roomPoints[state.pointIdx];
}
void clamp_indices(WarpSelectionState& state) {
if (gameRegions.empty()) {
state.regionIdx = -1;
state.mapIdx = -1;
state.roomIdx = -1;
state.pointIdx = -1;
state.layer = std::clamp(state.layer, kMinLayer, kMaxLayer);
return;
}
state.regionIdx = std::clamp(state.regionIdx, 0, static_cast<int>(gameRegions.size()) - 1);
const auto& region = gameRegions[state.regionIdx];
if (region.maps.empty()) {
state.mapIdx = -1;
state.roomIdx = -1;
state.pointIdx = -1;
state.layer = std::clamp(state.layer, kMinLayer, kMaxLayer);
return;
}
state.mapIdx = std::clamp(state.mapIdx, 0, static_cast<int>(region.maps.size()) - 1);
const auto& map = region.maps[state.mapIdx];
if (map.mapRooms.empty()) {
state.roomIdx = -1;
state.pointIdx = -1;
state.layer = std::clamp(state.layer, kMinLayer, kMaxLayer);
return;
}
state.roomIdx = std::clamp(state.roomIdx, 0, static_cast<int>(map.mapRooms.size()) - 1);
const auto& room = map.mapRooms[state.roomIdx];
if (room.roomPoints.empty()) {
state.pointIdx = -1;
} else {
state.pointIdx = std::clamp(state.pointIdx, 0, static_cast<int>(room.roomPoints.size()) - 1);
}
state.layer = std::clamp(state.layer, kMinLayer, kMaxLayer);
}
bool can_warp(const WarpSelectionState& state) {
return selected_point(state) != nullptr;
}
void reset_selection(WarpSelectionState& state) {
state.roomIdx = 0;
state.pointIdx = 0;
state.layer = kMinLayer;
clamp_indices(state);
}
void populate_map_picker(Pane& pane, WarpSelectionState& state) {
pane.clear();
clamp_indices(state);
if (state.regionIdx < 0 || state.regionIdx >= static_cast<int>(gameRegions.size())) {
return;
}
pane.add_button({
.text = "Show Internal Names",
.isSelected = [&state] { return state.showInternalNames; },
})
.on_pressed([&pane, &state] {
mDoAud_seStartMenu(kSoundItemChange);
state.showInternalNames = !state.showInternalNames;
populate_map_picker(pane, state);
});
pane.add_section("Maps");
const auto& region = gameRegions[state.regionIdx];
for (int i = 0; i < static_cast<int>(region.maps.size()); ++i) {
pane.add_button({
.text = stage_option_label(region.maps[i], state.showInternalNames),
.isSelected = [i, &state] { return state.mapIdx == i; },
})
.on_pressed([i, &state] {
mDoAud_seStartMenu(kSoundItemChange);
if (state.mapIdx != i) {
state.mapIdx = i;
reset_selection(state);
}
});
}
}
} // namespace
WarpWindow::WarpWindow() {
add_tab("Warp", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
auto& state = selection_state();
clamp_indices(state);
leftPane.add_section("Destination");
leftPane.register_control(
leftPane.add_select_button({
.key = "Region",
.getValue =
[&state] {
clamp_indices(state);
const auto* region = selected_region(state);
return region == nullptr ? Rml::String{"None"} :
Rml::String{region->regionName};
},
}),
rightPane, [&state](Pane& pane) {
pane.clear();
for (int i = 0; i < static_cast<int>(gameRegions.size()); ++i) {
pane.add_button({
.text = gameRegions[i].regionName,
.isSelected = [i, &state] { return state.regionIdx == i; },
})
.on_pressed([i, &state] {
mDoAud_seStartMenu(kSoundItemChange);
if (state.regionIdx != i) {
state.regionIdx = i;
state.mapIdx = 0;
reset_selection(state);
}
});
}
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Map",
.getValue =
[&state] {
clamp_indices(state);
const auto* map = selected_map(state);
return map == nullptr ? Rml::String{"None"} :
stage_option_label(*map, state.showInternalNames);
},
}),
rightPane, [&state](Pane& pane) { populate_map_picker(pane, state); });
leftPane.register_control(
leftPane.add_select_button({
.key = "Room",
.getValue = [&state] {
clamp_indices(state);
const auto* room = selected_room(state);
return room == nullptr ? Rml::String{"None"} :
fmt::format("{}", room->roomNo);
},
.isDisabled = [&state] {
clamp_indices(state);
const auto* map = selected_map(state);
return map == nullptr || map->mapRooms.size() <= 1;
},
}),
rightPane, [&state](Pane& pane) {
pane.clear();
clamp_indices(state);
if (state.regionIdx < 0 || state.regionIdx >= static_cast<int>(gameRegions.size())) {
return;
}
const auto& region = gameRegions[state.regionIdx];
if (state.mapIdx < 0 || state.mapIdx >= static_cast<int>(region.maps.size())) {
return;
}
const auto& map = region.maps[state.mapIdx];
for (int i = 0; i < static_cast<int>(map.mapRooms.size()); ++i) {
pane.add_button({
.text = fmt::format("{}", map.mapRooms[i].roomNo),
.isSelected = [i, &state] { return state.roomIdx == i; },
})
.on_pressed([i, &state] {
mDoAud_seStartMenu(kSoundItemChange);
if (state.roomIdx != i) {
state.roomIdx = i;
state.pointIdx = 0;
clamp_indices(state);
}
});
}
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Point",
.getValue = [&state] {
clamp_indices(state);
const auto* point = selected_point(state);
return point == nullptr ? Rml::String{"None"} : fmt::format("{}", *point);
},
.isDisabled = [&state] {
clamp_indices(state);
const auto* room = selected_room(state);
return room == nullptr || room->roomPoints.size() <= 1;
},
}),
rightPane, [&state](Pane& pane) {
pane.clear();
clamp_indices(state);
if (state.regionIdx < 0 || state.regionIdx >= static_cast<int>(gameRegions.size())) {
return;
}
const auto& region = gameRegions[state.regionIdx];
if (state.mapIdx < 0 || state.mapIdx >= static_cast<int>(region.maps.size())) {
return;
}
const auto& map = region.maps[state.mapIdx];
if (state.roomIdx < 0 || state.roomIdx >= static_cast<int>(map.mapRooms.size())) {
return;
}
const auto& room = map.mapRooms[state.roomIdx];
for (int i = 0; i < static_cast<int>(room.roomPoints.size()); ++i) {
pane.add_button({
.text = fmt::format("{}", room.roomPoints[i]),
.isSelected = [i, &state] { return state.pointIdx == i; },
})
.on_pressed([i, &state] {
if (state.pointIdx != i) {
mDoAud_seStartMenu(kSoundItemChange);
state.pointIdx = i;
clamp_indices(state);
}
});
}
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Layer",
.getValue = [&state] { return fmt::format("{}", state.layer); },
}),
rightPane, [&state](Pane& pane) {
pane.clear();
for (int layer = kMinLayer; layer <= kMaxLayer; ++layer) {
pane.add_button({
.text = fmt::format("{}", layer),
.isSelected = [layer, &state] { return state.layer == layer; },
})
.on_pressed([layer, &state] {
if (state.layer != layer) {
mDoAud_seStartMenu(kSoundItemChange);
state.layer = layer;
}
});
}
});
leftPane.add_section("Action");
leftPane.register_control(
leftPane.add_button({
.text = "Warp",
.isDisabled = [&state] {
clamp_indices(state);
return !can_warp(state);
},
})
.on_pressed([&state] {
clamp_indices(state);
if (!can_warp(state)) {
return;
}
mDoAud_seStartMenu(kSoundClick);
const auto& region = gameRegions[state.regionIdx];
const auto& map = region.maps[state.mapIdx];
const auto& room = map.mapRooms[state.roomIdx];
dComIfGp_setNextStage( map.mapFile, room.roomPoints[state.pointIdx], room.roomNo, state.layer);
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_text("Warp to the selected destination.");
});
});
}
} // namespace dusk::ui
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "window.hpp"
namespace dusk::ui {
class WarpWindow : public Window {
public:
WarpWindow();
};
} // namespace dusk::ui