From 4d8fcfa551f546aa1b8aca074f7f0899f36bced2 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Sat, 20 Jun 2026 07:57:49 -0700 Subject: [PATCH] add permalink sharing --- files.cmake | 1 + .../generator/logic/entrance_shuffle.cpp | 13 ++ .../generator/logic/entrance_shuffle.hpp | 1 + .../randomizer/generator/logic/item_pool.cpp | 17 ++ .../randomizer/generator/logic/item_pool.hpp | 2 + .../randomizer/generator/logic/location.cpp | 13 ++ .../randomizer/generator/logic/location.hpp | 6 + .../generator/logic/spoiler_log.cpp | 4 +- src/dusk/randomizer/generator/randomizer.cpp | 2 + .../randomizer/generator/seedgen/config.cpp | 192 ++++++++++++++++++ .../randomizer/generator/seedgen/config.hpp | 34 +--- .../generator/seedgen/packed_bits.hpp | 4 + .../randomizer/generator/seedgen/settings.cpp | 76 +++++-- .../randomizer/generator/seedgen/settings.hpp | 42 ++-- .../randomizer/generator/utility/base64pp.hpp | 28 +++ src/dusk/ui/rando_config.cpp | 90 +++++++- src/dusk/ui/rando_config.hpp | 2 + 17 files changed, 449 insertions(+), 78 deletions(-) create mode 100644 src/dusk/randomizer/generator/utility/base64pp.hpp diff --git a/files.cmake b/files.cmake index 4b9e2df5e0..3a4ead08fb 100644 --- a/files.cmake +++ b/files.cmake @@ -1610,6 +1610,7 @@ set(DUSK_FILES src/dusk/randomizer/generator/seedgen/settings.hpp src/dusk/randomizer/generator/test/test.cpp src/dusk/randomizer/generator/test/test.hpp + src/dusk/randomizer/generator/utility/base64pp.hpp src/dusk/randomizer/generator/utility/color.cpp src/dusk/randomizer/generator/utility/color.hpp src/dusk/randomizer/generator/utility/crc32.hpp diff --git a/src/dusk/randomizer/generator/logic/entrance_shuffle.cpp b/src/dusk/randomizer/generator/logic/entrance_shuffle.cpp index 68605ab208..a6e10a398b 100644 --- a/src/dusk/randomizer/generator/logic/entrance_shuffle.cpp +++ b/src/dusk/randomizer/generator/logic/entrance_shuffle.cpp @@ -783,4 +783,17 @@ namespace randomizer::logic::entrance_shuffle } return reverseEntrances; } + + const std::set& GetPossibleMixedPoolTypes() { + static std::set possibleMixedPoolTypes = { + TypeToStr(DUNGEON), + TypeToStr(BOSS), + TypeToStr(GROTTO), + TypeToStr(INTERIOR), + TypeToStr(CAVE), + TypeToStr(OVERWORLD), + }; + + return possibleMixedPoolTypes; + } } // namespace randomizer::logic::entrance_shuffle diff --git a/src/dusk/randomizer/generator/logic/entrance_shuffle.hpp b/src/dusk/randomizer/generator/logic/entrance_shuffle.hpp index 85e6dcd8e0..88c4939672 100644 --- a/src/dusk/randomizer/generator/logic/entrance_shuffle.hpp +++ b/src/dusk/randomizer/generator/logic/entrance_shuffle.hpp @@ -39,6 +39,7 @@ namespace randomizer::logic::entrance_shuffle void SetShuffledEntrances(entrance::EntrancePools& entrancePools); entrance::EntrancePool GetReverseEntrances(const entrance::EntrancePool& entrances); + const std::set& GetPossibleMixedPoolTypes(); class EntranceShuffleError: public std::runtime_error { diff --git a/src/dusk/randomizer/generator/logic/item_pool.cpp b/src/dusk/randomizer/generator/logic/item_pool.cpp index 664ddcf4da..9058ab2132 100644 --- a/src/dusk/randomizer/generator/logic/item_pool.cpp +++ b/src/dusk/randomizer/generator/logic/item_pool.cpp @@ -451,4 +451,21 @@ namespace randomizer::logic::item_pool return completeItemPool; } + + const std::map& GetValidStartingInventoryItems() { + static std::map validStartingInventoryItems{}; + if (validStartingInventoryItems.empty()) { + validStartingInventoryItems = minimalItemPool; + for (const auto& [item, count] : standardItemPool) { + validStartingInventoryItems[item] += count; + } + + // Remove junk + validStartingInventoryItems.erase("Purple Rupee Links House"); + validStartingInventoryItems.erase("Green Rupee"); + validStartingInventoryItems.erase("Orange Rupee"); + validStartingInventoryItems.erase("Silver Rupee"); + } + return validStartingInventoryItems; + } } // namespace randomizer::logic::item_pool diff --git a/src/dusk/randomizer/generator/logic/item_pool.hpp b/src/dusk/randomizer/generator/logic/item_pool.hpp index 2a9b7aa3fb..178693c91f 100644 --- a/src/dusk/randomizer/generator/logic/item_pool.hpp +++ b/src/dusk/randomizer/generator/logic/item_pool.hpp @@ -39,4 +39,6 @@ namespace randomizer::logic::item_pool std::map GetInitialJunkPool(); ItemPool GetCompleteItemPool(const world::WorldPool& worlds); + + const std::map& GetValidStartingInventoryItems(); } // namespace randomizer::logic::item_pool diff --git a/src/dusk/randomizer/generator/logic/location.cpp b/src/dusk/randomizer/generator/logic/location.cpp index d0edc704b3..bed3de3dfe 100644 --- a/src/dusk/randomizer/generator/logic/location.cpp +++ b/src/dusk/randomizer/generator/logic/location.cpp @@ -148,4 +148,17 @@ namespace randomizer::logic::location { this->_registeredLocationCategories = registeredLocationCategories; } + + const std::set& GetAllRandomizerLocationNames() { + static std::set locationNames{}; + + if (locationNames.empty()) { + auto locationDataTree = LOAD_EMBED_YAML(RANDO_DATA_PATH "locations.yaml"); + for (const auto& locationNode : locationDataTree) { + locationNames.insert(locationNode["Name"].as()); + } + } + + return locationNames; + } } // namespace randomizer::logic::location diff --git a/src/dusk/randomizer/generator/logic/location.hpp b/src/dusk/randomizer/generator/logic/location.hpp index 5e92c53342..62f469d622 100644 --- a/src/dusk/randomizer/generator/logic/location.hpp +++ b/src/dusk/randomizer/generator/logic/location.hpp @@ -110,4 +110,10 @@ namespace randomizer::logic::location }; using LocationPool = std::vector; + + /** + * + * @return A set of all randomizer location names + */ + const std::set& GetAllRandomizerLocationNames(); } // namespace randomizer::logic::location diff --git a/src/dusk/randomizer/generator/logic/spoiler_log.cpp b/src/dusk/randomizer/generator/logic/spoiler_log.cpp index 92b7daf067..b5b470358a 100644 --- a/src/dusk/randomizer/generator/logic/spoiler_log.cpp +++ b/src/dusk/randomizer/generator/logic/spoiler_log.cpp @@ -35,9 +35,7 @@ namespace randomizer::logic::spoiler_log { log << "Dusklight Version: " << DUSK_WC_DESCRIBE << std::endl; log << "Seed: " << randomizer->GetConfig().GetSeed() << std::endl; - - // TODO: Setting string - + log << "Permalink: " << randomizer->GetConfig().GetPermalink() << std::endl; log << "Hash: " << randomizer->GetConfig().GetHash() << std::endl; } diff --git a/src/dusk/randomizer/generator/randomizer.cpp b/src/dusk/randomizer/generator/randomizer.cpp index ad2230cb25..f0cb3c834a 100644 --- a/src/dusk/randomizer/generator/randomizer.cpp +++ b/src/dusk/randomizer/generator/randomizer.cpp @@ -84,6 +84,8 @@ namespace randomizer { utility::time::ScopedTimer<"Seed generation took ", std::chrono::milliseconds> timer; this->_config.LoadFromFile(GetConfigPath(), GetPrefPath()); + // Set permalink now so that resolving random settings doesn't change it + this->_config.SetPermalink(this->_config.GetPermalink()); utility::platform::Log(std::string("Seed: ") + this->_config.GetSeed()); diff --git a/src/dusk/randomizer/generator/seedgen/config.cpp b/src/dusk/randomizer/generator/seedgen/config.cpp index d972cfc2db..83db53013e 100644 --- a/src/dusk/randomizer/generator/seedgen/config.cpp +++ b/src/dusk/randomizer/generator/seedgen/config.cpp @@ -1,13 +1,18 @@ #include "config.hpp" +#include "packed_bits.hpp" #include "seed.hpp" +#include "../utility/base64pp.hpp" #include "../utility/crc32.hpp" #include "../utility/log.hpp" #include "../utility/platform.hpp" #include "../utility/random.hpp" #include "../utility/yaml.hpp" +#include "../logic/entrance_shuffle.hpp" +#include #include +#include // Fields which aren't part of settings_list.yaml constexpr std::string_view SEED = "Seed"; @@ -362,6 +367,193 @@ namespace randomizer::seedgen::config return this->_hash; } + std::string Config::GetPermalink() { + // If a permalink was set, return that instead + if (!this->_permalink.empty()) { + return this->_permalink; + } + + std::string permalink{}; + + permalink += DUSK_WC_DESCRIBE; + permalink += '\0'; + permalink += std::to_string(settings::GetSettingInfoHash()); + permalink += '\0'; + permalink += this->_seed; + permalink += '\0'; + + // Pack the settings up + PackedBitsWriter bitsWriter{}; + // Regular Settings + for (const auto& [settingName, setting] : GetSettings().GetMap()) { + if (setting.GetInfo()->GetType() != settings::Type::STANDARD) { + continue; + } + + auto optionIndex = setting.GetCurrentOptionIndex(); + auto bitLength = setting.GetInfo()->GetOptionsBitLength(); + bitsWriter.write(optionIndex, bitLength); + } + // Starting Items + const auto& startingInventory = GetSettings().GetStartingInventory(); + for (const auto& [itemName, maxCount] : logic::item_pool::GetValidStartingInventoryItems()) { + int count = 0; + if (startingInventory.contains(itemName)) { + count = startingInventory.at(itemName); + } + + int numBits = std::bit_width(static_cast(maxCount)); + bitsWriter.write(count, numBits); + } + // Excluded Locations + for (const auto& locationName : logic::location::GetAllRandomizerLocationNames()) { + if (GetSettings().GetExcludedLocations().contains(locationName)) { + bitsWriter.write(1, 1); + } else { + bitsWriter.write(0, 1); + } + } + // Mixed Entrance Pools + const auto& mixedEntrancePools = GetSettings().GetMixedEntrancePools(); + const auto& possibleMixedPoolTypes = logic::entrance_shuffle::GetPossibleMixedPoolTypes(); + for (const auto& entranceType : possibleMixedPoolTypes) { + uint32_t poolIndex = 0; + uint32_t counter = 0; + for (const auto& pool : mixedEntrancePools) { + counter += 1; + if (utility::container::ElementInContainer(pool, entranceType)) { + poolIndex = counter; + break; + } + } + bitsWriter.write(poolIndex, std::bit_width(possibleMixedPoolTypes.size())); + } + + bitsWriter.flush(); + for (auto byte : bitsWriter.bytes) { + permalink += byte; + } + permalink = b64_encode(permalink); + + return permalink; + } + + std::optional Config::LoadFromPermalink(std::string b64permalink) { + + // Strip trailing spaces + std::erase_if(b64permalink, [](unsigned char ch){ return std::isspace(ch); }); + + std::string permalink = b64_decode(b64permalink); + // Empty string gets returned if there was an error + if (permalink.empty()) { + return "Pasted permalink is invalid and could not be decoded. (You likely miscopied it.)"; + } + + // Split the string into 4 parts along the null terminator delimiter + // 1st part - Version string + // 2nd part - setting info hash + // 3rd part - seed string + // 4th part - packed bits representing settings + std::vector permaParts = {}; + constexpr char delimiter = '\0'; + size_t pos = permalink.find(delimiter); + while (pos != std::string::npos) { + if (permaParts.size() != 3) { + permaParts.push_back(permalink.substr(0, pos)); + permalink.erase(0, pos + 1); + } + else { + permaParts.push_back(permalink); + break; + } + + pos = permalink.find(delimiter); + } + + if (permaParts.size() != 4) { + return "Pasted permalink does not have the expected number of parts."; + } + + const auto& permaVersion = permaParts[0]; + const auto& permaSettingsInfoHash = permaParts[1]; + const auto& permaSeed = permaParts[2]; + const auto& permaPackedSettings = permaParts[3]; + + if (permaSettingsInfoHash != std::to_string(settings::GetSettingInfoHash())) { + return fmt::format("Pasted permalink was generated with an incompatible Dusklight version.\n" + "Your version: {}\nPermalink version: {}", DUSK_WC_DESCRIBE, permaVersion); + } + + const std::vector bytes(permaPackedSettings.begin(), permaPackedSettings.end()); + PackedBitsReader bitsReader{bytes}; + Config newConfig{}; + + for (auto& [settingName, setting] : newConfig.GetSettings().GetMap()) { + if (setting.GetInfo()->GetType() != settings::Type::STANDARD) { + continue; + } + + auto bitLength = setting.GetInfo()->GetOptionsBitLength(); + auto optionIndex = bitsReader.read(bitLength); + setting.SetCurrentOption(optionIndex); + } + // Starting Items + auto& startingInventory = newConfig.GetSettings().GetModifiableStartingInventory(); + for (const auto& [itemName, maxCount] : logic::item_pool::GetValidStartingInventoryItems()) { + int count = 0; + int numBits = std::bit_width(static_cast(maxCount)); + count = bitsReader.read(numBits); + + if (count > 0) { + startingInventory[itemName] = count; + } + } + // Excluded Locations + auto& excludedLocations = newConfig.GetSettings().GetModifiableExcludedLocations(); + for (const auto& locationName : logic::location::GetAllRandomizerLocationNames()) { + if (bitsReader.read(1) == 1) { + excludedLocations.insert(locationName); + } + } + + // Mixed Entrance Pools + auto& mixedEntrancePools = newConfig.GetSettings().GetModifiableMixedEntrancePools(); + const auto& possibleMixedPoolTypes = logic::entrance_shuffle::GetPossibleMixedPoolTypes(); + for (const auto& entranceType : possibleMixedPoolTypes) { + auto poolIndex = bitsReader.read(std::bit_width(possibleMixedPoolTypes.size())); + if (poolIndex == 0) { + continue; + } + poolIndex -= 1; + if (poolIndex < possibleMixedPoolTypes.size()) { + while (poolIndex >= mixedEntrancePools.size()) { + mixedEntrancePools.push_back({}); + } + auto& pool = *std::next(mixedEntrancePools.begin(), poolIndex); + pool.push_back(entranceType); + } + } + + if (!bitsReader.reached_last_byte()) { + return "Pasted permalink is incorrect length. (You likely miscopied it.)"; + } + + // Once we've gotten all the info, copy it over to this config + this->SetSeed(permaSeed); + for (auto& settings : this->_settingsList) { + for (auto& [settingName, setting] : settings.GetMap()) { + if (setting.GetInfo()->GetType() == settings::Type::STANDARD) { + setting.SetCurrentOption(newConfig.GetSettings().GetMap().at(settingName).GetCurrentOptionIndex()); + } + } + settings.GetModifiableExcludedLocations() = newConfig.GetSettings().GetExcludedLocations(); + settings.GetModifiableStartingInventory() = newConfig.GetSettings().GetStartingInventory(); + settings.GetModifiableMixedEntrancePools() = newConfig.GetSettings().GetMixedEntrancePools(); + } + + return std::nullopt; + } + int SeedRNG(Config& config, bool resolveNonStandardRandom /* = false */, bool ignoreInvalidPlandomizer /* = true */) diff --git a/src/dusk/randomizer/generator/seedgen/config.hpp b/src/dusk/randomizer/generator/seedgen/config.hpp index 549ef63e34..888075d457 100644 --- a/src/dusk/randomizer/generator/seedgen/config.hpp +++ b/src/dusk/randomizer/generator/seedgen/config.hpp @@ -13,34 +13,6 @@ namespace YAML namespace randomizer::seedgen::config { - enum struct [[nodiscard]] ConfigError - { - NONE = 0, - COULD_NOT_OPEN, - MISSING_KEY, - DIFFERENT_FILE_VERSION, - DIFFERENT_RANDO_VERSION, - BAD_PERMALINK, - INVALID_VALUE, - MODEL_ERROR, - UNKNOWN, - COUNT - }; - - enum struct [[nodiscard]] PermalinkError - { - NONE = 0, - EMPTY, - BAD_ENCODING, - MISSING_PARTS, - INVALID_VERSION, - INCORRECT_LENGTH, - COULD_NOT_READ, - UNHANDLED_OPTION, - COULD_NOT_LOAD_LOCATIONS, - UNKNOWN, - COUNT - }; class Config { @@ -68,8 +40,9 @@ namespace randomizer::seedgen::config void WritePreferencesToFile(const fspath& preferencesPath); void WriteToFile(const fspath& filePath, const fspath& preferencesPath); - // PermalinkError loadPermalink(std::string b64permalink); - // std::string getPermalink(const bool& internal = false) const; + std::optional LoadFromPermalink(std::string b64permalink); + std::string GetPermalink(); + void SetPermalink(const std::string& newPermalink) { this->_permalink = newPermalink; } /** * @brief Returns the hash for the config. @@ -85,6 +58,7 @@ namespace randomizer::seedgen::config std::string _seed; std::string _hash; + std::string _permalink; std::list _settingsList; bool _isUsingPlandomizer = false; bool _isGeneratingSpoilerLog = true; diff --git a/src/dusk/randomizer/generator/seedgen/packed_bits.hpp b/src/dusk/randomizer/generator/seedgen/packed_bits.hpp index 9720709d01..d69d8c0fae 100644 --- a/src/dusk/randomizer/generator/seedgen/packed_bits.hpp +++ b/src/dusk/randomizer/generator/seedgen/packed_bits.hpp @@ -102,4 +102,8 @@ class PackedBitsReader return value; } + + bool reached_last_byte() const { + return current_byte_index == bytes.size() - 1; + } }; diff --git a/src/dusk/randomizer/generator/seedgen/settings.cpp b/src/dusk/randomizer/generator/seedgen/settings.cpp index 8a321062d5..56482a6323 100644 --- a/src/dusk/randomizer/generator/seedgen/settings.cpp +++ b/src/dusk/randomizer/generator/seedgen/settings.cpp @@ -1,11 +1,15 @@ #include "settings.hpp" +#include "../utility/crc32.hpp" +#include "../utility/endian.hpp" #include "../utility/log.hpp" #include "../utility/container.hpp" #include "../utility/file.hpp" #include "../utility/random.hpp" #include "../utility/string.hpp" #include "../utility/yaml.hpp" +#include "../logic/location.hpp" +#include "../logic/entrance_shuffle.hpp" #include #include @@ -26,18 +30,18 @@ namespace randomizer::seedgen::settings return types.at(str); } - SettingInfo::SettingInfo(const int& id, + SettingInfo::SettingInfo(int id, const std::string& name, - const Type& type, + Type type, const std::vector& options, const std::vector& descriptions, - const int& defaultOptionIndex, - const bool& hasRandomOption, - const int& randomOptionIndex, - const int& randomLow, - const int& randomHigh, - const bool& trackerImportant, - const bool& needInGame): + int defaultOptionIndex, + bool hasRandomOption, + int randomOptionIndex, + int randomLow, + int randomHigh, + bool trackerImportant, + bool needInGame): _id(id), _name(name), _type(type), @@ -71,6 +75,9 @@ namespace randomizer::seedgen::settings this->_optionsAreNumbers = std::ranges::all_of(options, [](const std::string& option) { return !option.empty() && std::ranges::all_of(option, ::isdigit); }); + + // Set options bitlength + this->_optionsBitLength = std::bit_width(this->_options.size()); } std::string SettingInfo::GetDefaultOption() const @@ -80,7 +87,7 @@ namespace randomizer::seedgen::settings int SettingInfo::GetIndexOfOption(const std::string& option) const { - return randomizer::utility::container::GetIndex(this->_options, option); + return utility::container::GetIndex(this->_options, option); } std::string SettingInfo::GetRandomOption() const @@ -93,7 +100,7 @@ namespace randomizer::seedgen::settings this->_currentOptionIndex = info->GetIndexOfOption(option); } - void Setting::SetCurrentOption(const int& newOptionIndex) + void Setting::SetCurrentOption(int newOptionIndex) { if (newOptionIndex >= this->GetInfo()->GetOptions().size()) { @@ -122,7 +129,7 @@ namespace randomizer::seedgen::settings int Setting::GetCurrentOptionAsNumber() const { try { return std::stoi(this->GetCurrentOption()); - } catch (...) { + } catch (const std::invalid_argument&) { throw std::runtime_error("Option \"" + GetCurrentOption() + "\" for setting \"" + this->GetInfo()->GetName() + "\" cannot be turned into a number"); } @@ -134,7 +141,7 @@ namespace randomizer::seedgen::settings { this->_isUsingRandomOption = true; auto randomOption = - randomizer::utility::random::Random(this->GetInfo()->GetRandomLow(), this->GetInfo()->GetRandomHigh()); + utility::random::Random(this->GetInfo()->GetRandomLow(), this->GetInfo()->GetRandomHigh()); this->SetCurrentOption(randomOption); LOG_TO_DEBUG("Chose \"" + this->GetInfo()->GetOptions()[randomOption] + " as random option for setting \"" + this->GetInfo()->GetName()); @@ -300,7 +307,7 @@ namespace randomizer::seedgen::settings } // Calculate default option index - auto defaultOptionIndex = randomizer::utility::container::GetIndex(options, defaultOption); + auto defaultOptionIndex = utility::container::GetIndex(options, defaultOption); if (defaultOptionIndex == -1) { throw std::runtime_error(std::string("Default Option \"") + defaultOption + "\" is not defined for setting \"" + @@ -328,17 +335,17 @@ namespace randomizer::seedgen::settings if (settingNode["Random Low"]) { auto randomLowStr = settingNode["Random Low"].as(); - randomLow = randomizer::utility::container::GetIndex(options, randomLowStr); + randomLow = utility::container::GetIndex(options, randomLowStr); if (randomLow == -1) { throw std::runtime_error(std::string("Random Low Option \"") + randomLowStr + "\" is not defined for setting \"" + name + "\""); } } - if (settingNode["Random high"]) + if (settingNode["Random High"]) { auto randomHighStr = settingNode["Random High"].as(); - randomHigh = randomizer::utility::container::GetIndex(options, randomHighStr); + randomHigh = utility::container::GetIndex(options, randomHighStr); if (randomHigh == -1) { throw std::runtime_error(std::string("Random High Option \"") + randomHighStr + @@ -347,13 +354,13 @@ namespace randomizer::seedgen::settings } // Generate the random option if it's not already there - if (hasRandomOption && randomizer::utility::container::GetIndex(options, randomAlias) != -1) + if (hasRandomOption && utility::container::GetIndex(options, randomAlias) != -1) { options.push_back(randomAlias); descriptions.push_back("A random option will be chosen"); } - int randomOptionIndex = randomizer::utility::container::GetIndex(options, randomAlias); + int randomOptionIndex = utility::container::GetIndex(options, randomAlias); // Insert the data for the setting auto info = std::make_unique(settingIdCounter++, @@ -374,4 +381,35 @@ namespace randomizer::seedgen::settings return std::move(settingInfoMap); } + uint32_t GetSettingInfoHash() { + static uint32_t settingsInfoHash = 0; + if (settingsInfoHash == 0) { + auto allSettingInfo = GetAllSettingsInfo(); + for (const auto& [settingName, settingInfo] : *allSettingInfo) { + settingsInfoHash = utility::crc32(settingName.data(), settingName.length(), settingsInfoHash); + for (const auto& optionName : settingInfo->GetOptions()) { + settingsInfoHash = utility::crc32(optionName.data(), optionName.length(), settingsInfoHash); + } + } + + for (const auto& [itemName, maxRef] : logic::item_pool::GetValidStartingInventoryItems()) { + int maxCount = maxRef; + settingsInfoHash = utility::crc32(itemName.data(), itemName.length(), settingsInfoHash); + if constexpr (std::endian::native == std::endian::big) { + maxCount = Utility::Endian::byteswap(maxCount); + } + settingsInfoHash = utility::crc32(&maxCount, sizeof(maxCount), settingsInfoHash); + } + + for (const auto& locationName : logic::location::GetAllRandomizerLocationNames()) { + settingsInfoHash = utility::crc32(locationName.data(), locationName.length(), settingsInfoHash); + } + + for (const auto& entranceType : logic::entrance_shuffle::GetPossibleMixedPoolTypes()) { + settingsInfoHash = utility::crc32(entranceType.data(), entranceType.length(), settingsInfoHash); + } + } + return settingsInfoHash; + } + }; // namespace randomizer::seedgen::settings diff --git a/src/dusk/randomizer/generator/seedgen/settings.hpp b/src/dusk/randomizer/generator/seedgen/settings.hpp index f7a390786d..66c3b68fdb 100644 --- a/src/dusk/randomizer/generator/seedgen/settings.hpp +++ b/src/dusk/randomizer/generator/seedgen/settings.hpp @@ -14,7 +14,7 @@ namespace randomizer::seedgen::settings { class SettingInfo; - using SettingInfoMap_t = std::unordered_map>; + using SettingInfoMap_t = std::map>; /** * @brief Enum for different types of settings. @@ -48,18 +48,18 @@ namespace randomizer::seedgen::settings class SettingInfo { public: - SettingInfo(const int& id, + SettingInfo(int id, const std::string& name, - const Type& type, + Type type, const std::vector& options, const std::vector& descriptions, - const int& defaultOptionIndex, - const bool& hasRandomOption, - const int& randomOptionIndex, - const int& randomLow, - const int& randomHigh, - const bool& trackerImportant, - const bool& needInGame); + int defaultOptionIndex, + bool hasRandomOption, + int randomOptionIndex, + int randomLow, + int randomHigh, + bool trackerImportant, + bool needInGame); int GetID() const { return this->_id; } @@ -76,12 +76,12 @@ namespace randomizer::seedgen::settings /** * @brief Returns a vector of strings of the setting's available options. */ - std::vector GetOptions() const { return this->_options; } + const std::vector& GetOptions() const { return this->_options; } /** * @brief Returns a vector of strings of the setting's options' descriptions. */ - std::vector GetDescriptions() const { return this->_descriptions; } + const std::vector& GetDescriptions() const { return this->_descriptions; } /** * @brief Returns the index of the default option in the options vector for the setting. @@ -101,6 +101,7 @@ namespace randomizer::seedgen::settings bool TrackerImportant() const { return this->_trackerImportant; } bool NeedInGame() const { return this->_needInGame; } bool OptionsAreNumbers() const { return this->_optionsAreNumbers; } + int GetOptionsBitLength() const {return this->_optionsBitLength; } private: int _id = -1; @@ -116,6 +117,7 @@ namespace randomizer::seedgen::settings bool _trackerImportant = false; // Whether or not this setting can affect trackers bool _needInGame = false; // Whether or not we need to read this setting during gameplay bool _optionsAreNumbers = false;// Whether this setting's options are all numbers + int _optionsBitLength = 0; // Variables that hold the setting's name and options when being checked // in a logical requirement string. @@ -135,14 +137,13 @@ namespace randomizer::seedgen::settings Setting(SettingInfo* info, const std::string& option); void SetCurrentOption(const std::string& newOption); - void SetCurrentOption(const int& newOptionIndex); + void SetCurrentOption(int newOptionIndex); std::string GetCurrentOption() const; int GetCurrentOptionAsNumber() const; - const int& GetCurrentOptionIndex() const { return this->_currentOptionIndex; } - const bool& IsUsingRandomOption() const { return this->_isUsingRandomOption; } + int GetCurrentOptionIndex() const { return this->_currentOptionIndex; } + bool IsUsingRandomOption() const { return this->_isUsingRandomOption; } SettingInfo* GetInfo() const { return this->_info; } const std::string& GetCustomOption() const { return this->_customOption; } - // void SetCustomOption(const std::string& newCustomOption); void ResolveIfRandom(); bool operator==(const char* optionName) const; @@ -158,7 +159,7 @@ namespace randomizer::seedgen::settings // Check to make sure all listed options exist for (const auto& optionName : {optionNames...}) { - if (!randomizer::utility::container::ElementInContainer(this->GetInfo()->GetOptions(), optionName)) + if (!utility::container::ElementInContainer(this->GetInfo()->GetOptions(), optionName)) { throw std::runtime_error("\"" + std::string(optionName) + "\" is not a known option for setting \"" + this->GetInfo()->GetName() + "\""); @@ -222,4 +223,11 @@ namespace randomizer::seedgen::settings */ std::unique_ptr LoadAllSettingsInfo(); + /** + * @brief Generates a hash based on the current settings info. This can be used + * to determine if settings between different permalinks are valid + * @return the hash for the current settings info + */ + uint32_t GetSettingInfoHash(); + }; // namespace randomizer::seedgen::settings diff --git a/src/dusk/randomizer/generator/utility/base64pp.hpp b/src/dusk/randomizer/generator/utility/base64pp.hpp new file mode 100644 index 0000000000..a74a7b2d5a --- /dev/null +++ b/src/dusk/randomizer/generator/utility/base64pp.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "base64pp/base64pp.h" + +// extra wrappers for convenience +inline std::string b64_encode(const std::string& asciiStr) { + std::vector permalinkBytes = {}; + for(const char& ch : asciiStr) + { + permalinkBytes.push_back(ch); + } + + std::span span(permalinkBytes.begin(), permalinkBytes.end()); + + return base64pp::encode(span); +} + +inline std::string b64_decode(const std::string& b64Str) { + const auto optional = base64pp::decode(b64Str); + if(!optional.has_value()) + { + // Return an empty string if there was an error + return ""; + } + + const auto& bytes = optional.value(); + return {bytes.begin(), bytes.end()}; +} \ No newline at end of file diff --git a/src/dusk/ui/rando_config.cpp b/src/dusk/ui/rando_config.cpp index 81d241991c..b512e627dc 100644 --- a/src/dusk/ui/rando_config.cpp +++ b/src/dusk/ui/rando_config.cpp @@ -1,8 +1,5 @@ #include "rando_config.hpp" -#include -#include - #include "bool_button.hpp" #include "dusk/config.hpp" #include "dusk/data.hpp" @@ -15,8 +12,11 @@ #include "pane.hpp" #include "rando_seed_generation.hpp" #include "string_button.hpp" - #include "d/d_file_select.h" +#include "SDL3/SDL_clipboard.h" + +#include +#include namespace dusk::ui { struct ConfigBoolProps { @@ -823,6 +823,29 @@ RandomizerWindow::RandomizerWindow(dFile_select_c* fileSelect /*= nullptr*/) : m rightPane, delete_seed_callback ); + leftPane.add_section("Permalink"); + leftPane.register_control( + leftPane.add_button("Copy Permalink") + .on_pressed([] { + CopyPermalinkToClipboard(); + }), + rightPane, [](Pane& pane) { + pane.clear(); + pane.add_text("Copy your current settings permalink to share with others."); + auto text = pane.add_text(fmt::format("Current Permalink: {}", GetRandomizerConfig().GetPermalink())); + text->SetProperty("word-break", "break-word"); + }); + + leftPane.register_control( + leftPane.add_button("Paste Permalink") + .on_pressed([] { + PastePermalinkFromClipboard(); + }), + rightPane, [](Pane& pane) { + pane.clear(); + pane.add_text("Paste in a permalink from your clipboard. This will overwrite your current settings."); + }); + leftPane.add_section("Presets"); leftPane.register_control( leftPane.add_button("Save Current Settings as Preset") @@ -1250,7 +1273,7 @@ void SaveNewRandomizerPreset(const std::string& presetName, bool overwriteExisti try { GetRandomizerConfig().WriteSettingsToFile(presetFilepath); } catch (std::exception& e) { - auto modal = dynamic_cast(&push_document(std::make_unique(Modal::Props{ + push_document(std::make_unique(Modal::Props{ .title = "Error Saving Preset", .bodyRml = fmt::format("Error: {}", e.what()), .actions = { @@ -1262,7 +1285,7 @@ void SaveNewRandomizerPreset(const std::string& presetName, bool overwriteExisti } }, .icon = "error" - }))); + })); return; } @@ -1277,10 +1300,10 @@ void ApplyExistingRandomizerPreset(const std::filesystem::path& presetFilePath) // Don't overwrite the seed with the one from the preset auto seed = GetRandomizerConfig().GetSeed(); try { - GetRandomizerConfig().LoadFromFile(presetFilePath, GetRandomizerPreferencesPath(), false, false); + GetRandomizerConfig().LoadFromFile(presetFilePath, GetRandomizerPreferencesPath(), false, true); GetRandomizerConfig().SetSeed(seed); } catch (std::exception& e) { - auto modal = dynamic_cast(&push_document(std::make_unique(Modal::Props{ + push_document(std::make_unique(Modal::Props{ .title = "Error Loading Preset", .bodyRml = fmt::format("Error: {}", e.what()), .actions = { @@ -1292,7 +1315,7 @@ void ApplyExistingRandomizerPreset(const std::filesystem::path& presetFilePath) } }, .icon = "error" - }))); + })); return; } @@ -1303,6 +1326,55 @@ void ApplyExistingRandomizerPreset(const std::filesystem::path& presetFilePath) }); } +void CopyPermalinkToClipboard() { + if (SDL_SetClipboardText(GetRandomizerConfig().GetPermalink().data())) { + push_toast(Toast{ + .content = "Permalink Copied", + .duration = std::chrono::seconds(3) + }); + } else { + push_document(std::make_unique(Modal::Props{ + .title = "Permalink Error", + .bodyRml = fmt::format("Could not copy permalink to clipboard. Reason: {}", SDL_GetError()), + .actions = { + ModalAction{ + .label = "Okay", + .onPressed = [](Modal& modal) { + modal.pop(); + } + } + }, + .icon = "error" + })); + } +} + +void PastePermalinkFromClipboard() { + if (SDL_HasClipboardText()) { + std::string clipBoardText = SDL_GetClipboardText(); + auto result = GetRandomizerConfig().LoadFromPermalink(clipBoardText); + if (result.has_value()) { + auto modal = dynamic_cast(&push_document(std::make_unique(Modal::Props{ + .title = "Permalink Error", + .bodyRml = result.value(), + .actions = { + ModalAction{ + .label = "Okay", + .onPressed = [](Modal& modal) { + modal.pop(); + } + } + }, + .icon = "error" + }))); + modal->root()->SetProperty("white-space", "pre-line"); + } else { + push_toast(Toast{.content = "Applied Permalink", .duration = std::chrono::seconds(3)}); + SaveRandomizerConfig(); + } + } +} + std::filesystem::path GetRandomizerPath() { return data::configured_data_path() / "randomizer"; } diff --git a/src/dusk/ui/rando_config.hpp b/src/dusk/ui/rando_config.hpp index 817b22553e..d177452e82 100644 --- a/src/dusk/ui/rando_config.hpp +++ b/src/dusk/ui/rando_config.hpp @@ -12,6 +12,8 @@ class Pane; void SaveNewRandomizerPreset(const std::string& presetName, bool overwriteExisting = false); void ApplyExistingRandomizerPreset(const std::filesystem::path& presetFilePath); + void CopyPermalinkToClipboard(); + void PastePermalinkFromClipboard(); std::filesystem::path GetRandomizerPath(); std::filesystem::path GetRandomizerSettingsPath(); std::filesystem::path GetRandomizerPreferencesPath();