mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-06-24 15:43:13 -04:00
6366ab61d1
For the most part, solo archipelago runs seem to work fine now using the original TP apworld, but im sure there are plenty of weird issues still that need to be handled.
1376 lines
51 KiB
C++
1376 lines
51 KiB
C++
#include "randomizer_context.hpp"
|
|
|
|
#include "dusk/logging.h"
|
|
#include "dusk/main.h"
|
|
#include "dusk/data.hpp"
|
|
#include "dusk/ui/rando_config.hpp"
|
|
#include "dusk/randomizer/game/flags.h"
|
|
#include "dusk/randomizer/game/tools.h"
|
|
#include "dusk/randomizer/game/stages.h"
|
|
#include "dusk/randomizer/game/verify_item_functions.h"
|
|
#include "dusk/randomizer/generator/utility/crc32.hpp"
|
|
#include "dusk/randomizer/generator/utility/endian.hpp"
|
|
#include "dusk/randomizer/generator/utility/yaml.hpp"
|
|
#include "dusk/randomizer/generator/randomizer.hpp"
|
|
#include "dusk/randomizer/generator/utility/text.hpp"
|
|
|
|
#include <fstream>
|
|
|
|
#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_flow.h"
|
|
#include "dusk/archipelago/archipelago_context.hpp"
|
|
|
|
std::optional<std::string> RandomizerContext::WriteToFile() {
|
|
return WriteToFile(this->GetSeedDataPath());
|
|
}
|
|
|
|
std::optional<std::string> RandomizerContext::WriteToFile(const fspath& path) {
|
|
std::ofstream seedData(path);
|
|
if (!seedData.is_open()) {
|
|
return "Could not open seed data file";
|
|
}
|
|
|
|
YAML::Node out{};
|
|
|
|
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& [key, data] : this->mFlowItemMessageOverrides) {
|
|
auto node = out["mFlowItemMessageOverrides"][key];
|
|
node["itemId"] = data.itemId;
|
|
node["stage"] = data.stage;
|
|
node["flag"] = data.flag;
|
|
}
|
|
|
|
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) {
|
|
out["mObjectPatches"][stageRoomLayer][actorCRC] = ContainerToHexString(actorPatch);
|
|
}
|
|
}
|
|
|
|
for (const auto& [stageRoomLayer, newActors] : this->mObjectAdditions) {
|
|
for (const auto& actor : newActors) {
|
|
out["mObjectAdditions"][stageRoomLayer].push_back(ContainerToHexString(actor));
|
|
}
|
|
}
|
|
|
|
|
|
out["mFlowPatches"] = this->mFlowPatches;
|
|
|
|
// 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;
|
|
|
|
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())) {
|
|
DuskLog.error("Failed to load Hash: {}", hash);
|
|
mHash.clear();
|
|
return std::nullopt;
|
|
}
|
|
|
|
return LoadFromPath(this->GetSeedDataPath());
|
|
}
|
|
|
|
std::optional<std::string> RandomizerContext::LoadFromPath(const fspath& path) {
|
|
auto in = LoadYAML(path);
|
|
|
|
// 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>();
|
|
};
|
|
|
|
// FLW Override items
|
|
for (const auto& flwNode : in["mFlowItemMessageOverrides"]) {
|
|
u32 key = flwNode.first.as<u32>();
|
|
retrieveItemData(this->mFlowItemMessageOverrides[key], flwNode.second);
|
|
}
|
|
|
|
// Items we call by location name
|
|
for (const auto& locationNode : in["mItemLocations"]) {
|
|
const auto& locationName = 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>();
|
|
this->mObjectPatches[stageRoomLayer][actorCRC] = HexToBytes(actorPatchNode.second.as<std::string>());
|
|
}
|
|
}
|
|
|
|
// Object Additions
|
|
for (const auto& stageNode: in["mObjectAdditions"]) {
|
|
u32 stageRoomLayer = stageNode.first.as<u32>();
|
|
for (const auto& objectData : stageNode.second) {
|
|
this->mObjectAdditions[stageRoomLayer].emplace_back(HexToBytes(objectData.as<std::string>()));
|
|
}
|
|
}
|
|
|
|
// Flow Patches
|
|
for (const auto& flowNode: in["mFlowPatches"]) {
|
|
auto key = flowNode.first.as<u32>();
|
|
auto value = flowNode.second.as<u64>();
|
|
this->mFlowPatches[key] = value;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
dusk::ui::push_toast(dusk::ui::Toast{
|
|
.title = "Randomizer",
|
|
.content = fmt::format("Loaded Randomizer Seed {}", this->mHash),
|
|
.duration = std::chrono::seconds(3),
|
|
});
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::filesystem::path RandomizerContext::GetSeedDataPath() const {
|
|
return dusk::ui::GetRandomizerSeedsPath() / this->mHash / "seed.dat";
|
|
}
|
|
|
|
int RandomizerContext::SettingToEnum(const std::string& settingName) {
|
|
static const std::unordered_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},
|
|
};
|
|
|
|
if (nameToEnum.contains(settingName)) {
|
|
return nameToEnum.at(settingName);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
int RandomizerContext::OptionToEnum(const std::string& optionName) {
|
|
static const std::unordered_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},
|
|
};
|
|
|
|
if (nameToEnum.contains(optionName)) {
|
|
return nameToEnum.at(optionName);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
RandomizerState g_randomizerState;
|
|
|
|
int RandomizerState::_create() {
|
|
mInitialized = true;
|
|
mEventItemStatus = QUEUE_EMPTY;
|
|
mHasPendingToDChange = false;
|
|
// g_customMenuRing._initialize();
|
|
for (int i = 0; i < EVENT_ITEM_QUEUE_SIZE; i++) {
|
|
mEventItemQueue[i] = 0;
|
|
}
|
|
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()->getZButtonAlpha() != 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 = mDoAud_getZelAudio().getLoadedSeWave_1();
|
|
const u32 seWave2 = mDoAud_getZelAudio().getLoadedSeWave_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 Twlight 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();
|
|
// Handle giving item to the player at any time.
|
|
initGiveItemToPlayer();
|
|
}
|
|
else {
|
|
currentReloadingState = true;
|
|
}
|
|
|
|
bool prevReloadingState = getRoomReloadingState();
|
|
if (!currentReloadingState) {
|
|
if (prevReloadingState) {
|
|
offLoad();
|
|
}
|
|
}
|
|
setRoomReloadingState(currentReloadingState);
|
|
|
|
if (getStageID() != Title_Screen) {
|
|
handleFoolishItem();
|
|
}
|
|
|
|
dusk::archi::ArchipelagoContext::Execute();
|
|
|
|
return 1;
|
|
}
|
|
|
|
int RandomizerState::draw() {
|
|
return 1;
|
|
}
|
|
|
|
void RandomizerState::handlePoeItem(u8 bitSw)
|
|
{
|
|
u16 key = getStageID() << 8 | bitSw;
|
|
u8 item = randomizer_GetContext().mPoeOverrides[key];
|
|
addItemToEventQueue(item);
|
|
daAlink_getAlinkActorClass()->procWolfAtnActorMoveInit();
|
|
}
|
|
|
|
void RandomizerState::addItemToEventQueue(u8 item)
|
|
{
|
|
for (int i = 0; i < EVENT_ITEM_QUEUE_SIZE; i++)
|
|
{
|
|
if (mEventItemQueue[i] == 0)
|
|
{
|
|
mEventItemQueue[i] = item;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RandomizerState::initGiveItemToPlayer()
|
|
{
|
|
switch (daAlink_getAlinkActorClass()->mProcID)
|
|
{
|
|
case daAlink_c::PROC_WAIT:
|
|
case daAlink_c::PROC_TIRED_WAIT:
|
|
case daAlink_c::PROC_MOVE:
|
|
case daAlink_c::PROC_WOLF_WAIT:
|
|
case daAlink_c::PROC_WOLF_TIRED_WAIT:
|
|
case daAlink_c::PROC_WOLF_MOVE:
|
|
case daAlink_c::PROC_ATN_MOVE:
|
|
case daAlink_c::PROC_WOLF_ATN_AC_MOVE:
|
|
{
|
|
// Check if link is currently in a cutscene
|
|
if (daAlink_getAlinkActorClass()->checkEventRun())
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Ensure that link is not currently in a message-based event.
|
|
if (daAlink_getAlinkActorClass()->getEventId() != 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
u8 itemToGive = 0xFF;
|
|
|
|
for (int i = 0; i < EVENT_ITEM_QUEUE_SIZE; i++)
|
|
{
|
|
const u8 storedItem = mEventItemQueue[i];
|
|
|
|
if (storedItem)
|
|
{
|
|
const u8 giveItemToPlayerStatus = getGiveItemToPlayerStatus();
|
|
|
|
// If we have the call to clear the queue, then we want to clear the item and break out.
|
|
if (giveItemToPlayerStatus == CLEAR_QUEUE)
|
|
{
|
|
mEventItemQueue[i] = 0;
|
|
setGiveItemToPlayerStatus(QUEUE_EMPTY);
|
|
break;
|
|
}
|
|
|
|
// If the queue is empty and we have an item to give, update the queue state.
|
|
else if (giveItemToPlayerStatus == QUEUE_EMPTY)
|
|
{
|
|
setGiveItemToPlayerStatus(ITEM_IN_QUEUE);
|
|
}
|
|
|
|
itemToGive = verifyProgressiveItem(storedItem);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// if there is no item to give, break out of the case.
|
|
if (itemToGive == 0xFF)
|
|
{
|
|
break;
|
|
}
|
|
|
|
g_dComIfG_gameInfo.play.getEvent()->setGtItm(itemToGive);
|
|
|
|
// Set the process value for getting an item to start the "get item" cutscene when next available.
|
|
daAlink_getAlinkActorClass()->mProcID = daAlink_c::PROC_GET_ITEM;
|
|
|
|
// Get the event index for the "Get Item" event.
|
|
const s16 eventIdx = dComIfGp_getEventManager().getEventIdx((fopAc_ac_c*)daAlink_getAlinkActorClass(),"DEFAULT_GETITEM",0xFF);
|
|
|
|
// Finally we want to modify the event stack to prioritize our custom event so that it happens next.
|
|
fopAcM_orderChangeEventId(daAlink_getAlinkActorClass(), eventIdx, 1, 0xFFFF);
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
dComIfGp_setEnableNextStage();
|
|
}
|
|
}
|
|
|
|
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 dusk::IsGameLaunched && (!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[locationName].itemId;
|
|
}
|
|
|
|
static void randomizer_setTempFlag(RandomizerContext::itemLocationData data) {
|
|
// 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));
|
|
}
|
|
}
|
|
|
|
void randomizer_setTempFlagForLocation(const std::string& locationName) {
|
|
randomizer_setTempFlag(randomizer_GetContext().mItemLocations[locationName]);
|
|
}
|
|
|
|
void randomizer_setTempFlagForFLWOverride(u32 key) {
|
|
randomizer_setTempFlag(randomizer_GetContext().mFlowItemMessageOverrides[key]);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 |= dComIfGp_getLayerNo();
|
|
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()) {
|
|
// skip locations with nothing
|
|
if (location->GetCurrentItem()->GetID() == -1) {
|
|
continue;
|
|
}
|
|
|
|
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 determine the text of and then give during a FLW message
|
|
if (location->HasCategories("FLW Message")) {
|
|
for (const auto& flwMessageNode : metaData["FLW Message"]) {
|
|
u8 group = flwMessageNode["Group"].as<u8>();
|
|
u16 messageId = flwMessageNode["Message Id"].as<u16>();
|
|
u32 key = (group << 16) | messageId;
|
|
randoData.mFlowItemMessageOverrides[key].itemId = location->GetCurrentItem()->GetID();
|
|
getNodeFlags(randoData.mFlowItemMessageOverrides[key], metaData);
|
|
}
|
|
}
|
|
|
|
// 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 = 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)) {
|
|
DuskLog.debug("Setting flags for {}", 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"];
|
|
DuskLog.debug("Setting region flags for {}", region);
|
|
// 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
|
|
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
|
|
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
|
|
std::vector<u8> actorData(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.resize(objDataSize);
|
|
std::memcpy(actorData.data(), &object, objDataSize);
|
|
} else if (action == "add") {
|
|
// If we're adding the object, add it's regular data to the actorData
|
|
actorData.resize(objDataSize);
|
|
std::memcpy(actorData.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.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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flow Patches
|
|
auto flowPatches = LOAD_EMBED_YAML(RANDO_DATA_PATH "flow_patches.yaml");
|
|
for (const auto& groupNode : flowPatches) {
|
|
u8 groupNo = groupNode.first.as<u8>();
|
|
for (const auto& flowNode : groupNode.second) {
|
|
u16 index = flowNode["index"].as<u16>();
|
|
const auto& type = flowNode["type"].as<std::string>();
|
|
u64 value{};
|
|
if (type == "branch") {
|
|
auto branch = reinterpret_cast<mesg_flow_node_branch*>(&value);
|
|
branch->type = 2;
|
|
branch->field_0x1 = flowNode["num results"].as<u8>();
|
|
branch->query_idx = flowNode["query"].as<u16>();
|
|
branch->param = flowNode["parameters"].as<u16>();
|
|
branch->next_node_idx = flowNode["next node index"].as<u16>();
|
|
}
|
|
else if (type == "event") {
|
|
auto event = reinterpret_cast<mesg_flow_node_event*>(&value);
|
|
event->type = 3;
|
|
event->event_idx = flowNode["event"].as<u8>();
|
|
event->next_node_idx = flowNode["next node index"].as<u16>();
|
|
u32 params = flowNode["parameters"].as<u32>();
|
|
event->params[0] = (params >> 24) & 0xFF;
|
|
event->params[1] = (params >> 16) & 0xFF;
|
|
event->params[2] = (params >> 8) & 0xFF;
|
|
event->params[3] = params & 0xFF;
|
|
}
|
|
|
|
u32 key = (groupNo << 16) | index;
|
|
randoData.mFlowPatches[key] = value;
|
|
}
|
|
}
|
|
|
|
// Text Overrides
|
|
auto textOverrides = LOAD_EMBED_YAML(RANDO_DATA_PATH "text/text_overrides.yaml");
|
|
for (const auto& overrideNode : textOverrides) {
|
|
const auto& name = overrideNode["Name"].as<std::string>();
|
|
for (auto language : randomizer::supportedLanguages) {
|
|
std::string text;
|
|
if (world->GetTextDatabase().contains(name)) {
|
|
text = world->GetText(name, randomizer::Text::STANDARD, language);
|
|
} else {
|
|
text = randomizer::getTextStr(name, randomizer::Text::STANDARD, language);
|
|
}
|
|
u8 group = overrideNode["Group"].as<u8>();
|
|
u16 messageId = overrideNode["Message Id"].as<u16>();
|
|
u32 key = (group << 16) | messageId;
|
|
randomizer::applyMessageCodes(text);
|
|
randoData.mTextOverrides[language][key] = text;
|
|
}
|
|
}
|
|
|
|
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{dusk::ui::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;
|
|
} |