mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-06-14 05:34:38 -04:00
d25695be0e
serves as a sort of placeholder until Midna can be properly edited to behave like console rando
504 lines
20 KiB
C++
504 lines
20 KiB
C++
#include "rando_config.hpp"
|
|
|
|
#include <mutex>
|
|
#include <thread>
|
|
|
|
#include "SDL3/SDL_filesystem.h"
|
|
|
|
#include "bool_button.hpp"
|
|
#include "modal.hpp"
|
|
#include "dusk/app_info.hpp"
|
|
#include "dusk/config.hpp"
|
|
#include "dusk/data.hpp"
|
|
#include "dusk/logging.h"
|
|
#include "number_button.hpp"
|
|
#include "pane.hpp"
|
|
#include "string_button.hpp"
|
|
#include "dusk/randomizer/game/tools.h"
|
|
#include "dusk/randomizer/generator/seedgen/seed.hpp"
|
|
|
|
namespace dusk::ui {
|
|
struct ConfigBoolProps {
|
|
Rml::String key;
|
|
Rml::String icon;
|
|
Rml::String helpText;
|
|
std::function<void(bool)> onChange;
|
|
std::function<bool()> isDisabled;
|
|
};
|
|
|
|
static bool generatingSeed = false;
|
|
static std::string generationStatusMsg{};
|
|
|
|
static void StartSeedGeneration() {
|
|
GenerateAndWriteSeed(generationStatusMsg);
|
|
generatingSeed = false;
|
|
DuskLog.debug("{}", generationStatusMsg);
|
|
}
|
|
|
|
randomizer::seedgen::settings::Setting* FindSetting(const std::string& key) {
|
|
if (key.empty()) {
|
|
DuskLog.fatal("Key is empty! Unable to find setting.");
|
|
}
|
|
|
|
// TODO: handle multi-world selection
|
|
auto& settings = GetRandomizerConfig().GetSettingsList().front();
|
|
try {
|
|
return &settings.GetMap().at(key);
|
|
} catch (std::exception e) {
|
|
DuskLog.fatal("Failed to get Settings Key: {}", key);
|
|
}
|
|
}
|
|
|
|
void SaveConfig() {
|
|
GetRandomizerConfig().WriteToFile(GetRandomizerSettingsPath(), GetRandomizerPreferencesPath());
|
|
}
|
|
|
|
bool TryCreateRandomSeed() {
|
|
auto& config = GetRandomizerConfig();
|
|
|
|
const std::string& configSeed = config.GetSeed();
|
|
if (configSeed.empty()) {
|
|
config.SetSeed(randomizer::seedgen::seed::GenerateSeed());
|
|
SaveConfig();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ripped straight from settings window
|
|
SelectButton& config_bool_select(
|
|
Pane& leftPane, Pane& rightPane, ConfigVar<bool>& var, ConfigBoolProps props) {
|
|
auto& button = leftPane.add_child<BoolButton>(BoolButton::Props{
|
|
.key = std::move(props.key),
|
|
.icon = std::move(props.icon),
|
|
.getValue = [&var] { return var.getValue(); },
|
|
.setValue =
|
|
[&var, callback = std::move(props.onChange)](bool value) {
|
|
if (value == var.getValue()) {
|
|
return;
|
|
}
|
|
var.setValue(value);
|
|
config::Save();
|
|
if (callback) {
|
|
callback(value);
|
|
}
|
|
},
|
|
.isDisabled = std::move(props.isDisabled),
|
|
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
|
|
});
|
|
leftPane.register_control(
|
|
button, rightPane, [helpText = std::move(props.helpText)](Pane& pane) {
|
|
pane.clear();
|
|
pane.add_rml(helpText);
|
|
});
|
|
return button;
|
|
}
|
|
|
|
void rando_config_group(Pane& leftPane, Pane& rightPane, std::string settingKey,
|
|
std::function<Component*(const std::string&, Pane&)> onSelected = nullptr) {
|
|
auto randoSettings = randomizer::seedgen::settings::GetAllSettingsInfo();
|
|
auto& settingData = randoSettings->at(settingKey);
|
|
|
|
if (settingData == nullptr) {
|
|
return;
|
|
}
|
|
auto curSetting = FindSetting(settingKey);
|
|
|
|
leftPane.register_control(
|
|
leftPane.add_select_button({
|
|
.key = settingKey,
|
|
.getValue =
|
|
[curSetting] { return Rml::String{curSetting->GetCurrentOption()}; },
|
|
}),
|
|
rightPane, [curSetting, onSelected](Pane& pane) {
|
|
auto curSelIdx = curSetting->GetCurrentOptionIndex();
|
|
auto settingInfo = curSetting->GetInfo();
|
|
|
|
Rml::Element* text_elem = pane.add_rml(settingInfo->GetDescriptions().at(curSelIdx));
|
|
|
|
for (int i = 0; i < settingInfo->GetOptions().size(); ++i) {
|
|
pane.add_button(
|
|
{
|
|
.text = settingInfo->GetOptions()[i],
|
|
.isSelected = [curSetting, i] {
|
|
return curSetting->GetCurrentOptionIndex() == i;
|
|
},
|
|
})
|
|
.on_pressed([i, text_elem, curSetting] {
|
|
auto settingInfo = curSetting->GetInfo();
|
|
|
|
mDoAud_seStartMenu(kSoundItemChange);
|
|
curSetting->SetCurrentOption(i);
|
|
text_elem->SetInnerRML(settingInfo->GetDescriptions().at(i));
|
|
|
|
SaveConfig();
|
|
});
|
|
}
|
|
|
|
if (onSelected) {
|
|
onSelected(curSetting->GetCurrentOption(), pane);
|
|
}
|
|
});
|
|
}
|
|
|
|
SelectButton& rando_config_toggle(
|
|
Pane& leftPane, Pane& rightPane, std::string settingKey) {
|
|
auto setting = FindSetting(settingKey);
|
|
|
|
auto& button = leftPane.add_child<BoolButton>(BoolButton::Props{
|
|
.key = settingKey,
|
|
.getValue = [setting] { return setting->GetCurrentOptionIndex() != 0; },
|
|
.setValue =
|
|
[setting](bool value) {
|
|
auto idx = setting->GetCurrentOptionIndex();
|
|
if (idx == value) {
|
|
return;
|
|
}
|
|
|
|
setting->SetCurrentOption(value);
|
|
|
|
SaveConfig();
|
|
},
|
|
});
|
|
auto& comp = leftPane.register_control(
|
|
button, rightPane, [setting](Pane& pane) {
|
|
pane.clear();
|
|
|
|
auto info = setting->GetInfo();
|
|
pane.add_rml(info->GetDescriptions()[setting->GetCurrentOptionIndex()]);
|
|
});
|
|
|
|
comp.listen(comp.root(), Rml::EventId::Click, [&rightPane, setting](Rml::Event&) {
|
|
rightPane.clear();
|
|
|
|
auto info = setting->GetInfo();
|
|
rightPane.add_rml(info->GetDescriptions()[setting->GetCurrentOptionIndex()]);
|
|
});
|
|
|
|
return button;
|
|
}
|
|
|
|
NumberButton* rando_add_optional_setting(std::string optionValue, std::string optionsKeyPrefix,
|
|
Pane& pane) {
|
|
std::string fullOptionalKey = fmt::format("{} {}", optionsKeyPrefix, optionValue);
|
|
|
|
// check if setting exists
|
|
auto randoSettings = randomizer::seedgen::settings::GetAllSettingsInfo();
|
|
if (!randoSettings->contains(fullOptionalKey)) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto curSetting = FindSetting(fullOptionalKey);
|
|
const auto& options = curSetting->GetInfo()->GetOptions();
|
|
|
|
return &pane.add_child<NumberButton>(NumberButton::Props{
|
|
.key = fmt::format("{} Count", optionValue),
|
|
.getValue = [curSetting] { return curSetting->GetCurrentOptionAsNumber(); },
|
|
.setValue = [curSetting](int value) {
|
|
curSetting->SetCurrentOption(std::to_string(value));
|
|
},
|
|
.min = std::stoi(options.front()),
|
|
.max = std::stoi(options.back()),
|
|
});
|
|
}
|
|
|
|
Document* show_seed_gen_modal(std::string_view message) {
|
|
auto* modal = &push_document(std::make_unique<Modal>(Modal::Props{
|
|
.title = "Randomizer",
|
|
.bodyRml = escape(message),
|
|
.onDismiss = [](Modal& modal) {
|
|
mDoAud_seStartMenu(kSoundWindowClose);
|
|
modal.pop();
|
|
},
|
|
.icon = "verifying",
|
|
}));
|
|
|
|
if (auto* doc = top_document()) {
|
|
doc->focus();
|
|
}
|
|
|
|
return modal;
|
|
}
|
|
|
|
RandomizerWindow::RandomizerWindow() {
|
|
add_tab("Seed Management", [this](Rml::Element* content) {
|
|
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
|
auto& rightPane = add_child<Pane>(content, Pane::Type::Controlled);
|
|
|
|
leftPane.register_control(
|
|
leftPane.add_select_button({
|
|
.key = "Selected Seed",
|
|
.getValue =
|
|
[] {
|
|
return randomizer_GetContext().mHash.empty() ?
|
|
"None" :
|
|
randomizer_GetContext().mHash;
|
|
},
|
|
}),
|
|
rightPane, [](Pane& pane) {
|
|
std::filesystem::path seedDirectory =
|
|
dusk::data::configured_data_path() / "randomizer" / "seeds";
|
|
|
|
if (!std::filesystem::exists(seedDirectory)) {
|
|
std::filesystem::create_directories(seedDirectory);
|
|
}
|
|
|
|
if (std::filesystem::is_empty(seedDirectory)) {
|
|
pane.add_rml(
|
|
"No seeds generated! Check out the options tab and generate a seed using the button below.");
|
|
return;
|
|
}
|
|
|
|
pane.add_rml(
|
|
"Apply a seed generated using the randomizer generator. (Note: must be done before selecting a save file).");
|
|
|
|
for (const auto& entry : std::filesystem::directory_iterator(seedDirectory)) {
|
|
if (entry.is_directory()) {
|
|
std::string hash = entry.path().filename().string();
|
|
|
|
pane.add_button(
|
|
{
|
|
.text = hash,
|
|
.isSelected = [hash] {
|
|
return randomizer_GetContext().mHash == hash;
|
|
},
|
|
})
|
|
.on_pressed([hash] {
|
|
randomizer_GetContext() = RandomizerContext();
|
|
randomizer_GetContext().LoadFromHash(hash);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
leftPane.register_control(
|
|
leftPane.add_button("Delete Seeds"),
|
|
rightPane, [](Pane& pane) {
|
|
std::filesystem::path seedDirectory =
|
|
dusk::data::configured_data_path() / "randomizer" / "seeds";
|
|
|
|
if (!std::filesystem::exists(seedDirectory)) {
|
|
std::filesystem::create_directories(seedDirectory);
|
|
}
|
|
|
|
if (std::filesystem::is_empty(seedDirectory)) {
|
|
pane.add_rml(
|
|
"No seeds generated.");
|
|
return;
|
|
}
|
|
|
|
pane.add_rml(
|
|
"Delete any seed currently not active in the randomizer.");
|
|
|
|
for (const auto& entry : std::filesystem::directory_iterator(seedDirectory)) {
|
|
if (entry.is_directory()) {
|
|
std::string hash = entry.path().filename().string();
|
|
|
|
// TODO: our ui lib doesnt have an easy way to either refresh or remove values from the right pane
|
|
pane.add_button(
|
|
{
|
|
.text = hash,
|
|
.isDisabled = [hash] {
|
|
return !playerIsOnTitleScreen() || randomizer_GetContext().mHash == hash;
|
|
}
|
|
})
|
|
.on_pressed([hash, entry] {
|
|
if (randomizer_GetContext().mHash != hash) {
|
|
std::filesystem::remove_all(entry);
|
|
} else if (!randomizer_IsActive()){
|
|
// If the user selected the currently seed, but it's not active, we'll allow the delete
|
|
std::filesystem::remove_all(entry);
|
|
randomizer_GetContext() = RandomizerContext();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
leftPane.register_control(leftPane.add_button(ControlledButton::Props{
|
|
.text = "Deactivate Seed",
|
|
.isDisabled = [] { return randomizer_GetContext().mHash.empty() || !playerIsOnTitleScreen(); }
|
|
}).on_pressed([] {
|
|
if (!randomizer_IsActive()) {
|
|
randomizer_GetContext() = RandomizerContext();
|
|
}
|
|
}), rightPane, [](Pane& pane) {
|
|
pane.clear();
|
|
pane.add_rml("Disables currently chosen seed.");
|
|
});
|
|
|
|
leftPane.register_control(leftPane.add_button("Generate Seed").on_pressed(
|
|
[this, &rightPane] {
|
|
if (TryCreateRandomSeed()) {
|
|
DuskLog.info("Created new Seed for generator.");
|
|
}
|
|
|
|
std::thread randoGenerationThread(StartSeedGeneration);
|
|
randoGenerationThread.detach();
|
|
|
|
generatingSeed = true;
|
|
m_genSeedModal = show_seed_gen_modal("Generating Seed...");
|
|
|
|
}),rightPane, [](Pane& pane) {
|
|
pane.clear();
|
|
pane.add_rml(
|
|
"Generate a Randomizer seed using the current configuration options, and the supplied seed string.");
|
|
});
|
|
|
|
leftPane.register_control(leftPane.add_child<StringButton>(StringButton::Props{
|
|
.key = "Seed String",
|
|
.getValue = [] {
|
|
return GetRandomizerConfig().GetSeed();
|
|
},
|
|
.setValue = [](Rml::String value) {
|
|
GetRandomizerConfig().SetSeed(value);
|
|
SaveConfig();
|
|
},
|
|
.maxLength = 32,
|
|
}),
|
|
rightPane, [](Pane& pane) {
|
|
pane.clear();
|
|
pane.add_rml(
|
|
"Current value of the seed used by the randomizer for generation. Leave blank for a random value.");
|
|
});
|
|
});
|
|
|
|
add_tab("Seed Options", [this](Rml::Element* content) {
|
|
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
|
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
|
|
|
leftPane.add_section("Logic Settings");
|
|
|
|
rando_config_group(leftPane, rightPane, "Logic Rules");
|
|
|
|
leftPane.add_section("Access Options");
|
|
|
|
rando_config_group(leftPane, rightPane, "Hyrule Barrier Requirements",
|
|
[](const std::string& value, Pane& pane) {
|
|
return rando_add_optional_setting(value, "Hyrule Barrier", pane);
|
|
});
|
|
rando_config_group(leftPane, rightPane, "Palace of Twilight Requirements");
|
|
rando_config_group(leftPane, rightPane, "Faron Woods Logic");
|
|
|
|
leftPane.add_section("World (TODO)");
|
|
|
|
leftPane.add_section("Item Pool");
|
|
|
|
rando_config_toggle(leftPane, rightPane, "Golden Bugs");
|
|
rando_config_toggle(leftPane, rightPane, "Sky Characters");
|
|
rando_config_toggle(leftPane, rightPane, "Gifts From NPCs");
|
|
rando_config_toggle(leftPane, rightPane, "Shop Items");
|
|
rando_config_toggle(leftPane, rightPane, "Hidden Skills");
|
|
rando_config_toggle(leftPane, rightPane, "Hidden Rupees");
|
|
rando_config_toggle(leftPane, rightPane, "Freestanding Rupees");
|
|
|
|
rando_config_group(leftPane, rightPane, "Poe Souls");
|
|
rando_config_group(leftPane, rightPane, "Ilia Memory Quest");
|
|
rando_config_group(leftPane, rightPane, "Item Scarcity");
|
|
|
|
leftPane.add_section("Dungeon Items");
|
|
|
|
rando_config_group(leftPane, rightPane, "Small Keys");
|
|
rando_config_group(leftPane, rightPane, "Big Keys");
|
|
rando_config_group(leftPane, rightPane, "Maps and Compasses");
|
|
rando_config_group(leftPane, rightPane, "Hyrule Castle Big Key Requirements",
|
|
[](const std::string& value, Pane& pane) {
|
|
return rando_add_optional_setting(value, "Hyrule Castle Big Key", pane);
|
|
});
|
|
|
|
rando_config_toggle(leftPane, rightPane, "Dungeon Rewards Can Be Anywhere");
|
|
rando_config_toggle(leftPane, rightPane, "No Small Keys on Bosses");
|
|
rando_config_toggle(leftPane, rightPane, "Unrequired Dungeons Are Barren");
|
|
|
|
leftPane.add_section("Timesavers");
|
|
|
|
rando_config_toggle(leftPane, rightPane, "Skip Prologue");
|
|
rando_config_toggle(leftPane, rightPane, "Faron Twilight Cleared");
|
|
rando_config_toggle(leftPane, rightPane, "Eldin Twilight Cleared");
|
|
rando_config_toggle(leftPane, rightPane, "Lanayru Twilight Cleared");
|
|
rando_config_toggle(leftPane, rightPane, "Skip Midna's Desparate Hour");
|
|
rando_config_toggle(leftPane, rightPane, "Skip Minor Cutscenes");
|
|
rando_config_toggle(leftPane, rightPane, "Skip Major Cutscenes");
|
|
rando_config_toggle(leftPane, rightPane, "Unlock Map Regions");
|
|
rando_config_toggle(leftPane, rightPane, "Open Door of Time");
|
|
rando_config_toggle(leftPane, rightPane, "Active Goron Mines Magnets");
|
|
rando_config_toggle(leftPane, rightPane, "Lower Hyrule Castle Chandelier");
|
|
rando_config_toggle(leftPane, rightPane, "Skip Bridge Donation");
|
|
|
|
leftPane.add_section("Additional Settings");
|
|
|
|
rando_config_group(leftPane, rightPane, "Starting Form");
|
|
rando_config_toggle(leftPane, rightPane, "Increase Wallet Capacity");
|
|
rando_config_toggle(leftPane, rightPane, "Shops Display The Replaced Item");
|
|
rando_config_toggle(leftPane, rightPane, "Bonks Do Damage");
|
|
rando_config_group(leftPane, rightPane, "Trap Item Frequency");
|
|
rando_config_toggle(leftPane, rightPane, "Starting Time of Day");
|
|
|
|
leftPane.add_section("Dungeon Entrance Settings");
|
|
|
|
rando_config_toggle(leftPane, rightPane, "Lakebed Does Not Require Water Bombs");
|
|
rando_config_toggle(leftPane, rightPane, "Arbiters Does Not Require Bulblin Camp");
|
|
rando_config_toggle(leftPane, rightPane, "Snowpeak Does Not Require Reekfish Scent");
|
|
rando_config_toggle(leftPane, rightPane, "Sacred Grove Does Not Require Skull Kid");
|
|
rando_config_toggle(leftPane, rightPane, "City Does Not Require Filled Skybook");
|
|
rando_config_group(leftPane, rightPane, "Goron Mines Entrance");
|
|
rando_config_group(leftPane, rightPane, "Temple of Time Sword Requirement");
|
|
rando_config_toggle(leftPane, rightPane, "Randomize Starting Spawn");
|
|
rando_config_group(leftPane, rightPane, "Randomize Dungeon Entrances");
|
|
rando_config_toggle(leftPane, rightPane, "Randomize Boss Entrances");
|
|
rando_config_toggle(leftPane, rightPane, "Randomize Grotto Entrances");
|
|
rando_config_toggle(leftPane, rightPane, "Randomize Cave Entrances");
|
|
rando_config_toggle(leftPane, rightPane, "Randomize Interior Entrances");
|
|
rando_config_toggle(leftPane, rightPane, "Randomize Overworld Entrances");
|
|
rando_config_toggle(leftPane, rightPane, "Decouple Double Door Entrances");
|
|
rando_config_toggle(leftPane, rightPane, "Decouple Entrances");
|
|
|
|
leftPane.add_section("Tricks");
|
|
|
|
rando_config_toggle(leftPane, rightPane, "Back Slice as Sword");
|
|
rando_config_toggle(leftPane, rightPane, "Ball and Chain Webs");
|
|
rando_config_toggle(leftPane, rightPane, "Logic Transform Anywhere");
|
|
});
|
|
|
|
if (randomizer_IsActive()) {
|
|
add_tab("In-Game", [this](Rml::Element* content) {
|
|
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
|
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
|
|
|
leftPane.add_section("General");
|
|
|
|
leftPane.register_control(leftPane.add_button("Warp to Start").on_pressed([] {
|
|
mDoAud_seStartMenu(kSoundClick);
|
|
auto& locData = randomizer_GetContext().mStartLocation;
|
|
dComIfGp_setNextStage(locData.mapName.c_str(), locData.pointNo, locData.roomNo, locData.mapLayer);
|
|
}), rightPane, [](Pane& pane) {
|
|
pane.clear();
|
|
pane.add_rml("Respawns the player at their appropriate starting location.");
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
void RandomizerWindow::update() {
|
|
Window::update();
|
|
|
|
if (m_genSeedModal && !generatingSeed) {
|
|
m_genSeedModal->pop();
|
|
m_genSeedModal = nullptr;
|
|
}
|
|
}
|
|
|
|
std::filesystem::path GetRandomizerSettingsPath() {
|
|
return data::configured_data_path() / "randomizer" / "settings.yaml";
|
|
}
|
|
|
|
std::filesystem::path GetRandomizerPreferencesPath() {
|
|
return data::configured_data_path() / "randomizer" / "preferences.yaml";
|
|
}
|
|
|
|
randomizer::seedgen::config::Config& GetRandomizerConfig() {
|
|
static randomizer::seedgen::config::Config s_config{GetRandomizerSettingsPath(),
|
|
GetRandomizerPreferencesPath()};
|
|
return s_config;
|
|
}
|
|
} |