Files
dusklight/src/dusk/randomizer/game/randomizer_context.cpp
T

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);
}