#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 // Fields which aren't part of settings_list.yaml constexpr std::string_view SEED = "Seed"; constexpr std::string_view PLANDOMIZER = "Plandomizer"; constexpr std::string_view PLANDOMIZER_PATH = "Plandomizer Path"; constexpr std::string_view STARTING_INVENTORY = "Starting Inventory"; constexpr std::string_view EXCLUDED_LOCATIONS = "Excluded Locations"; constexpr std::string_view MIXED_ENTRANCE_POOLS = "Mixed Entrance Pools"; constexpr std::string_view GENERATE_SPOILER_LOG = "Generate Spoiler Log"; namespace randomizer::seedgen::config { Config::Config() { // Create at least one player's settings this->_settingsList.push_front(settings::Settings()); } Config::Config(const fspath& settingsPath, const fspath& preferencesPath) { // Create at least one player's settings this->_settingsList.push_front(settings::Settings()); LoadFromFile(settingsPath, preferencesPath); } void Config::ResetSettingsToDefault() { for (auto& settings : this->_settingsList) { for (auto& [settingName, setting] : settings.GetMap()) { if (setting.GetInfo()->GetType() == settings::Type::STANDARD) { setting.SetCurrentOption(setting.GetInfo()->GetDefaultOption()); } } settings.GetModifiableExcludedLocations().clear(); settings.GetModifiableStartingInventory().clear(); settings.GetModifiableMixedEntrancePools().clear(); } } void Config::ResetPreferencesToDefault() { for (auto& settings : this->_settingsList) { for (auto& [settingName, setting] : settings.GetMap()) { if (setting.GetInfo()->GetType() == settings::Type::PREFERENCE) { setting.SetCurrentOption(setting.GetInfo()->GetDefaultOption()); } } this->_plandomizerPath = ""; } } void Config::LoadFromFile(const fspath& settingsPath, const fspath& preferencesPath, bool createIfNotFound /*= true*/, bool allowRewrite /*= true*/) { // Create files for settings/preferences if they don't exist if (!std::filesystem::exists(settingsPath)) { if (createIfNotFound) { WriteSettingsToFile(settingsPath); } else { throw std::runtime_error("Could not open settings file at \"" + settingsPath.generic_string() + "\""); } } if (!std::filesystem::exists(preferencesPath)) { if (createIfNotFound) { WritePreferencesToFile(preferencesPath); } else { throw std::runtime_error("Could not open preferences file at \"" + preferencesPath.generic_string() + "\""); } } auto& settings = this->_settingsList.front(); settings.GetModifiableExcludedLocations().clear(); settings.GetModifiableMixedEntrancePools().clear(); settings.GetModifiableStartingInventory().clear(); // Load settings info auto settingInfoMap = settings::GetAllSettingsInfo(); // Read in settings and preferences. If we have to change anything, // rewrite the appropriate file if allowed. bool rewriteSettings = false; auto settingsTree = LoadYAML(settingsPath); // Loop through all setting fields for (const auto& settingNode : settingsTree) { const auto& settingName = settingNode.first.as(); // Insert the setting if it's in the info map if (settingInfoMap->contains(settingName)) { auto& settingInfo = settingInfoMap->at(settingName); auto settingOption = settingNode.second.as(); // If the option doesn't exist, revert to default and rewrite later if necessary if (settingInfo->GetIndexOfOption(settingOption) == -1) { utility::platform::Log(std::string("Setting \"") + settingName + "\" has no option \"" + settingOption + "\". Reverting to default \"" + settingInfo->GetDefaultOption() + "\""); settingOption = settingInfo->GetDefaultOption(); rewriteSettings = true; } settings.GetMap().at(settingName).SetCurrentOption(settingOption); } // Special handling for starting inventory else if (settingName == STARTING_INVENTORY) { for (const auto& inventoryNode : settingNode.second) { const auto& itemName = inventoryNode.first.as(); const auto& count = inventoryNode.second.as(); settings.AddStartingItem(itemName, count); } } // Special Handling for Excluded Locations else if (settingName == EXCLUDED_LOCATIONS) { for (const auto& locationNode : settingNode.second) { const auto& locationName = locationNode.as(); settings.AddExcludedLocation(locationName); } } // Special Handling for Mixed Entrance Pools else if (settingName == MIXED_ENTRANCE_POOLS) { for (const auto& poolNode : settingNode.second) { if (!poolNode.IsSequence()) { throw std::runtime_error("Mixed Entrance Pools is not a nested sequence of strings"); } settings.AddMixedPool(poolNode.as>()); } } // Special handling for Seed else if (settingName == SEED) { const auto& seed = settingNode.second.as(); this->_seed = seed; // If seed is empty string, generate a new one if (this->_seed.empty()) { this->_seed = seed::GenerateSeed(); } } // Special handling for Plandomizer else if (settingName == PLANDOMIZER) { const auto& plandomizer = settingNode.second.as(false); this->_isUsingPlandomizer = plandomizer; } } // Loop through all preference fields bool rewritePreferences = false; auto preferencesTree = LoadYAML(preferencesPath); for (const auto& preferenceNode : preferencesTree) { const auto& preferenceName = preferenceNode.first.as(); // Insert the preference if it's in the info map if (settingInfoMap->contains(preferenceName)) { auto& preferenceInfo = settingInfoMap->at(preferenceName); auto preferenceOption = preferenceNode.second.as(); // If the option doesn't exist, revert to default and rewrite later if necessary if (preferenceInfo->GetIndexOfOption(preferenceOption) == -1) { utility::platform::Log(std::string("Preference \"") + preferenceName + " has no option \"" + preferenceOption + "\". Reverting to default \"" + preferenceInfo->GetDefaultOption() + "\""); preferenceOption = preferenceInfo->GetDefaultOption(); rewritePreferences = true; } settings.GetMap().at(preferenceName).SetCurrentOption(preferenceOption); } else if (preferenceName == PLANDOMIZER_PATH) { const auto& plandomizerPath = preferenceNode.second.as(); this->_plandomizerPath = plandomizerPath; } } // Rewrite the file(s) if any settings or preferences are missing for (auto& [settingName, settingInfo] : *settingInfoMap) { if (!settingsTree[settingName]) { utility::platform::Log(std::string("Added missing setting \"") + settingName + "\""); if (settingInfo->GetType() == settings::Type::STANDARD) { rewriteSettings = true; } else if (settingInfo->GetType() == settings::Type::PREFERENCE) { rewritePreferences = true; } } } if (!settingsTree[SEED]) { this->_seed = seed::GenerateSeed(); utility::platform::Log("Seed is missing. Generated new seed."); rewriteSettings = true; } if (!settingsTree[PLANDOMIZER] || !settingsTree[GENERATE_SPOILER_LOG] || !settingsTree[STARTING_INVENTORY] || !settingsTree[EXCLUDED_LOCATIONS] || !settingsTree[MIXED_ENTRANCE_POOLS]) { rewriteSettings = true; } if (!preferencesTree[PLANDOMIZER_PATH]) { rewritePreferences = true; } // Rewrite files if deemed necessary if (allowRewrite && rewriteSettings) { utility::platform::Log(std::string("Rewriting ") + settingsPath.generic_string()); this->WriteSettingsToFile(settingsPath); } if (allowRewrite && rewritePreferences) { utility::platform::Log(std::string("Rewriting ") + preferencesPath.generic_string()); this->WritePreferencesToFile(preferencesPath); } } YAML::Node Config::SettingsToYaml() { YAML::Node out; for (auto& settings : this->_settingsList) { out[SEED] = this->_seed; out[PLANDOMIZER] = this->_isUsingPlandomizer; out[GENERATE_SPOILER_LOG] = this->_isGeneratingSpoilerLog; // Sort settings by id to keep relevant settings close together in the settings file std::list sortedNames = {}; for (auto& [settingName, setting] : settings.GetMap()) { sortedNames.push_back(settingName); } sortedNames.sort( [&](const auto& a, const auto& b) { return settings.GetMap().at(a).GetInfo()->GetID() < settings.GetMap().at(b).GetInfo()->GetID(); }); for (const auto& settingName : sortedNames) { auto& setting = settings.GetMap().at(settingName); if (setting.GetInfo()->GetType() == settings::Type::STANDARD) { out[settingName] = setting.GetCurrentOption(); } } out[STARTING_INVENTORY] = std::map(); for (const auto& [itemName, count] : settings.GetStartingInventory()) { out[STARTING_INVENTORY][itemName] = count; } out[EXCLUDED_LOCATIONS] = std::list(); for (const auto& locationName : settings.GetExcludedLocations()) { out[EXCLUDED_LOCATIONS].push_back(locationName); } out[MIXED_ENTRANCE_POOLS] = std::list>(); int i = 0; for (const auto& pool : settings.GetMixedEntrancePools()) { out[MIXED_ENTRANCE_POOLS].push_back({}); for (const auto& type : pool) { out[MIXED_ENTRANCE_POOLS][i].push_back(type); } i += 1; } } return out; } YAML::Node Config::PreferencesToYaml() { YAML::Node out; for (auto& settings : this->_settingsList) { out[PLANDOMIZER_PATH] = this->_plandomizerPath.generic_string(); for (auto& [settingName, setting] : settings.GetMap()) { if (setting.GetInfo()->GetType() == settings::Type::PREFERENCE) { out[settingName] = setting.GetCurrentOption(); } } } return out; } void Config::WriteSettingsToFile(const fspath& settingsPath) { std::ofstream outputFile(settingsPath); if (outputFile.is_open() == false) { throw std::runtime_error("Unable to open settings file \"" + settingsPath.generic_string() + "\" for writing."); } outputFile << this->SettingsToYaml(); outputFile.close(); } void Config::WritePreferencesToFile(const fspath& preferencesPath) { std::ofstream outputFile(preferencesPath); if (outputFile.is_open() == false) { throw std::runtime_error("Unable to open preferences file \"" + preferencesPath.generic_string() + "\" for writing."); } outputFile << this->PreferencesToYaml(); outputFile.close(); } void Config::WriteToFile(const fspath& settingsPath, const fspath& preferencesPath) { WriteSettingsToFile(settingsPath); WritePreferencesToFile(preferencesPath); } std::string Config::GetHash(bool generateIfEmpty) { if (this->_hash.empty() && generateIfEmpty) { this->_hash = seed::GenerateHash(); } 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{}; // TODO: print mod version instead of dusklight version? /*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())) { // TODO: print mod version instead of dusklight version? return fmt::format("Pasted permalink was generated with an incompatible Dusklight version.\n" "Your version: {}\nPermalink version: {}", /*DUSK_WC_DESCRIBE*/ 0, 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 */) { // Seed with system time incase we have to choose random preferences during seeding auto seed = static_cast(std::random_device {}()); utility::random::RandomInit(seed); // Seed the rng using a combination of the seed and standard settings std::string hashStr = config.GetSeed(); for (auto& settings : config.GetSettingsList()) { for (auto& [settingName, setting] : settings.GetMap()) { if (setting.GetInfo()->GetType() == settings::Type::STANDARD) { hashStr += settingName + setting.GetCurrentOption(); } else if (resolveNonStandardRandom) { setting.ResolveIfRandom(); } } // Special handling for other settings for (const auto& [itemName, count] : settings.GetStartingInventory()) { hashStr += itemName + std::to_string(count); } for (const auto& locationName : settings.GetExcludedLocations()) { hashStr += locationName; } for (const auto& pool : settings.GetMixedEntrancePools()) { for (const auto& type : pool) { hashStr += type; } } } // Change the seed if we're using plandomizer if (config.IsUsingPlandomizer()) { std::string plandomizerContents; auto retVal = utility::file::GetContents(config.GetPlandomizerPath(), plandomizerContents); if (!ignoreInvalidPlandomizer && retVal != 0) { LOG_TO_ERROR("Could not read plandomizer file at \"" + config.GetPlandomizerPath().generic_string() + "\""); return 1; } hashStr += plandomizerContents; } // Change the seed if we're generating a spoiler log if (config.IsGeneratingSpoilerLog()) { hashStr += "Spoiler Log: True"; } const size_t integerSeed = utility::crc32(hashStr.data(), hashStr.length()); utility::random::RandomInit(integerSeed); return 0; } } // namespace randomizer::seedgen::config