mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-26 06:52:40 -04:00
1769 lines
70 KiB
C++
1769 lines
70 KiB
C++
#include "randomizer_context.hpp"
|
|
|
|
#include "session.hpp"
|
|
#include "paths.hpp"
|
|
#include "flags.h"
|
|
#include "tools.h"
|
|
#include "stages.h"
|
|
#include "verify_item_functions.h"
|
|
#include "item_ids.h"
|
|
#include "../generator/utility/crc32.hpp"
|
|
#include "../generator/utility/endian.hpp"
|
|
#include "../generator/utility/yaml.hpp"
|
|
#include "../generator/randomizer.hpp"
|
|
#include "../generator/utility/text.hpp"
|
|
#include "../generator/utility/string.hpp"
|
|
#include "../generator/logic/entrance_shuffle.hpp"
|
|
|
|
#include <fstream>
|
|
#include <type_traits>
|
|
#include <unordered_set>
|
|
#include <mods/svc/log.hpp>
|
|
|
|
#include "d/actor/d_a_alink.h"
|
|
#include "d/d_com_inf_game.h"
|
|
#include "d/d_meter2.h"
|
|
#include "d/d_meter2_draw.h"
|
|
#include "d/d_meter2_info.h"
|
|
#include "d/d_msg_class.h"
|
|
#include "m_Do/m_Do_audio.h"
|
|
|
|
namespace {
|
|
|
|
const char* flow_node_type_name(RandomizerContext::FlowNodeType type) {
|
|
switch (type) {
|
|
case RandomizerContext::FlowNodeType::MESSAGE:
|
|
return "message";
|
|
case RandomizerContext::FlowNodeType::BRANCH:
|
|
return "branch";
|
|
case RandomizerContext::FlowNodeType::EVENT:
|
|
return "event";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
RandomizerContext::FlowNodeType parse_flow_node_type(const YAML::Node& node) {
|
|
const auto type = node.as<std::string>();
|
|
if (type == "message") {
|
|
return RandomizerContext::FlowNodeType::MESSAGE;
|
|
}
|
|
if (type == "branch") {
|
|
return RandomizerContext::FlowNodeType::BRANCH;
|
|
}
|
|
if (type == "event") {
|
|
return RandomizerContext::FlowNodeType::EVENT;
|
|
}
|
|
throw std::runtime_error("Unknown flow node type: " + type);
|
|
}
|
|
|
|
YAML::Node write_flow_reference(const RandomizerContext::FlowReference& reference) {
|
|
YAML::Node node{};
|
|
if (reference.nativeId.has_value()) {
|
|
node = reference.nativeId.value();
|
|
} else {
|
|
node = reference.name;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
RandomizerContext::FlowReference parse_flow_reference(const YAML::Node& node) {
|
|
if (!node.IsScalar()) {
|
|
throw std::runtime_error("Flow reference must be a node ID or name");
|
|
}
|
|
RandomizerContext::FlowReference reference{};
|
|
try {
|
|
reference.nativeId = node.as<u16>();
|
|
} catch (const YAML::BadConversion&) {
|
|
reference.name = node.as<std::string>();
|
|
}
|
|
return reference;
|
|
}
|
|
|
|
void write_message_style(
|
|
YAML::Node node, const RandomizerContext::MessageStyleData& style) {
|
|
node["eventLabelId"] = style.eventLabelId;
|
|
node["speaker"] = static_cast<u16>(style.speaker);
|
|
node["boxKind"] = static_cast<u16>(style.boxKind);
|
|
node["drawType"] = static_cast<u16>(style.drawType);
|
|
node["boxPosition"] = static_cast<u16>(style.boxPosition);
|
|
node["lineAlignment"] = static_cast<u16>(style.lineAlignment);
|
|
node["speakerMood"] = static_cast<u16>(style.speakerMood);
|
|
node["cameraAttr"] = static_cast<u16>(style.cameraAttr);
|
|
node["talkAnim"] = static_cast<u16>(style.talkAnim);
|
|
node["faceAnim"] = static_cast<u16>(style.faceAnim);
|
|
node["trailingData"] = style.trailingData;
|
|
}
|
|
|
|
RandomizerContext::MessageStyleData parse_message_style(const YAML::Node& node) {
|
|
RandomizerContext::MessageStyleData style{};
|
|
style.eventLabelId = node["eventLabelId"].as<u16>();
|
|
style.speaker = node["speaker"].as<u8>();
|
|
style.boxKind = node["boxKind"].as<u8>();
|
|
style.drawType = node["drawType"].as<u8>();
|
|
style.boxPosition = node["boxPosition"].as<u8>();
|
|
style.lineAlignment = node["lineAlignment"].as<u8>();
|
|
style.speakerMood = node["speakerMood"].as<u8>();
|
|
style.cameraAttr = node["cameraAttr"].as<u8>();
|
|
style.talkAnim = node["talkAnim"].as<u8>();
|
|
style.faceAnim = node["faceAnim"].as<u8>();
|
|
style.trailingData = node["trailingData"].as<u16>();
|
|
return style;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::optional<std::string> RandomizerContext::WriteToFile() {
|
|
|
|
std::ofstream seedData(this->GetSeedDataPath());
|
|
if (!seedData.is_open()) {
|
|
return "Could not open seed data file";
|
|
}
|
|
|
|
YAML::Node out{};
|
|
out["formatVersion"] = FORMAT_VERSION;
|
|
|
|
for (const auto& [setting, option] : this->mSettings) {
|
|
out["mSettings"][setting] = option;
|
|
}
|
|
|
|
// NOTE: When dumping u8s, they must be converted to u16s (or higher), otherwise they get dumped
|
|
// as single characters and not numbers
|
|
|
|
out["mStartEventFlags"] = this->mStartEventFlags;
|
|
for (const auto& [region, flags] : this->mStartRegionFlags) {
|
|
const std::list<u16> u16Flags(flags.begin(), flags.end());
|
|
out["mStartRegionFlags"][static_cast<u16>(region)] = u16Flags;
|
|
}
|
|
|
|
const std::list<u16> u16Inventory(this->mStartingInventory.begin(), this->mStartingInventory.end());
|
|
out["mStartingInventory"] = u16Inventory;
|
|
|
|
const std::unordered_map<u16, u16> u16ChestOverrides(this->mTreasureChestOverrides.begin(), this->mTreasureChestOverrides.end());
|
|
out["mTreasureChestOverrides"] = u16ChestOverrides;
|
|
|
|
const std::unordered_map<u16, u16> u16PoeOverrides(this->mPoeOverrides.begin(), this->mPoeOverrides.end());
|
|
out["mPoeOverrides"] = u16PoeOverrides;
|
|
|
|
const std::unordered_map<u16, u16> u16FreestandingItemOverrides(this->mFreestandingItemOverrides.begin(), this->mFreestandingItemOverrides.end());
|
|
out["mFreestandingItemOverrides"] = u16FreestandingItemOverrides;
|
|
|
|
const std::unordered_map<u16, u16> u16BugRewardOverrides(this->mBugRewardOverrides.begin(), this->mBugRewardOverrides.end());
|
|
out["mBugRewardOverrides"] = u16BugRewardOverrides;
|
|
|
|
const std::unordered_map<u16, u16> u16SkyCharacterOverrides(this->mSkyCharacterOverrides.begin(), this->mSkyCharacterOverrides.end());
|
|
out["mSkyCharacterOverrides"] = u16SkyCharacterOverrides;
|
|
|
|
const std::unordered_map<u16, u16> u16GoldenWolfOverrides(this->mGoldenWolfOverrides.begin(), this->mGoldenWolfOverrides.end());
|
|
out["mGoldenWolfOverrides"] = u16GoldenWolfOverrides;
|
|
|
|
const std::unordered_map<u16, u16> u16ShopOverrides(this->mShopOverrides.begin(), this->mShopOverrides.end());
|
|
out["mShopOverrides"] = u16ShopOverrides;
|
|
|
|
out["mTwilitInsectOverrides"] = mTwilitInsectOverrides;
|
|
|
|
for (const auto& [name, data] : this->mItemLocations) {
|
|
auto node = out["mItemLocations"][name];
|
|
node["itemId"] = data.itemId;
|
|
node["stage"] = data.stage;
|
|
node["flag"] = data.flag;
|
|
}
|
|
|
|
out["mStartHour"] = static_cast<u16>(this->mStartHour);
|
|
out["mMapBits"] = static_cast<u16>(this->mMapBits);
|
|
|
|
for (const auto& [stageRoomLayer, actorPatches] : this->mObjectPatches) {
|
|
for (const auto& [actorCRC, actorPatch] : actorPatches) {
|
|
auto node = out["mObjectPatches"][stageRoomLayer][actorCRC];
|
|
node["data"] = ContainerToHexString(actorPatch.bytes);
|
|
if (!actorPatch.flow.empty()) {
|
|
node["flow"] = actorPatch.flow;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto& [stageRoomLayer, newActors] : this->mObjectAdditions) {
|
|
for (const auto& actor : newActors) {
|
|
YAML::Node node{};
|
|
node["data"] = ContainerToHexString(actor.bytes);
|
|
if (!actor.flow.empty()) {
|
|
node["flow"] = actor.flow;
|
|
}
|
|
out["mObjectAdditions"][stageRoomLayer].push_back(node);
|
|
}
|
|
}
|
|
|
|
for (const auto& flow : mFlowNodes) {
|
|
YAML::Node node{};
|
|
node["type"] = flow_node_type_name(flow.type);
|
|
node["group"] = static_cast<u16>(flow.group);
|
|
if (flow.patchIndex.has_value()) {
|
|
node["patchIndex"] = flow.patchIndex.value();
|
|
} else {
|
|
node["name"] = flow.name;
|
|
}
|
|
node["parameters"] = flow.parameters;
|
|
if (!flow.operation.empty()) {
|
|
node["operation"] = flow.operation;
|
|
}
|
|
if (flow.type == FlowNodeType::MESSAGE) {
|
|
node["message"] = write_flow_reference(flow.message);
|
|
}
|
|
if (flow.type != FlowNodeType::BRANCH) {
|
|
node["next"] = write_flow_reference(flow.next);
|
|
}
|
|
for (const auto& result : flow.results) {
|
|
node["results"].push_back(write_flow_reference(result));
|
|
}
|
|
out["mFlowNodes"].push_back(node);
|
|
}
|
|
|
|
for (const auto& message : mCustomMessages) {
|
|
YAML::Node node{};
|
|
node["group"] = static_cast<u16>(message.group);
|
|
node["name"] = message.name;
|
|
write_message_style(node["style"], message.style);
|
|
for (const auto& [language, text] : message.text) {
|
|
const auto languageName = randomizer::languageToString(
|
|
static_cast<randomizer::Text::Language>(language));
|
|
node["text"][languageName] = YAML::Binary(
|
|
reinterpret_cast<const unsigned char*>(text.data()), text.size());
|
|
}
|
|
out["mCustomMessages"].push_back(node);
|
|
}
|
|
|
|
// Dump text overrides as binary to avoid losing intentional null characters
|
|
YAML::Emitter textData;
|
|
textData << YAML::BeginMap;
|
|
textData << YAML::Key << "mTextOverrides";
|
|
textData << YAML::BeginMap;
|
|
for (auto language : randomizer::supportedLanguages) {
|
|
auto languageStr = randomizer::languageToString(language);
|
|
textData << YAML::Key << languageStr;
|
|
textData << YAML::BeginMap;
|
|
for (const auto& [key, text] : this->mTextOverrides[language]) {
|
|
textData << YAML::Key << key;
|
|
textData << YAML::Value << YAML::Binary(reinterpret_cast<const unsigned char*>(text.data()), text.size());
|
|
}
|
|
textData << YAML::EndMap;
|
|
}
|
|
textData << YAML::EndMap;
|
|
textData << YAML::EndMap;
|
|
|
|
for (const auto& [key, override] : mEntranceOverrides) {
|
|
out["mEntranceOverrides"][std::bit_cast<uint64_t>(key)] = std::bit_cast<uint64_t>(override);
|
|
}
|
|
|
|
for (const auto& [key, override] : mReturnToPlaceOverrides) {
|
|
out["mReturnToPlaceOverrides"][key] = std::bit_cast<uint64_t>(override);
|
|
}
|
|
|
|
seedData << YAML::Dump(out);
|
|
seedData << '\n' << textData.c_str();
|
|
seedData.close();
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<std::string> RandomizerContext::LoadFromHash(const std::string& hash) {
|
|
this->mHash = hash;
|
|
|
|
if (!std::filesystem::exists(this->GetSeedDataPath())) {
|
|
mods::log::error("Failed to load Hash: {}", hash);
|
|
mHash.clear();
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto in = LoadYAML(this->GetSeedDataPath());
|
|
if (!in["formatVersion"] || in["formatVersion"].as<u32>() != FORMAT_VERSION) {
|
|
mods::log::error("Seed {} uses an obsolete data format and must be regenerated", hash);
|
|
mHash.clear();
|
|
return "Seed data format is obsolete; regenerate this seed";
|
|
}
|
|
|
|
// Necessary settings
|
|
for (const auto& settingNode : in["mSettings"] ) {
|
|
const auto& setting = settingNode.first.as<int>();
|
|
const auto& option = settingNode.second.as<int>();
|
|
this->mSettings[setting] = option;
|
|
}
|
|
|
|
// Event flags
|
|
for (const auto& flag : in["mStartEventFlags"]) {
|
|
this->mStartEventFlags.push_back(flag.as<u16>());
|
|
}
|
|
// Region Flags
|
|
for (const auto& regionNode : in["mStartRegionFlags"]) {
|
|
const auto& regionId = regionNode.first.as<u8>();
|
|
for (const auto& flag : regionNode.second) {
|
|
this->mStartRegionFlags[regionId].push_back(flag.as<u8>());
|
|
}
|
|
}
|
|
|
|
// Starting inventory
|
|
for (const auto& itemId : in["mStartingInventory"]) {
|
|
this->mStartingInventory.push_back(itemId.as<u8>());
|
|
}
|
|
|
|
// Chest overrides
|
|
for (const auto& chestNode : in["mTreasureChestOverrides"]) {
|
|
u16 key = chestNode.first.as<u16>();
|
|
u8 itemId = chestNode.second.as<u8>();
|
|
this->mTreasureChestOverrides[key] = itemId;
|
|
}
|
|
|
|
// Poe Overrides
|
|
for (const auto& poeNode : in["mPoeOverrides"]) {
|
|
u16 key = poeNode.first.as<u16>();
|
|
u8 itemId = poeNode.second.as<u8>();
|
|
this->mPoeOverrides[key] = itemId;
|
|
}
|
|
|
|
// Freestanding overrides
|
|
for (const auto& itemNode : in["mFreestandingItemOverrides"]) {
|
|
u16 key = itemNode.first.as<u16>();
|
|
u8 itemId = itemNode.second.as<u8>();
|
|
this->mFreestandingItemOverrides[key] = itemId;
|
|
}
|
|
|
|
// Bug Rewards
|
|
for (const auto& bugNode : in["mBugRewardOverrides"]) {
|
|
u8 bugItemId = bugNode.first.as<u8>();
|
|
u8 itemId = bugNode.second.as<u8>();
|
|
this->mBugRewardOverrides[bugItemId] = itemId;
|
|
}
|
|
|
|
// Sky Characters
|
|
for (const auto& skyCharacterNode : in["mSkyCharacterOverrides"]) {
|
|
u16 key = skyCharacterNode.first.as<u16>();
|
|
u8 itemId = skyCharacterNode.second.as<u8>();
|
|
this->mSkyCharacterOverrides[key] = itemId;
|
|
}
|
|
|
|
// Golden Wolves
|
|
for (const auto& goldenWolfNode : in["mGoldenWolfOverrides"]) {
|
|
u16 key = goldenWolfNode.first.as<u16>();
|
|
u8 itemId = goldenWolfNode.second.as<u8>();
|
|
this->mGoldenWolfOverrides[key] = itemId;
|
|
}
|
|
|
|
// Shop Items
|
|
for (const auto& shopNode : in["mShopOverrides"]) {
|
|
u16 key = shopNode.first.as<u16>();
|
|
u8 itemId = shopNode.second.as<u8>();
|
|
this->mShopOverrides[key] = itemId;
|
|
}
|
|
|
|
for (const auto& twilitInsectNode : in["mTwilitInsectOverrides"]) {
|
|
u16 key = twilitInsectNode.first.as<u16>();
|
|
u16 itemId = twilitInsectNode.second.as<u16>();
|
|
this->mTwilitInsectOverrides[key] = itemId;
|
|
}
|
|
|
|
// Helper function for getting the item data out of a YAML node
|
|
auto retrieveItemData = [](auto& itemData, const YAML::Node& node) {
|
|
itemData.itemId = node["itemId"].as<int>();
|
|
itemData.stage = node["stage"].as<int>();
|
|
itemData.flag = node["flag"].as<u16>();
|
|
};
|
|
|
|
// Items we call by location name
|
|
for (const auto& locationNode : in["mItemLocations"]) {
|
|
const auto& locationName = nameLookupOverride(locationNode.first.as<std::string>());
|
|
retrieveItemData(this->mItemLocations[locationName], locationNode.second);
|
|
}
|
|
|
|
// Starting hour
|
|
this->mStartHour = in["mStartHour"].as<u8>();
|
|
// Starting map bits
|
|
this->mMapBits = in["mMapBits"].as<u8>();
|
|
|
|
// Object Patches
|
|
for (const auto& stageRoomLayerNode: in["mObjectPatches"]) {
|
|
u32 stageRoomLayer = stageRoomLayerNode.first.as<u32>();
|
|
for (const auto& actorPatchNode : stageRoomLayerNode.second) {
|
|
u32 actorCRC = actorPatchNode.first.as<u32>();
|
|
auto& actor = this->mObjectPatches[stageRoomLayer][actorCRC];
|
|
actor.bytes = HexToBytes(actorPatchNode.second["data"].as<std::string>());
|
|
if (actorPatchNode.second["flow"]) {
|
|
actor.flow = actorPatchNode.second["flow"].as<std::string>();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Object Additions
|
|
for (const auto& stageNode: in["mObjectAdditions"]) {
|
|
u32 stageRoomLayer = stageNode.first.as<u32>();
|
|
for (const auto& objectData : stageNode.second) {
|
|
ActorData actor{};
|
|
actor.bytes = HexToBytes(objectData["data"].as<std::string>());
|
|
if (objectData["flow"]) {
|
|
actor.flow = objectData["flow"].as<std::string>();
|
|
}
|
|
this->mObjectAdditions[stageRoomLayer].push_back(std::move(actor));
|
|
}
|
|
}
|
|
|
|
for (const auto& flowNode : in["mFlowNodes"]) {
|
|
FlowNode flow{};
|
|
flow.type = parse_flow_node_type(flowNode["type"]);
|
|
flow.group = flowNode["group"].as<u8>();
|
|
if (flowNode["patchIndex"]) {
|
|
flow.patchIndex = flowNode["patchIndex"].as<u16>();
|
|
} else {
|
|
flow.name = flowNode["name"].as<std::string>();
|
|
}
|
|
flow.parameters = flowNode["parameters"].as<u32>();
|
|
if (flowNode["operation"]) {
|
|
flow.operation = flowNode["operation"].as<std::string>();
|
|
}
|
|
if (flowNode["message"]) {
|
|
flow.message = parse_flow_reference(flowNode["message"]);
|
|
}
|
|
if (flowNode["next"]) {
|
|
flow.next = parse_flow_reference(flowNode["next"]);
|
|
}
|
|
for (const auto& result : flowNode["results"]) {
|
|
flow.results.push_back(parse_flow_reference(result));
|
|
}
|
|
mFlowNodes.push_back(std::move(flow));
|
|
}
|
|
|
|
for (const auto& messageNode : in["mCustomMessages"]) {
|
|
CustomMessage message{};
|
|
message.group = messageNode["group"].as<u8>();
|
|
message.name = messageNode["name"].as<std::string>();
|
|
message.style = parse_message_style(messageNode["style"]);
|
|
for (const auto& textNode : messageNode["text"]) {
|
|
const auto language = randomizer::stringToLanguage(textNode.first.as<std::string>());
|
|
const auto binary = textNode.second.as<YAML::Binary>();
|
|
message.text[language] =
|
|
std::string(reinterpret_cast<const char*>(binary.data()), binary.size());
|
|
}
|
|
mCustomMessages.push_back(std::move(message));
|
|
}
|
|
|
|
// Text Overrides
|
|
for (const auto& languageNode: in["mTextOverrides"]) {
|
|
const auto& languageStr = languageNode.first.as<std::string>();
|
|
auto language = randomizer::stringToLanguage(languageStr);
|
|
for (const auto& textNode : languageNode.second) {
|
|
auto key = textNode.first.as<u32>();
|
|
auto binary = textNode.second.as<YAML::Binary>();
|
|
std::string text(reinterpret_cast<const char*>(binary.data()), binary.size());
|
|
this->mTextOverrides[language][key] = std::move(text);
|
|
}
|
|
}
|
|
|
|
// Entrance Overrides
|
|
for (const auto& entranceNode : in["mEntranceOverrides"]) {
|
|
const auto key = std::bit_cast<EntranceOverride>(entranceNode.first.as<uint64_t>());;
|
|
const auto override = std::bit_cast<EntranceOverride>(entranceNode.second.as<uint64_t>());
|
|
this->mEntranceOverrides[key] = override;
|
|
}
|
|
|
|
// Return to Place Overrides
|
|
for (const auto& entranceNode : in["mReturnToPlaceOverrides"]) {
|
|
auto key = entranceNode.first.as<int>();
|
|
const auto override = std::bit_cast<EntranceOverride>(entranceNode.second.as<uint64_t>());
|
|
this->mReturnToPlaceOverrides[key] = override;
|
|
}
|
|
|
|
UiToastDesc desc = UI_TOAST_DESC_INIT;
|
|
desc.type = "success";
|
|
desc.title_rml = "Randomizer";
|
|
std::string body_text = fmt::format("Loaded Randomizer Seed {}", this->mHash);
|
|
desc.body_rml = body_text.c_str();
|
|
desc.duration_ms = 3000;
|
|
randomizer::session::svc_mng.ui->push_toast(randomizer::session::svc_mng.mod_ctx, &desc);
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::filesystem::path RandomizerContext::GetSeedDataPath() const {
|
|
return ::randomizer::paths::GetRandomizerSeedsPath() / this->mHash / "seed.dat";
|
|
}
|
|
|
|
int RandomizerContext::SettingToEnum(const std::string& settingName) {
|
|
static const std::map<std::string, int> nameToEnum = {
|
|
{"Hyrule Barrier Dungeons", HYRULE_BARRIER_DUNGEONS},
|
|
{"Hyrule Barrier Requirements", HYRULE_BARRIER_REQUIREMENTS},
|
|
{"Hyrule Barrier Fused Shadows", HYRULE_BARRIER_FUSED_SHADOWS},
|
|
{"Hyrule Barrier Mirror Shards", HYRULE_BARRIER_MIRROR_SHARDS},
|
|
{"Hyrule Castle Big Key Requirements", HYRULE_BIG_KEY_REQUIREMENTS},
|
|
{"Hyrule Barrier Poe Souls", HYRULE_BARRIER_POE_SOULS},
|
|
{"Hyrule Barrier Hearts", HYRULE_BARRIER_HEARTS},
|
|
{"Hyrule Castle Big Key Mirror Shards", HYRULE_BIG_KEY_MIRROR_SHARDS},
|
|
{"Hyrule Castle Big Key Fused Shadows", HYRULE_BIG_KEY_FUSED_SHADOWS},
|
|
{"Hyrule Castle Big Key Dungeons", HYRULE_BIG_KEY_DUNGEONS},
|
|
{"Hyrule Castle Big Key Poe Souls", HYRULE_BIG_KEY_POE_SOULS},
|
|
{"Hyrule Castle Big Key Hearts", HYRULE_BIG_KEY_HEARTS},
|
|
{"Palace of Twilight Requirements", PALACE_OF_TWILIGHT_REQUIREMENTS},
|
|
{"Temple of Time Sword Requirement", TEMPLE_OF_TIME_SWORD_REQUIREMENT},
|
|
{"Skip Minor Cutscenes", SKIP_MINOR_CUTSCENES},
|
|
{"Skip Major Cutscenes", SKIP_MAJOR_CUTSCENES},
|
|
{"Skip Bridge Donation", SKIP_BRIDGE_DONATION},
|
|
{"Mirror Chamber Access", MIRROR_CHAMBER_ACCESS},
|
|
};
|
|
|
|
if (nameToEnum.contains(settingName)) {
|
|
return nameToEnum.at(settingName);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
int RandomizerContext::OptionToEnum(const std::string& optionName) {
|
|
static const std::map<std::string, int> nameToEnum = {
|
|
{"On", ON},
|
|
{"Off", OFF},
|
|
{"None", NONE},
|
|
{"Vanilla", VANILLA},
|
|
{"Open", OPEN},
|
|
{"Fused Shadows", FUSED_SHADOWS},
|
|
{"Mirror Shards", MIRROR_SHARDS},
|
|
{"Poe Souls", POE_SOULS},
|
|
{"Hearts", HEARTS},
|
|
{"Dungeons", DUNGEONS},
|
|
{"Wooden Sword", WOODEN_SWORD},
|
|
{"Ordon Sword", ORDON_SWORD},
|
|
{"Master Sword", MASTER_SWORD},
|
|
{"Light Sword", LIGHT_SWORD},
|
|
{"Closed", CLOSED},
|
|
{"Barrier", BARRIER},
|
|
};
|
|
|
|
if (nameToEnum.contains(optionName)) {
|
|
return nameToEnum.at(optionName);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
RandomizerState g_randomizerState;
|
|
|
|
int RandomizerState::_create() {
|
|
mInitialized = true;
|
|
mHasPendingToDChange = false;
|
|
// g_customMenuRing._initialize();
|
|
return 1;
|
|
}
|
|
|
|
int RandomizerState::_delete() {
|
|
mInitialized = false;
|
|
return 1;
|
|
}
|
|
|
|
static bool checkFoolishItemEffectReady()
|
|
{
|
|
// Verify Link is loaded on the map.
|
|
if (!daAlink_getAlinkActorClass())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Ensure Link is not in a cutscene
|
|
if (daAlink_getAlinkActorClass()->checkEventRun())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Make sure Link isn't riding anything
|
|
if (daAlink_getAlinkActorClass()->checkRide())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Ensure there are pointers to the mMeterClass and mpMeterDraw structs
|
|
if (!dMeter2Info_getMeterClass())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!dMeter2Info_getMeterClass()->getMeterDrawPtr())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Make sure Z button isn't dimmed
|
|
if (dMeter2Info_getMeterClass()->getMeterDrawPtr()->getButtonZAlpha() != 1.f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
switch (daAlink_getAlinkActorClass()->mProcID)
|
|
{
|
|
case daAlink_c::PROC_TALK:
|
|
case daAlink_c::PROC_WOLF_SWIM_MOVE:
|
|
case daAlink_c::PROC_SWIM_MOVE:
|
|
case daAlink_c::PROC_SWIM_WAIT:
|
|
case daAlink_c::PROC_WOLF_SWIM_WAIT:
|
|
case daAlink_c::PROC_SWIM_UP:
|
|
case daAlink_c::PROC_SWIM_DIVE:
|
|
{
|
|
return false;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void handleFoolishItem() {
|
|
u32 count = g_randomizerState.mFoolishItemCount;
|
|
if (count == 0) {
|
|
return;
|
|
}
|
|
|
|
if (!checkFoolishItemEffectReady())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Failsafe: Make sure the count does not somehow exceed 100
|
|
if (count > 100) {
|
|
count = 100;
|
|
}
|
|
|
|
// Reset count
|
|
g_randomizerState.mFoolishItemCount = 0;
|
|
|
|
/* Store the currently loaded sound wave to local variables as we will need to load them back later.
|
|
* We use this method because if we just loaded the sound waves every time the item was gotten, we'd
|
|
* eventually run out of memory so it is safer to unload everything and load it back in. */
|
|
|
|
auto sceneMgr = Z2GetSceneMgr();
|
|
const u32 seWave1 = Z2AudioMgr::getInterface()->loadedSeWave_1;
|
|
const u32 seWave2 = Z2AudioMgr::getInterface()->loadedSeWave_2;
|
|
sceneMgr->eraseSeWave(seWave1);
|
|
sceneMgr->eraseSeWave(seWave2);
|
|
sceneMgr->loadSeWave(0x46);
|
|
mDoAud_seStartLevel(0x10040, nullptr, 0, 0);
|
|
sceneMgr->loadSeWave(seWave1);
|
|
sceneMgr->loadSeWave(seWave2);
|
|
|
|
// Initiate the appropriate visual damage process
|
|
if (daAlink_getAlinkActorClass()->checkWolf())
|
|
{
|
|
daAlink_getAlinkActorClass()->procWolfDamageInit(nullptr);
|
|
}
|
|
else
|
|
{
|
|
daAlink_getAlinkActorClass()->procDamageInit(nullptr, 0);
|
|
}
|
|
|
|
daPy_py_c::setPlayerDamage(count, TRUE);
|
|
}
|
|
|
|
/*
|
|
* Updates flags for Hyrule Castle Barrier, Palace of Twilight Access,
|
|
* and Hyrule Castle Big Key chest. Maybe a bit overkill to check this every frame, but
|
|
* it keeps it all in one place for now.
|
|
*/
|
|
static void updateGoalFlags() {
|
|
auto& settings = randomizer_GetContext().mSettings;
|
|
|
|
// Hyrule Castle Barrier
|
|
if (!dComIfGs_isEventBit(BARRIER_GONE)) {
|
|
bool destroyBarrier = false;
|
|
switch (settings[RandomizerContext::HYRULE_BARRIER_REQUIREMENTS]) {
|
|
case RandomizerContext::VANILLA:
|
|
destroyBarrier = dComIfGs_isEventBit(PALACE_OF_TWILIGHT_CLEARED);
|
|
break;
|
|
case RandomizerContext::FUSED_SHADOWS:
|
|
destroyBarrier = numFusedShadows() >= settings[RandomizerContext::HYRULE_BARRIER_FUSED_SHADOWS];
|
|
break;
|
|
case RandomizerContext::MIRROR_SHARDS:
|
|
destroyBarrier = numMirrorShards() >= settings[RandomizerContext::HYRULE_BARRIER_MIRROR_SHARDS];
|
|
break;
|
|
case RandomizerContext::DUNGEONS:
|
|
destroyBarrier = numCompletedDungeons() >= settings[RandomizerContext::HYRULE_BARRIER_DUNGEONS];
|
|
break;
|
|
case RandomizerContext::POE_SOULS:
|
|
destroyBarrier = dComIfGs_getPohSpiritNum() >= settings[RandomizerContext::HYRULE_BARRIER_POE_SOULS];
|
|
break;
|
|
case RandomizerContext::HEARTS:
|
|
destroyBarrier = dComIfGs_getMaxLife() >= 5 * settings[RandomizerContext::HYRULE_BARRIER_HEARTS];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (destroyBarrier) {
|
|
dComIfGs_onEventBit(BARRIER_GONE);
|
|
}
|
|
}
|
|
|
|
// Hyrule Castle Big Key Gate
|
|
if (!dComIfGs_isStageSwitch(0x18, 0x4B)) {
|
|
bool openGate = false;
|
|
switch (settings[RandomizerContext::HYRULE_BIG_KEY_REQUIREMENTS]) {
|
|
case RandomizerContext::FUSED_SHADOWS:
|
|
openGate = numFusedShadows() >= settings[RandomizerContext::HYRULE_BIG_KEY_FUSED_SHADOWS];
|
|
break;
|
|
case RandomizerContext::MIRROR_SHARDS:
|
|
openGate = numMirrorShards() >= settings[RandomizerContext::HYRULE_BIG_KEY_MIRROR_SHARDS];
|
|
break;
|
|
case RandomizerContext::DUNGEONS:
|
|
openGate = numCompletedDungeons() >= settings[RandomizerContext::HYRULE_BIG_KEY_DUNGEONS];
|
|
break;
|
|
case RandomizerContext::POE_SOULS:
|
|
openGate = dComIfGs_getPohSpiritNum() >= settings[RandomizerContext::HYRULE_BIG_KEY_POE_SOULS];
|
|
break;
|
|
case RandomizerContext::HEARTS:
|
|
openGate = dComIfGs_getMaxLife() >= 5 * settings[RandomizerContext::HYRULE_BIG_KEY_HEARTS];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (openGate) {
|
|
dComIfGs_onStageSwitch(0x18, 0x4B);
|
|
}
|
|
}
|
|
|
|
// Palace of Twilight Access
|
|
if (!dComIfGs_isEventBit(FIXED_THE_MIRROR_OF_TWILIGHT)) {
|
|
bool openPalace = false;
|
|
switch (settings[RandomizerContext::PALACE_OF_TWILIGHT_REQUIREMENTS]) {
|
|
case RandomizerContext::VANILLA:
|
|
openPalace = dComIfGs_isEventBit(CITY_IN_THE_SKY_CLEARED);
|
|
break;
|
|
case RandomizerContext::FUSED_SHADOWS:
|
|
openPalace = numFusedShadows() >= 3;
|
|
break;
|
|
case RandomizerContext::MIRROR_SHARDS:
|
|
openPalace = numMirrorShards() >= 4;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (openPalace) {
|
|
dComIfGs_onEventBit(FIXED_THE_MIRROR_OF_TWILIGHT);
|
|
}
|
|
}
|
|
}
|
|
|
|
int RandomizerState::execute() {
|
|
if (!mInitialized) {
|
|
return 0;
|
|
}
|
|
|
|
// Always check for and handle time of day changes
|
|
if (getTimeChange() != NO_CHANGE) {
|
|
handleTimeSpeed();
|
|
}
|
|
|
|
bool currentReloadingState;
|
|
// Any custom functionality that relies on Link's actor being on a stage
|
|
if (daAlink_getAlinkActorClass()) {
|
|
currentReloadingState = daAlink_getAlinkActorClass()->checkRestartRoom();
|
|
}
|
|
else {
|
|
currentReloadingState = true;
|
|
}
|
|
|
|
bool prevReloadingState = getRoomReloadingState();
|
|
if (!currentReloadingState) {
|
|
if (prevReloadingState) {
|
|
offLoad();
|
|
}
|
|
}
|
|
setRoomReloadingState(currentReloadingState);
|
|
|
|
if (getStageID() != Title_Screen) {
|
|
handleFoolishItem();
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
int RandomizerState::draw() {
|
|
return 1;
|
|
}
|
|
|
|
void RandomizerState::handleTimeOfDayChange()
|
|
{
|
|
if (dComIfGp_roomControl_getTimePass())
|
|
{
|
|
// No point in changing values if we are already changing the time.
|
|
if (getTimeChange() == NO_CHANGE)
|
|
{
|
|
if (!dKy_daynight_check()) // Day time
|
|
{
|
|
setTimeChange(CHANGE_TO_NIGHT);
|
|
}
|
|
else
|
|
{
|
|
setTimeChange(CHANGE_TO_DAY);
|
|
}
|
|
g_env_light.time_change_rate = 1.f; // Increase time speed
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!dKy_daynight_check()) // Day time
|
|
{
|
|
dComIfGs_setTime(285.f);
|
|
}
|
|
else
|
|
{
|
|
dComIfGs_setTime(105.f);
|
|
}
|
|
|
|
static_cast<dStage_nextStage_c*>(dComIfGp_getNextStartStage())->onEnable();
|
|
}
|
|
}
|
|
|
|
void RandomizerState::handleTimeSpeed()
|
|
{
|
|
|
|
if (!dKy_daynight_check()) // Day time
|
|
{
|
|
if (getTimeChange() == CHANGE_TO_DAY)
|
|
{
|
|
g_env_light.time_change_rate = 0.012f; // Set time speed to normal
|
|
setTimeChange(NO_CHANGE);
|
|
}
|
|
}
|
|
else if (getTimeChange() == CHANGE_TO_NIGHT)
|
|
{
|
|
g_env_light.time_change_rate = 0.012f; // Set time speed to normal
|
|
setTimeChange(NO_CHANGE);
|
|
}
|
|
}
|
|
|
|
void RandomizerState::offLoad()
|
|
{
|
|
if ((getStageID() == City_in_the_Sky) && (dStage_roomControl_c::mStayNo == 0) && (dComIfGp_getStartStagePoint() == 3))
|
|
{
|
|
// Fan in the main room active
|
|
dComIfGs_offSaveSwitch(0xA);
|
|
|
|
// Main Room 1F explored
|
|
dComIfGs_offSaveSwitch(0xF);
|
|
}
|
|
|
|
if (playerIsInRoomStage(1, allStages[Sacred_Grove]))
|
|
{
|
|
// If the portal in SG isn't active then we want to spawn the shadow beasts.
|
|
if (!dComIfGs_isSaveSwitch(0x64))
|
|
{
|
|
dComIfGs_onSvOneZoneSwitch(0, 0xE);
|
|
}
|
|
}
|
|
|
|
if ((getStageID() == Ordon_Ranch) && (dComIfGp_getStartStagePoint() == 1))
|
|
{
|
|
// Clear the danBit that starts a conversation when entering the ranch so the player can do goats as needed.
|
|
dComIfGs_offSaveDunSwitch(0x0);
|
|
}
|
|
|
|
// Check and update our goal flags
|
|
updateGoalFlags();
|
|
}
|
|
|
|
RandomizerContext& randomizer_GetContext() {
|
|
static RandomizerContext instance;
|
|
return instance;
|
|
}
|
|
|
|
bool randomizer_IsActive() {
|
|
return (!playerIsOnTitleScreen() || randomizer_GetContext().mCreatingSave) && !randomizer_GetContext().mHash.empty();
|
|
}
|
|
|
|
std::vector<u8> HexToBytes(std::string hex) {
|
|
std::vector<u8> bytes;
|
|
// Strip "0x" if present
|
|
if (hex.substr(0, 2) == "0x") hex = hex.substr(2);
|
|
|
|
for (size_t i = 0; i < hex.length(); i += 2) {
|
|
std::string byteString = hex.substr(i, 2);
|
|
u8 byte = static_cast<u8>(strtol(byteString.c_str(), nullptr, 16));
|
|
bytes.push_back(byte);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
int randomizer_getItemAtLocation(const std::string& locationName) {
|
|
return randomizer_GetContext().mItemLocations[nameLookupOverride(locationName)].itemId;
|
|
}
|
|
|
|
void randomizer_checkAndOverrideEntranceData(const char*& stageName, s8& roomNo, s16& pointNo, s8& mapLayer, u32& lastMode) {
|
|
RandomizerContext::EntranceOverride override = {
|
|
.stageId = static_cast<u8>(getStageID(stageName)), .roomNo = roomNo, .mapLayer = mapLayer, .pointNo = static_cast<s16>(pointNo)};
|
|
|
|
// Debugging: Dump mEntranceOverrides
|
|
// for (const auto& [key,val] : randomizer_GetContext().mEntranceOverrides) {
|
|
// mods::log::info("({},{},{},{}) -> ({},{},{},{})",key.stageId,key.roomNo,key.mapLayer,key.pointNo,val.stageId,val.roomNo,val.mapLayer,val.pointNo);
|
|
// }
|
|
|
|
// mods::log::info("Original Stage:{}, {}, {}, {}",stageName,roomNo,pointNo,mapLayer);
|
|
|
|
auto it = randomizer_GetContext().mEntranceOverrides.find(override);
|
|
if (it == randomizer_GetContext().mEntranceOverrides.end()) {
|
|
// If the specific layer specified isn't found, search the overrides again for layer -1
|
|
// This is used to resolve issues where a transition (like the door to the past) requests
|
|
// A specific layer to load
|
|
override.mapLayer = -1;
|
|
it = randomizer_GetContext().mEntranceOverrides.find(override);
|
|
}
|
|
|
|
if (it != randomizer_GetContext().mEntranceOverrides.end()) {
|
|
const auto& newOverride = it->second;
|
|
stageName = allStages[newOverride.stageId];
|
|
pointNo = newOverride.pointNo;
|
|
roomNo = newOverride.roomNo;
|
|
mapLayer = newOverride.mapLayer;
|
|
|
|
// Override the lastSceneMode if we are coming from a wolf dig hole
|
|
if ((lastMode&0xF) == 9) {
|
|
lastMode = (lastMode&0xFFFFFFF0);
|
|
}
|
|
|
|
// mods::log::info("New Stage:{}, {}, {}, {}",stageName,roomNo,pointNo,mapLayer);
|
|
}
|
|
}
|
|
|
|
void randomizer_setTempFlag(const RandomizerContext::itemLocationData& data) {
|
|
if (data.flag == 0xFFFF) {
|
|
return;
|
|
}
|
|
|
|
// If stage is 0xFF, then this is an event flag
|
|
if (data.stage == 0xFF) {
|
|
g_randomizerState.mTrackerTempEventFlag = data.flag;
|
|
}
|
|
// If it's less than 0x80 then it's a switch flag
|
|
else if (data.flag < 0x80) {
|
|
g_randomizerState.mTrackerTempSwitchFlag.stage = getStageSaveId(data.stage);
|
|
g_randomizerState.mTrackerTempSwitchFlag.flag = data.flag;
|
|
}
|
|
// Otherwise it's an item flag. Currently, any item flags that go through here are custom
|
|
// so we just set the bit directly.
|
|
else {
|
|
dComIfGs_onItem(data.flag, getStageSaveId(data.stage));
|
|
}
|
|
}
|
|
|
|
bool randomizer_checkTempleOfTimeRequirement() {
|
|
auto swordRequirement = randomizer_GetContext().mSettings[RandomizerContext::TEMPLE_OF_TIME_SWORD_REQUIREMENT];
|
|
u8 roomNo = dComIfGp_getStartStageRoomNo();
|
|
|
|
// Don't strike the pedestal again if we've already set the flag for striking it
|
|
if (roomNo == 1 && dComIfGs_isSwitch(0x63, roomNo)) {
|
|
return false;
|
|
}
|
|
|
|
// Make sure we have a sword in Link's hands.
|
|
auto equippedSword = dComIfGs_getSelectEquipSword();
|
|
if (equippedSword != 0xFF) {
|
|
// Fallthrough is intentional to check each potential sword requirement below the current equipped sword
|
|
switch (equippedSword) {
|
|
case dItemNo_LIGHT_SWORD_e:
|
|
if (swordRequirement == RandomizerContext::LIGHT_SWORD) {
|
|
return true;
|
|
}
|
|
case dItemNo_MASTER_SWORD_e:
|
|
if (swordRequirement == RandomizerContext::MASTER_SWORD) {
|
|
return true;
|
|
}
|
|
case dItemNo_SWORD_e:
|
|
if (swordRequirement == RandomizerContext::ORDON_SWORD) {
|
|
return true;
|
|
}
|
|
case dItemNo_WOOD_STICK_e:
|
|
if (swordRequirement == RandomizerContext::WOODEN_SWORD) {
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool randomizer_mirrorChamberWallShouldExist() {
|
|
auto mirrorChamberAccess = randomizer_GetContext().mSettings[RandomizerContext::MIRROR_CHAMBER_ACCESS];
|
|
return mirrorChamberAccess == RandomizerContext::CLOSED ||
|
|
(mirrorChamberAccess == RandomizerContext::BARRIER && !dComIfGs_isStageBossEnemy(0x13));
|
|
}
|
|
|
|
void randomizer_returnToSpawn(bool tryOverride) {
|
|
|
|
auto& placeOverrides = randomizer_GetContext().mReturnToPlaceOverrides;
|
|
auto stageId = getStageID();
|
|
|
|
// If we're trying to override the default return to spawn
|
|
if (tryOverride && placeOverrides.contains(stageId)) {
|
|
auto entrance = placeOverrides[stageId];
|
|
|
|
// If in lakebed temple, spawn on land if shadow crystal is obtained like vanilla
|
|
if (entrance.stageId == Lakebed_Temple && dComIfGs_isEventBit(TRANSFORMING_UNLOCKED)) {
|
|
entrance.pointNo = 2;
|
|
}
|
|
|
|
dComIfGp_setNextStage(allStages[entrance.stageId], entrance.pointNo, entrance.roomNo, entrance.mapLayer);
|
|
return;
|
|
}
|
|
|
|
// If a player hasn't completed a twilight/MDH, we want to unset the transform flag so they aren't forced to be wolf
|
|
// unnecessarily.
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
if (!dComIfGs_isDarkClearLV(i)) {
|
|
dComIfGs_offTransformLV(i);
|
|
}
|
|
}
|
|
|
|
// If Midna's Desperate Hour is not complete, unset the flags that trigger it incase the player
|
|
// used return to spawn while MDH was active
|
|
if (!dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED)) {
|
|
dComIfGs_offStageSwitch(4, 0xE);
|
|
dComIfGs_offEventBit(MIDNAS_DESPERATE_HOUR_STARTED);
|
|
}
|
|
|
|
// Turn the player back into Link if they are currently wolf
|
|
dComIfGs_setTransformStatus(TF_STATUS_HUMAN);
|
|
|
|
// Return to spawn. If the spawn has been randomized, that's taken care of within the function
|
|
dComIfGp_setNextStage("F_SP103", 1, 1, -1);
|
|
}
|
|
|
|
u8 randomizer_getRandomFoolishItemModelID() {
|
|
static constexpr auto foolishItemModels = std::to_array<u8>({
|
|
dItemNo_Randomizer_ARMOR_e,
|
|
dItemNo_Randomizer_WOOD_STICK_e,
|
|
dItemNo_Randomizer_WOOD_SHIELD_e,
|
|
dItemNo_Randomizer_HYLIA_SHIELD_e,
|
|
dItemNo_Randomizer_MAGIC_LV1_e,
|
|
dItemNo_Randomizer_FISHING_ROD_1_e,
|
|
dItemNo_Randomizer_HAWK_EYE_e,
|
|
dItemNo_Randomizer_BOOMERANG_e,
|
|
dItemNo_Randomizer_SPINNER_e,
|
|
dItemNo_Randomizer_IRONBALL_e,
|
|
dItemNo_Randomizer_BOW_e,
|
|
dItemNo_Randomizer_COPY_ROD_e,
|
|
dItemNo_Randomizer_HOOKSHOT_e,
|
|
dItemNo_Randomizer_HVY_BOOTS_e,
|
|
dItemNo_Randomizer_PACHINKO_e,
|
|
dItemNo_Randomizer_BOMB_BAG_LV1_e,
|
|
dItemNo_Randomizer_ANCIENT_DOCUMENT_e,
|
|
});
|
|
|
|
u8 selectedModal = foolishItemModels[static_cast<int>(cM_rnd() * foolishItemModels.size()) % foolishItemModels.size()];
|
|
return verifyProgressiveItem(selectedModal);
|
|
}
|
|
|
|
u32 getActorPatchesCurrentStageKey(u8 roomNo) {
|
|
u32 actorPatchesStageKey{};
|
|
actorPatchesStageKey |= getStageID(dComIfGp_getStartStageName()) << 16;
|
|
actorPatchesStageKey |= roomNo << 8;
|
|
actorPatchesStageKey |= dComIfG_play_c::getLayerNo(0);
|
|
return actorPatchesStageKey;
|
|
}
|
|
|
|
u32 getStageObjCRC32(u8* data, size_t size) {
|
|
return randomizer::utility::crc32(data, size);
|
|
}
|
|
|
|
stage_tgsc_data_class parseObjData(const YAML::Node& objectNode) {
|
|
using namespace Utility::Endian;
|
|
// Get all the data for the actor (with endian shenanigans)
|
|
stage_tgsc_data_class object{};
|
|
const auto& actorName = objectNode["name"].as<std::string>();
|
|
strncpy(object.name, actorName.c_str(), 8);
|
|
object.base.parameters = toPlatform(target, objectNode["parameters"].as<u32>());
|
|
object.base.position.x = toPlatform(target, objectNode["position"]["x"].as<f32>());
|
|
object.base.position.y = toPlatform(target, objectNode["position"]["y"].as<f32>());
|
|
object.base.position.z = toPlatform(target, objectNode["position"]["z"].as<f32>());
|
|
// Have to retrieve as u16 and then cast as s16 because otherwise yaml-cpp
|
|
// complains about values over 32767 not fitting in s16
|
|
object.base.angle.x = toPlatform(target, static_cast<s16>(objectNode["angle"]["x"].as<u16>()));
|
|
object.base.angle.y = toPlatform(target, static_cast<s16>(objectNode["angle"]["y"].as<u16>()));
|
|
object.base.angle.z = toPlatform(target, static_cast<s16>(objectNode["angle"]["z"].as<u16>()));
|
|
object.base.setID = toPlatform(target, static_cast<s16>(objectNode["set id"].as<u16>()));
|
|
|
|
if (objectNode["scale"]) {
|
|
object.scale.x = objectNode["scale"]["x"].as<u8>();
|
|
object.scale.y = objectNode["scale"]["y"].as<u8>();
|
|
object.scale.z = objectNode["scale"]["z"].as<u8>();
|
|
} else {
|
|
object.scale = fopAcM_prmScale_class{0, 0, 0};
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
void parseObjPatchData(stage_tgsc_data_class& object, const YAML::Node& patchNode) {
|
|
using namespace Utility::Endian;
|
|
if (patchNode["name"]) {
|
|
const auto& newName = patchNode["name"].as<std::string>();
|
|
strncpy(object.name, newName.c_str(), 8);
|
|
}
|
|
if (patchNode["parameters"]) {
|
|
object.base.parameters = toPlatform(target, patchNode["parameters"].as<u32>());
|
|
}
|
|
if (auto patchPosition = patchNode["position"]) {
|
|
if (patchPosition["x"]) {
|
|
object.base.position.x = toPlatform(target, patchPosition["x"].as<f32>());
|
|
}
|
|
if (patchPosition["y"]) {
|
|
object.base.position.y = toPlatform(target, patchPosition["y"].as<f32>());
|
|
}
|
|
if (patchPosition["z"]) {
|
|
object.base.position.z = toPlatform(target, patchPosition["z"].as<f32>());
|
|
}
|
|
}
|
|
if (auto patchAngle = patchNode["angle"]) {
|
|
// Have to retrieve as u16 and then cast as s16 because otherwise yaml-cpp
|
|
// complains about values over 32767 not fitting in s16
|
|
if (patchAngle["x"]) {
|
|
object.base.angle.x = toPlatform(target, static_cast<s16>(patchAngle["x"].as<u16>()));
|
|
}
|
|
if (patchAngle["y"]) {
|
|
object.base.angle.y = toPlatform(target, static_cast<s16>(patchAngle["y"].as<u16>()));
|
|
}
|
|
if (patchAngle["z"]) {
|
|
object.base.angle.z = toPlatform(target, static_cast<s16>(patchAngle["z"].as<u16>()));
|
|
}
|
|
}
|
|
if (auto patchScale = patchNode["scale"]) {
|
|
// Have to retrieve as u16 and then cast as s16 because otherwise yaml-cpp
|
|
// complains about values over 32767 not fitting in s16
|
|
if (patchScale["x"]) {
|
|
object.scale.x = toPlatform(target, static_cast<s16>(patchScale["x"].as<u16>()));
|
|
}
|
|
if (patchScale["y"]) {
|
|
object.scale.y = toPlatform(target, static_cast<s16>(patchScale["y"].as<u16>()));
|
|
}
|
|
if (patchScale["z"]) {
|
|
object.scale.z = toPlatform(target, static_cast<s16>(patchScale["z"].as<u16>()));
|
|
}
|
|
}
|
|
}
|
|
|
|
RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
|
|
RandomizerContext randoData{};
|
|
|
|
// Settings we need to check ingame
|
|
for (const auto& [setting, info] : *randomizer::seedgen::settings::GetAllSettingsInfo()) {
|
|
if (info->NeedInGame()) {
|
|
auto settingEnum = RandomizerContext::SettingToEnum(setting);
|
|
if (settingEnum == -1) {
|
|
throw std::runtime_error("Setting \"" + setting + "\" does not have an associated enum value");
|
|
}
|
|
auto option = world->Setting(setting).GetCurrentOption();
|
|
int optionEnum{};
|
|
// If this setting's options are just numbers, get the numeric value
|
|
if (info->OptionsAreNumbers()) {
|
|
optionEnum = world->Setting(setting).GetCurrentOptionAsNumber();
|
|
} else {
|
|
optionEnum = RandomizerContext::OptionToEnum(option);
|
|
}
|
|
if (optionEnum == -1) {
|
|
throw std::runtime_error("Option \"" + option + "\" for setting \"" + setting + "\" does not have an associated enum value");
|
|
}
|
|
randoData.mSettings[settingEnum] = optionEnum;
|
|
}
|
|
}
|
|
|
|
// Set data for all locations
|
|
for (const auto& location : world->GetAllLocations()) {
|
|
const auto& metaData = location->GetMetadata();
|
|
|
|
// Chest Overrides
|
|
// Keyed by u16 of 0xFF00 (stage index) and 0x00FF (tbox id)
|
|
if (location->HasCategories("Chest")) {
|
|
for (const auto& chestNode : metaData["Chest"]) {
|
|
u8 stage = chestNode["Stage"].as<u8>();
|
|
u8 tboxId = chestNode["Tbox Id"].as<u8>();
|
|
u8 itemId = location->GetCurrentItem()->GetID();
|
|
u16 key = (stage << 8) | tboxId;
|
|
randoData.mTreasureChestOverrides[key] = itemId;
|
|
}
|
|
}
|
|
|
|
// Poe Overrides
|
|
// Keyed by u16 of 0xFF00 (stage index) and 0x00FF (collectible flag)
|
|
if (location->HasCategories("Poe")) {
|
|
for (const auto& poeNode : metaData["Poe"]) {
|
|
const auto& stage = poeNode["Stage"].as<u8>();
|
|
const auto& flag = poeNode["Flag"].as<u8>();
|
|
u8 itemId = location->GetCurrentItem()->GetID();
|
|
u16 key = (stage << 8) | flag;
|
|
randoData.mPoeOverrides[key] = itemId;
|
|
}
|
|
}
|
|
|
|
// Freestanding Overrides
|
|
// Keyed by the stage index and collectible flag of the item
|
|
if (location->HasCategories("Freestanding Item")) {
|
|
for (const auto& freestandingItemNode: metaData["Freestanding Item"]) {
|
|
u8 stage = freestandingItemNode["Stage"].as<u8>();
|
|
u8 flag = freestandingItemNode["Flag"].as<u8>();
|
|
u8 itemId = location->GetCurrentItem()->GetID();
|
|
u16 key = (stage << 8) | flag;
|
|
randoData.mFreestandingItemOverrides[key] = itemId;
|
|
}
|
|
}
|
|
|
|
// Bug Rewards
|
|
// Keyed by the item id of the original bug
|
|
if (location->HasCategories("Bug Reward")) {
|
|
for (const auto& bugRewardNode : metaData["Bug Reward"]) {
|
|
u8 bugItemId = bugRewardNode["Item Id"].as<u8>();
|
|
u8 itemId = location->GetCurrentItem()->GetID();
|
|
randoData.mBugRewardOverrides[bugItemId] = itemId;
|
|
}
|
|
}
|
|
|
|
// Sky Characters
|
|
// Keyed by u16 of 0xFF00 (stage index) and 0x00FF (roomNo)
|
|
if (location->HasCategories("Sky Character")) {
|
|
for (const auto& skyCharacterNode : metaData["Sky Character"]) {
|
|
u8 stageIdx = skyCharacterNode["Stage"].as<u8>();
|
|
u8 roomNo = skyCharacterNode["Room"].as<u8>();
|
|
u8 itemId = location->GetCurrentItem()->GetID();
|
|
u16 key = (stageIdx << 8) | roomNo;
|
|
randoData.mSkyCharacterOverrides[key] = itemId;
|
|
}
|
|
}
|
|
|
|
// Golden Wolves
|
|
// Keyed by u16 of the event flag for obtaining the golden wolf item
|
|
if (location->HasCategories("Golden Wolf")) {
|
|
for (const auto& goldenWolfNode : metaData["Golden Wolf"]) {
|
|
u16 flag = goldenWolfNode["Flag"].as<u16>();
|
|
u8 itemId = location->GetCurrentItem()->GetID();
|
|
randoData.mGoldenWolfOverrides[flag] = itemId;
|
|
}
|
|
}
|
|
|
|
// Shop Items
|
|
// Keyed by u16 of the stage and original shop item
|
|
if (location->HasCategories("Shop") && world->Setting("Shop Items") == "On") {
|
|
for (const auto& shopNode : metaData["Shop"]) {
|
|
u8 stage = shopNode["Stage"].as<u8>();
|
|
u8 originalItem = shopNode["Item"].as<u8>();
|
|
u16 key = (stage << 8) | originalItem;
|
|
randoData.mShopOverrides[key] = location->GetCurrentItem()->GetID();
|
|
}
|
|
}
|
|
|
|
// Twilit Insect Overrides
|
|
// Keyed by u16 of 0xFF00 (stage index) and 0x00FF (flag, which is a tbox id)
|
|
if (location->HasCategories("Twilit Insect")) {
|
|
for (const auto& twilitInsectNode : metaData["Twilit Insect"]) {
|
|
u8 stage = twilitInsectNode["Stage"].as<u8>();
|
|
u8 tboxId = twilitInsectNode["Flag"].as<u8>();
|
|
u16 itemId = location->GetCurrentItem()->GetID();
|
|
u16 key = (stage << 8) | tboxId;
|
|
randoData.mTwilitInsectOverrides[key] = itemId;
|
|
}
|
|
}
|
|
|
|
// Helper function for getting flag values
|
|
auto getNodeFlags = [](auto& itemData, const YAML::Node& metaData) {
|
|
if (metaData["Event Flag"]) {
|
|
itemData.flag = metaData["Event Flag"].as<u16>();
|
|
} else if (metaData["Switch Flag"]) {
|
|
itemData.stage = metaData["Switch Flag"]["Stage"].as<u8>();
|
|
itemData.flag = metaData["Switch Flag"]["Flag"].as<u8>();
|
|
} else if (metaData["Item Flag"]) {
|
|
itemData.stage = metaData["Item Flag"]["Stage"].as<u8>();
|
|
itemData.flag = metaData["Item Flag"]["Flag"].as<u8>();
|
|
}
|
|
};
|
|
|
|
// Items that we lookup just by calling their location name
|
|
if (location->HasCategories("Name Lookup")) {
|
|
for (const auto& locationNameNode : metaData["Name Lookup"]) {
|
|
const auto& locationName = nameLookupOverride(locationNameNode.as<std::string>());
|
|
const int itemId = location->GetCurrentItem()->GetID();
|
|
randoData.mItemLocations[locationName].itemId = itemId;
|
|
getNodeFlags(randoData.mItemLocations[locationName], metaData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set starting inventory
|
|
for (const auto& item: world->GetStartingItemPool()) {
|
|
randoData.mStartingInventory.push_back(item->GetID());
|
|
}
|
|
|
|
// Set starting flags
|
|
auto startFlags = LOAD_EMBED_YAML(RANDO_DATA_PATH "startflags.yaml");
|
|
// Event Flags
|
|
for (const auto& flagNode : startFlags["EventFlags"]) {
|
|
if (flagNode.IsScalar()) {
|
|
const auto& flag = flagNode.as<u16>();
|
|
randoData.mStartEventFlags.push_back(flag);
|
|
} else if (flagNode.IsMap()) {
|
|
const auto& condition = flagNode.begin()->first.as<std::string>();
|
|
if (world->EvaluateSettingCondition(condition)) {
|
|
for (const auto& conditionalFlag : flagNode.begin()->second) {
|
|
const auto& flag = conditionalFlag.as<u16>();
|
|
randoData.mStartEventFlags.push_back(flag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Region Flags
|
|
for (const auto& regionNode : startFlags["RegionFlags"]) {
|
|
const auto& region = regionNode.first.as<std::string>();
|
|
const auto& index = regionNode.second["Index"].as<int>();
|
|
const auto& flags = regionNode.second["Flags"];
|
|
// This seems kinda scuffed so maybe we change it later
|
|
for (const auto& flagNode : flags) {
|
|
if (flagNode.IsScalar()) {
|
|
const auto& flag = flagNode.as<int>();
|
|
randoData.mStartRegionFlags[index].push_back(flag);
|
|
} else if (flagNode.IsMap()) {
|
|
const auto& condition = flagNode.begin()->first.as<std::string>();
|
|
if (world->EvaluateSettingCondition(condition)) {
|
|
for (const auto& conditionalFlag : flagNode.begin()->second) {
|
|
const auto& flag = conditionalFlag.as<int>();
|
|
randoData.mStartRegionFlags[index].push_back(flag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (world->Setting("Unlock Map Regions") == "On")
|
|
{
|
|
auto& bits = randoData.mMapBits;
|
|
bits = 0x20;
|
|
if (world->Setting("Snowpeak Does Not Require Reekfish Scent") == "On") {bits |= 0x40;}
|
|
if (world->Setting("Lanayru Twilight Cleared") == "On") {bits |= 0x10;}
|
|
if (world->Setting("Eldin Twilight Cleared") == "On") {bits |= 0x08;}
|
|
if (world->Setting("Faron Twilight Cleared") == "On") {bits |= 0x04;}
|
|
if (world->Setting("Skip Prologue") == "On") {bits |= 0x02;}
|
|
}
|
|
|
|
// Set starting time of day
|
|
const auto startTimeSetting = world->Setting("Starting Time of Day");
|
|
if (startTimeSetting == "Morning")
|
|
randoData.mStartHour = 6;
|
|
else if (startTimeSetting == "Noon")
|
|
randoData.mStartHour = 12;
|
|
else if (startTimeSetting == "Evening")
|
|
randoData.mStartHour = 18;
|
|
else if (startTimeSetting == "Night")
|
|
randoData.mStartHour = 24;
|
|
|
|
// Actor Patches
|
|
std::unordered_map<std::string, u8> objectFlowGroups{};
|
|
auto actorPatches = LOAD_EMBED_YAML(RANDO_DATA_PATH "object_patches.yaml");
|
|
for (const auto& stageNode : actorPatches) {
|
|
const auto& stageName = stageNode.first.as<std::string>();
|
|
for (const auto& roomNode : stageNode.second) {
|
|
u8 roomNo{};
|
|
// Special value for actors always on the stage and not just one specific room
|
|
if (roomNode.first.as<std::string>() == "Stage") {
|
|
roomNo = RandomizerContext::ROOM_STAGE;
|
|
} else {
|
|
roomNo = roomNode.first.as<u8>();
|
|
}
|
|
for (const auto& objectNode : roomNode.second) {
|
|
const auto& action = objectNode["action"].as<std::string>();
|
|
|
|
// Get all the data for the actor (with endian shenanigans)
|
|
auto object = parseObjData(objectNode);
|
|
|
|
size_t objDataSize = RandomizerContext::TGSC_CRC_SIZE;
|
|
// If the scale of this object is all zeros, it's an ACTR
|
|
if (object.scale.x == 0 && object.scale.y == 0 && object.scale.z == 0) {
|
|
objDataSize = RandomizerContext::ACTR_CRC_SIZE;
|
|
}
|
|
|
|
// Create unique hash based off of actor data
|
|
u32 objectCRC32 = getStageObjCRC32(reinterpret_cast<u8*>(&object), objDataSize);
|
|
|
|
// Depending on the action, store data on this actor
|
|
RandomizerContext::ActorData actorData{};
|
|
if (objectNode["flow"]) {
|
|
actorData.flow = objectNode["flow"].as<std::string>();
|
|
const int stageId = getStageID(stageName.c_str());
|
|
if (stageId < 0 ||
|
|
static_cast<size_t>(stageId) >= std::size(allStageMessageGroups))
|
|
{
|
|
throw std::runtime_error("Unknown stage for flow-bearing actor: " + stageName);
|
|
}
|
|
const u8 group = allStageMessageGroups[stageId];
|
|
const auto [found, inserted] = objectFlowGroups.emplace(actorData.flow, group);
|
|
if (!inserted && found->second != group) {
|
|
throw std::runtime_error(
|
|
"Actor flow is referenced from multiple message groups: " + actorData.flow);
|
|
}
|
|
object.base.angle.x = 0;
|
|
}
|
|
// If we're patching this object, Then override the object with whatever parts are being patched
|
|
// and add that patch data to our actorData
|
|
if (action == "patch") {
|
|
parseObjPatchData(object, objectNode["patch"]);
|
|
actorData.bytes.resize(objDataSize);
|
|
std::memcpy(actorData.bytes.data(), &object, objDataSize);
|
|
} else if (action == "add") {
|
|
// If we're adding the object, add it's regular data to the actorData
|
|
actorData.bytes.resize(objDataSize);
|
|
std::memcpy(actorData.bytes.data(), &object, objDataSize);
|
|
} else if (action == "delete") {
|
|
// If we're deleting this actor, give it a specific size to indicate we're deleting it
|
|
actorData.bytes.resize(RandomizerContext::OBJ_DELETE_SIZE);
|
|
} else {
|
|
// Unknown action. Don't continue
|
|
throw std::runtime_error("object patch action \"" + action + "\" not recognized");
|
|
}
|
|
|
|
// Loop through all of our layers to apply this action to
|
|
for (const auto& layerNode : objectNode["layers"]) {
|
|
u8 layerNo = layerNode.as<u8>();
|
|
// Create key based off of stage index, room, and layer
|
|
u32 stageRoomLayerKey{};
|
|
stageRoomLayerKey |= getStageID(stageName.c_str()) << 16;
|
|
stageRoomLayerKey |= roomNo << 8;
|
|
stageRoomLayerKey |= layerNo;
|
|
|
|
if (action == "add") {
|
|
randoData.mObjectAdditions[stageRoomLayerKey].push_back(actorData);
|
|
} else { // patch or delete
|
|
randoData.mObjectPatches[stageRoomLayerKey][objectCRC32] = actorData;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto source_reference = [](const YAML::Node& node) {
|
|
RandomizerContext::FlowReference reference{};
|
|
const auto value = node.as<std::string>();
|
|
if (const auto numeric = randomizer::utility::str::toInt(value); numeric.has_value()) {
|
|
if (numeric.value() < 0 || numeric.value() > 0xffff) {
|
|
throw std::runtime_error("Flow reference is outside the 16-bit range: " + value);
|
|
}
|
|
reference.nativeId = static_cast<u16>(numeric.value());
|
|
} else {
|
|
reference.name = value;
|
|
}
|
|
return reference;
|
|
};
|
|
|
|
std::unordered_map<std::string, std::unordered_set<u8>> customMessageGroups{};
|
|
std::unordered_map<std::string, std::string> splitMessageStyles{};
|
|
auto flowPatches = LOAD_EMBED_YAML(RANDO_DATA_PATH "flow_patches.yaml");
|
|
for (const auto& groupNode : flowPatches) {
|
|
const auto groupName = groupNode.first.as<std::string>();
|
|
const bool customSection = groupName == "custom";
|
|
const u8 defaultGroup = customSection ? 0 : groupNode.first.as<u8>();
|
|
for (const auto& flowNode : groupNode.second) {
|
|
if (flowNode["only if"] &&
|
|
!world->EvaluateSettingCondition(flowNode["only if"].as<std::string>()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
RandomizerContext::FlowNode flow{};
|
|
flow.type = parse_flow_node_type(flowNode["type"]);
|
|
|
|
std::vector<u16> patchIndices{};
|
|
if (customSection) {
|
|
flow.name = flowNode["name"].as<std::string>();
|
|
if (flowNode["group"]) {
|
|
flow.group = flowNode["group"].as<u8>();
|
|
} else if (const auto found = objectFlowGroups.find(flow.name);
|
|
found != objectFlowGroups.end())
|
|
{
|
|
flow.group = found->second;
|
|
} else {
|
|
throw std::runtime_error("Custom flow has no message group: " + flow.name);
|
|
}
|
|
} else {
|
|
flow.group = defaultGroup;
|
|
if (flowNode["index"].IsSequence()) {
|
|
for (const auto& index : flowNode["index"]) {
|
|
patchIndices.push_back(index.as<u16>());
|
|
}
|
|
} else {
|
|
patchIndices.push_back(flowNode["index"].as<u16>());
|
|
}
|
|
}
|
|
|
|
if (flow.type == RandomizerContext::FlowNodeType::BRANCH) {
|
|
flow.parameters = flowNode["parameters"].as<u32>();
|
|
flow.operation = flowNode["query"].as<std::string>();
|
|
for (const auto& result : flowNode["results"]) {
|
|
flow.results.push_back(source_reference(result));
|
|
}
|
|
if (flow.parameters > 0xffff || flow.results.empty() ||
|
|
flow.results.size() > 0xff)
|
|
{
|
|
throw std::runtime_error("Flow branch has invalid parameters or results");
|
|
}
|
|
} else if (flow.type == RandomizerContext::FlowNodeType::EVENT) {
|
|
flow.parameters = flowNode["parameters"].as<u32>();
|
|
flow.operation = flowNode["event"].as<std::string>();
|
|
flow.next = source_reference(flowNode["next"]);
|
|
} else {
|
|
flow.message = source_reference(flowNode["message"]);
|
|
flow.next = source_reference(flowNode["next"]);
|
|
if (!flow.message.nativeId.has_value()) {
|
|
customMessageGroups[flow.message.name].insert(flow.group);
|
|
if (world->GetTextDatabase().contains(flow.message.name)) {
|
|
auto& text = world->GetTextObject(flow.message.name);
|
|
// The game still owns textbox pagination, so preserve the existing
|
|
// per-message limit while representing overflow as ordinary flow nodes.
|
|
const auto extraText = text.SplitToFitTextLimits();
|
|
if (!extraText.empty()) {
|
|
const auto originalNext = flow.next;
|
|
std::vector<std::string> extraNames{};
|
|
for (size_t i = 0; i < extraText.size(); ++i) {
|
|
auto extraName = flow.message.name + std::to_string(i + 1);
|
|
world->AddNewText(extraName) = extraText[i];
|
|
splitMessageStyles[extraName] = flow.message.name;
|
|
customMessageGroups[extraName].insert(flow.group);
|
|
extraNames.push_back(std::move(extraName));
|
|
}
|
|
flow.next = {.name = extraNames.front()};
|
|
for (size_t i = 0; i < extraNames.size(); ++i) {
|
|
RandomizerContext::FlowNode extraFlow{
|
|
.type = RandomizerContext::FlowNodeType::MESSAGE,
|
|
.group = flow.group,
|
|
.name = extraNames[i],
|
|
.message = {.name = extraNames[i]},
|
|
.next = i + 1 < extraNames.size() ?
|
|
RandomizerContext::FlowReference{
|
|
.name = extraNames[i + 1]} :
|
|
originalNext,
|
|
};
|
|
randoData.mFlowNodes.push_back(std::move(extraFlow));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (customSection) {
|
|
randoData.mFlowNodes.push_back(std::move(flow));
|
|
} else {
|
|
for (const u16 patchIndex : patchIndices) {
|
|
auto patch = flow;
|
|
patch.patchIndex = patchIndex;
|
|
randoData.mFlowNodes.push_back(std::move(patch));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::array<std::unordered_set<std::string>, 9> flowNames{};
|
|
for (const auto& flow : randoData.mFlowNodes) {
|
|
if (flow.group >= flowNames.size()) {
|
|
throw std::runtime_error("Flow node has an invalid message group");
|
|
}
|
|
if (!flow.patchIndex.has_value() &&
|
|
(flow.name.empty() || !flowNames[flow.group].insert(flow.name).second))
|
|
{
|
|
throw std::runtime_error("Custom flow name is empty or duplicated: " + flow.name);
|
|
}
|
|
}
|
|
auto validate_flow_reference = [&](u8 group, const RandomizerContext::FlowReference& reference) {
|
|
if (!reference.nativeId.has_value() &&
|
|
(reference.name.empty() || !flowNames[group].contains(reference.name)))
|
|
{
|
|
throw std::runtime_error(fmt::format(
|
|
"Unresolved flow reference in group {}: {}", group, reference.name));
|
|
}
|
|
};
|
|
for (const auto& [name, group] : objectFlowGroups) {
|
|
validate_flow_reference(group, {.name = name});
|
|
}
|
|
for (const auto& flow : randoData.mFlowNodes) {
|
|
if (flow.type == RandomizerContext::FlowNodeType::BRANCH) {
|
|
for (const auto& result : flow.results) {
|
|
validate_flow_reference(flow.group, result);
|
|
}
|
|
} else {
|
|
validate_flow_reference(flow.group, flow.next);
|
|
}
|
|
}
|
|
|
|
auto parse_style = [](const YAML::Node& node) {
|
|
RandomizerContext::MessageStyleData style{};
|
|
if (!node) {
|
|
return style;
|
|
}
|
|
auto set = [&](const char* key, auto& field) {
|
|
if (node[key]) {
|
|
field = node[key].as<std::remove_reference_t<decltype(field)>>();
|
|
}
|
|
};
|
|
set("Event Label", style.eventLabelId);
|
|
set("Speaker", style.speaker);
|
|
set("Box Kind", style.boxKind);
|
|
set("Draw Type", style.drawType);
|
|
set("Box Position", style.boxPosition);
|
|
set("Line Alignment", style.lineAlignment);
|
|
set("Speaker Mood", style.speakerMood);
|
|
set("Camera", style.cameraAttr);
|
|
set("Talk Animation", style.talkAnim);
|
|
set("Face Animation", style.faceAnim);
|
|
set("Trailing Data", style.trailingData);
|
|
return style;
|
|
};
|
|
|
|
auto textOverrides = LOAD_EMBED_YAML(RANDO_DATA_PATH "text/text_overrides.yaml");
|
|
std::unordered_map<std::string, YAML::Node> textDefinitions{};
|
|
for (const auto& overrideNode : textOverrides) {
|
|
const auto name = overrideNode["Name"].as<std::string>();
|
|
textDefinitions.emplace(name, overrideNode);
|
|
if (!overrideNode["Message Id"] ||
|
|
(overrideNode["Only If"] &&
|
|
!world->EvaluateSettingCondition(overrideNode["Only If"].as<std::string>())))
|
|
{
|
|
continue;
|
|
}
|
|
const u8 group = overrideNode["Group"].as<u8>();
|
|
const u16 messageId = overrideNode["Message Id"].as<u16>();
|
|
const u32 key = static_cast<u32>(group) << 16 | messageId;
|
|
for (const auto language : randomizer::supportedLanguages) {
|
|
std::string text = world->GetTextDatabase().contains(name) ?
|
|
world->GetText(name, randomizer::Text::STANDARD, language) :
|
|
randomizer::getTextStr(name, randomizer::Text::STANDARD, language);
|
|
randomizer::applyMessageCodes(text);
|
|
randoData.mTextOverrides[language][key] = std::move(text);
|
|
}
|
|
}
|
|
|
|
for (const auto& [name, groups] : customMessageGroups) {
|
|
const auto styleName = splitMessageStyles.contains(name) ? splitMessageStyles[name] : name;
|
|
const auto definition = textDefinitions.find(styleName);
|
|
if (definition == textDefinitions.end()) {
|
|
throw std::runtime_error("Custom flow message has no text definition: " + styleName);
|
|
}
|
|
for (const u8 group : groups) {
|
|
RandomizerContext::CustomMessage message{
|
|
.group = group,
|
|
.name = name,
|
|
.style = parse_style(definition->second["Style"]),
|
|
};
|
|
for (const auto language : randomizer::supportedLanguages) {
|
|
std::string text = world->GetTextDatabase().contains(name) ?
|
|
world->GetText(name, randomizer::Text::STANDARD, language) :
|
|
randomizer::getTextStr(name, randomizer::Text::STANDARD, language);
|
|
randomizer::applyMessageCodes(text);
|
|
message.text[language] = std::move(text);
|
|
}
|
|
randoData.mCustomMessages.push_back(std::move(message));
|
|
}
|
|
}
|
|
|
|
// Vanilla Return to Place Overrides. Will need to change when boss/miniboss ER is implemented
|
|
static const std::vector<std::pair<std::vector<int>, RandomizerContext::EntranceOverride>> defaultPlaceOverrides{
|
|
{{Forest_Temple, Ook, Diababa}, {.stageId = Forest_Temple, .roomNo = 22, .mapLayer = -1, .pointNo = 0}},
|
|
{{Goron_Mines, Dangoro, Fyrus}, {.stageId = Goron_Mines, .roomNo = 1, .mapLayer = -1, .pointNo = 0}},
|
|
{{Lakebed_Temple, Deku_Toad, Morpheel}, {.stageId = Lakebed_Temple, .roomNo = 0, .mapLayer = -1, .pointNo = 0}},
|
|
{{Arbiters_Grounds, Death_Sword, Stallord}, {.stageId = Arbiters_Grounds, .roomNo = 0, .mapLayer = -1, .pointNo = 0}},
|
|
{{Snowpeak_Ruins, Darkhammer, Blizzeta}, {.stageId = Snowpeak_Ruins, .roomNo = 0, .mapLayer = -1, .pointNo = 0}},
|
|
{{Temple_of_Time, Darknut, Armogohma}, {.stageId = Temple_of_Time, .roomNo = 0, .mapLayer = -1, .pointNo = 0}},
|
|
{{City_in_the_Sky, Aeralfos, Argorok}, {.stageId = City_in_the_Sky, .roomNo = 0, .mapLayer = -1, .pointNo = 3}},
|
|
{{Palace_of_Twilight, Phantom_Zant_1,
|
|
Phantom_Zant_2, Zant_Main_Room, Zant_Fight}, {.stageId = Palace_of_Twilight, .roomNo = 0, .mapLayer = -1, .pointNo = 0}},
|
|
{{Hyrule_Castle, Ganondorf_Castle, Ganondorf_Field}, {.stageId = Hyrule_Castle, .roomNo = 11, .mapLayer = -1, .pointNo = 0}},
|
|
};
|
|
|
|
// Return to Place Overrides
|
|
for (const auto& [stages, returnPlace] : defaultPlaceOverrides) {
|
|
for (auto stage : stages) {
|
|
randoData.mReturnToPlaceOverrides[stage] = returnPlace;
|
|
}
|
|
}
|
|
|
|
// Apply entrance randomization
|
|
if (world->AnyEntranceRandomizerEnabled()) {
|
|
for (const auto& entrance : world->GetShuffledEntrances()) {
|
|
randomizer::logic::entrance::Entrance* replacesEntrance = entrance->GetReplaces();
|
|
RandomizerContext::EntranceOverride forward = {.stageId = entrance->GetStageId(), .roomNo = entrance->GetRoomNo(), .mapLayer = entrance->GetLayerNo(), .pointNo = entrance->GetPointNo()};
|
|
RandomizerContext::EntranceOverride replaces = {.stageId = replacesEntrance->GetStageId(), .roomNo = replacesEntrance->GetRoomNo(), .mapLayer = replacesEntrance->GetLayerNo(), .pointNo = replacesEntrance->GetPointNo()};
|
|
if (forward.stageId == 0xFF || replaces.stageId == 0xFF || forward.roomNo == -1 || replaces.roomNo == -1) {
|
|
continue;
|
|
}
|
|
randoData.mEntranceOverrides[forward] = replaces;
|
|
|
|
// Set overrides for all coupled entrances
|
|
for (const auto& point : entrance->getCoupledEntrances()) {
|
|
RandomizerContext::EntranceOverride coupled = {.stageId = entrance->GetStageId(), .roomNo = entrance->GetRoomNo(), .mapLayer = entrance->GetLayerNo(), .pointNo = point};
|
|
randoData.mEntranceOverrides[coupled] = replaces;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (world->Setting("Mirror Chamber Access") == "Closed") {
|
|
// Set exiting the Arbiter's Grounds Boss Room to spawn at the Arbiter's Grounds entrance
|
|
// if mirror chamber access is closed
|
|
RandomizerContext::EntranceOverride original = {
|
|
.stageId = StageIDs::Mirror_Chamber,
|
|
.roomNo = 4,
|
|
.mapLayer = -1,
|
|
.pointNo = 0,
|
|
};
|
|
|
|
RandomizerContext::EntranceOverride override = {
|
|
.stageId = StageIDs::Bulblin_Camp,
|
|
.roomNo = 3,
|
|
.mapLayer = -1,
|
|
.pointNo = 3,
|
|
};
|
|
|
|
// Check if we are already overriding the bulblin camp entrance, and correctly override the entrance
|
|
const auto& it = randoData.mEntranceOverrides.find(override);
|
|
randoData.mEntranceOverrides[original] = (it != randoData.mEntranceOverrides.end()) ? it->second : override;
|
|
}
|
|
|
|
return std::move(randoData);
|
|
}
|
|
|
|
static void DeleteFailedGenerationFiles(randomizer::Randomizer& rando) {
|
|
// If the hash is empty, then we never generated any files
|
|
if (!rando.GetConfig().GetHash().empty()) {
|
|
std::filesystem::remove_all(rando.GetSeedOutputPath());
|
|
}
|
|
}
|
|
|
|
bool GenerateAndWriteSeed(std::string& generationStatusMsg) {
|
|
auto r = randomizer::Randomizer{::randomizer::paths::GetRandomizerPath()};
|
|
|
|
auto generationResult = r.Generate();
|
|
if (generationResult.has_value()) {
|
|
generationStatusMsg = fmt::format("Failed to generate seed. Reason:\n{}", generationResult.value());
|
|
DeleteFailedGenerationFiles(r);
|
|
return false;
|
|
}
|
|
|
|
const auto world = r.GetWorld();
|
|
RandomizerContext randoData{};
|
|
try {
|
|
randoData = WriteSeedData(world);
|
|
} catch (const std::runtime_error& e) {
|
|
generationStatusMsg =
|
|
fmt::format("Failed to write seed data. Reason:\n{}", e.what());
|
|
DeleteFailedGenerationFiles(r);
|
|
return false;
|
|
}
|
|
|
|
randoData.mHash = r.GetConfig().GetHash();
|
|
auto writeToFileResult = randoData.WriteToFile();
|
|
if (writeToFileResult.has_value()) {
|
|
generationStatusMsg =
|
|
fmt::format("Failed to write seed data to file. Reason:\n{}", writeToFileResult.value());
|
|
DeleteFailedGenerationFiles(r);
|
|
return false;
|
|
}
|
|
|
|
generationStatusMsg = fmt::format("Seed generated! Hash: {}", randoData.mHash);
|
|
return true;
|
|
}
|