mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-05-23 22:45:05 -04:00
1316 lines
48 KiB
C++
1316 lines
48 KiB
C++
#include "randomizer_context.hpp"
|
|
|
|
#include "dusk/app_info.hpp"
|
|
#include "dusk/logging.h"
|
|
#include "dusk/main.h"
|
|
#include "dusk/data.hpp"
|
|
#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/endian.hpp"
|
|
#include "dusk/randomizer/generator/utility/yaml.hpp"
|
|
#include "dusk/randomizer/generator/randomizer.hpp"
|
|
#include "dusk/randomizer/generator/utility/text.hpp"
|
|
|
|
#include "SDL3/SDL_filesystem.h"
|
|
#include <zlib.h>
|
|
|
|
#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 "flags.h"
|
|
|
|
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{};
|
|
|
|
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;
|
|
|
|
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 (const auto& [key, text] : this->mTextOverrides) {
|
|
textData << YAML::Key << key;
|
|
textData << YAML::Value << YAML::Binary(reinterpret_cast<const unsigned char*>(text.data()), text.size());
|
|
}
|
|
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;
|
|
}
|
|
|
|
auto in = LoadYAML(this->GetSeedDataPath());
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Helper function for getting the item data out of a YAML node
|
|
auto retrieveItemData = [](auto& itemData, auto& 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& textNode: in["mTextOverrides"]) {
|
|
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[key] = std::move(text);
|
|
}
|
|
|
|
DuskLog.debug("Loaded Randomizer Seed {}", this->mHash);
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::filesystem::path RandomizerContext::GetSeedDataPath() const {
|
|
return dusk::data::configured_data_path() / "randomizer" / "seeds" / 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},
|
|
{"Skip Minor Cutscenes", SKIP_MINOR_CUTSCENES},
|
|
{"Skip Major Cutscenes", SKIP_MAJOR_CUTSCENES},
|
|
{"Temple of Time Sword Requirement", TEMPLE_OF_TIME_SWORD_REQUIREMENT},
|
|
};
|
|
|
|
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 = sceneMgr->getLoadedSeWave_1();
|
|
const u32 seWave2 = sceneMgr->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();
|
|
}
|
|
|
|
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(0x1);
|
|
}
|
|
|
|
// 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;
|
|
} else {
|
|
g_randomizerState.mTrackerTempSwitchFlag.stage = getStageSaveId(data.stage);
|
|
g_randomizerState.mTrackerTempSwitchFlag.flag = data.flag;
|
|
}
|
|
}
|
|
|
|
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 crc32(0, (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();
|
|
}
|
|
}
|
|
|
|
// Helper function for getting flag values
|
|
auto getNodeFlags = [](auto& itemData, auto& 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>();
|
|
}
|
|
};
|
|
|
|
// 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>();
|
|
// TODO: Handle multiple languages
|
|
auto language = randomizer::Text::ENGLISH;
|
|
auto text = randomizer::getTextStr(name);
|
|
u8 group = overrideNode["Group"].as<u8>();
|
|
u16 messageId = overrideNode["Message Id"].as<u16>();
|
|
u32 key = (group << 16) | messageId;
|
|
randomizer::applyMessageCodes(text);
|
|
randoData.mTextOverrides[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());
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Generates a seed and writes the necessary seed files to the players seed directory
|
|
*/
|
|
void GenerateAndWriteSeed(std::string& generationStatusMsg) {
|
|
auto r = randomizer::Randomizer{dusk::data::configured_data_path()};
|
|
|
|
auto generationResult = r.Generate();
|
|
if (generationResult.has_value()) {
|
|
generationStatusMsg = fmt::format("Seed Generation failed. Reason:\n{}", generationResult.value());
|
|
DeleteFailedGenerationFiles(r);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
generationStatusMsg = fmt::format("Seed generated! Hash: {}", randoData.mHash);
|
|
} |