Files
dusklight/src/dusk/archipelago/archipelago_context.cpp
T
CraftyBoss 7ee39edee7 add death link support, add needed archipelago item messages to language yamls
Archipelago items will still be green rupees for now until we decide how we want to handle custom game assets
2026-06-24 03:22:42 -07:00

950 lines
31 KiB
C++

#include <dusk/archipelago/archipelago_context.hpp>
#include <thread>
#include "Archipelago.h"
#include "d/d_item.h"
#include "d/actor/d_a_alink.h"
#include "dusk/config.hpp"
#include "dusk/logging.h"
#include "dusk/randomizer/game/tools.h"
#include "dusk/randomizer/game/verify_item_functions.h"
#include "dusk/randomizer/generator/logic/hints.hpp"
#include "dusk/ui/rando_config.hpp"
#include "dusk/ui/ui.hpp"
namespace dusk::archi
{
static constexpr int ARCHI_ITEM_OFFSET = 2320000;
struct SettingsNameConvert {
static constexpr std::string kDefaultYes = "On";
static constexpr std::string kDefaultNo = "Off";
std::string apName;
std::string dusklightName;
std::vector<std::pair<std::string, std::string>> optionsConvert;
const std::string& tryGetOptionConvert(const std::string& option) const {
if (optionsConvert.empty()) {
if (option == "Yes")
return kDefaultYes;
if (option == "No")
return kDefaultNo;
return option;
}
for (const auto& value : optionsConvert) {
if (value.first == option) {
return value.second;
}
}
return option;
}
};
static auto sArchiSettingToDusklight = std::to_array<SettingsNameConvert>({
{"", ""},
{"Golden Bugs Shuffled", "Golden Bugs"},
{"Sky Chracters Shuffled", "Sky Characters"},
{"NPC Items Shuffled", "Gifts From NPCs"},
{"Shop Items Shuffled", "Shop Items"},
{"Hidden Skills Shuffled", "Hidden Skills"},
{"Skip Prologue", "Skip Prologue"},
{"Faron Twilight Cleared", "Faron Twilight Cleared"},
{"Eldin Twilight Cleared", "Eldin Twilight Cleared"},
{"Lanayru Twilight Cleared", "Lanayru Twilight Cleared"},
{"Skip MDH", "Skip Midna's Desparate Hour"},
{"Open Map", "Unlock Map Regions"},
{"Increase Wallet", "Logic Increase Wallet Capacity"},
{"Transform Anywhere", "Logic Transform Anywhere"},
{"Bonks do Damage", "Bonks Do Damage"},
{"Lakebed Entrance Requirements", "Lakebed Does Not Require Water Bombs"},
{"Arbiters Grounds Entrance Requirements", "Arbiters Does Not Require Bulblin Camp"},
{"Snowpeak Entrance Requirements", "Snowpeak Does Not Require Reekfish Scent"},
{"City in the Sky Entrance Requirements", "City Does Not Require Filled Skybook"},
{"Goron Mines Entrance Requirements", "Goron Mines Entrance"},
{"Palace of Twilight Requirements", "Palace of Twilight Requirements"},
{"Faron Woods Logic", "Faron Woods Logic"},
{"Starting ToD", "Starting Time of Day"},
{"Skip Major Cutscenes", "Skip Major Cutscenes"},
{"Skip Minor Cutscenes", "Skip Minor Cutscenes"},
{"Open Door of Time", "Open Door of Time"},
{"Dungeon Rewards Progression", "Dungeon Rewards Can Be Anywhere", {
// these two are functionally identical in terms of tracker logic, so treat it as such
{"Anything", "On"},
{"Any Progressive", "On"},
{"Vanilla", "Off"},
}},
{"Small Key Settings", "Small Keys", {
{"Startwith", "Keysy"},
}},
{"Big Key Settings", "Big Keys", {
{"Startwith", "Keysy"},
}},
{"Map and Compass Settings", "Maps and Compasses", {
{"Startwith", "Start With"},
}},
{"Trap Frequency", "Trap Item Frequency", {
{"No Traps", "None"},
}},
{"Damage Magnification", "Logic Damage Multiplier", {
{"Ohko", "OHKO"},
}},
{"Logic Settings", "Logic Rules", {
{"Glitchless", "All Locations Reachable"},
{"Glitched", "Beatable Only"},
}},
{"Poes Shuffled", "Poe Souls", {
{"Yes", "All"},
{"No", "Vanilla"}
}}
});
ArchipelagoContext& instance() {
static ArchipelagoContext instance;
return instance;
}
const SettingsNameConvert& GetAPSettingNameConvert(const std::string& apSettingName) {
for (const auto& entry : sArchiSettingToDusklight) {
if (entry.apName == apSettingName)
return entry;
}
return sArchiSettingToDusklight[0];
}
const char* getMessageTypeName(AP_MessageType type) {
switch (type) {
case AP_MessageType::Plaintext:
return "Plaintext";
case AP_MessageType::ItemSend:
return "ItemSend";
case AP_MessageType::ItemRecv:
return "ItemRecv";
case AP_MessageType::Hint:
return "Hint";
case AP_MessageType::Countdown:
return "Countdown";
default:
return nullptr;
}
}
void ParseMessageData() {
auto msg = AP_GetLatestMessage();
switch (msg->type) {
case AP_MessageType::ItemSend: {
auto sendMsg = (AP_ItemSendMessage*)msg;
ui::push_toast({
.title = "Item Sent",
.content = fmt::format("Sent {} to {}", sendMsg->item, sendMsg->recvPlayer),
.duration = std::chrono::seconds(3),
});
DuskLog.info("[{}] {}", getMessageTypeName(msg->type), msg->text);
break;
}
case AP_MessageType::ItemRecv: {
auto recvMsg = (AP_ItemRecvMessage*)msg;
ui::push_toast({
.title = "Item Received",
.content = fmt::format("Got {} From {}", recvMsg->item, recvMsg->sendPlayer),
.duration = std::chrono::seconds(3),
});
// fallthrough for debug logging text contents
}
case AP_MessageType::Plaintext:
case AP_MessageType::Hint:
case AP_MessageType::Countdown:
DuskLog.info("[{}] {}", getMessageTypeName(msg->type), msg->text);
break;
default:
DuskLog.warn("Unknown message type! Type: {}", fmt::underlying(msg->type));
break;
}
AP_ClearLatestMessage();
}
void ArchipelagoContext::LoadTempItemInfo() {
auto itemDataTree = LOAD_EMBED_YAML(RANDO_DATA_PATH "items.yaml");
for (const auto& itemNode : itemDataTree) {
if (!itemNode["APItemId"]) {
DuskLog.warn("Item {} missing APItemId field!", itemNode["Name"].as<std::string>());
continue;
}
auto apItemId = itemNode["APItemId"].as<int>();
if (apItemId == -1)
continue;
auto id = itemNode["Id"].as<int>();
auto importance = randomizer::logic::item::ImportanceFromStr(itemNode["Importance"].as<std::string>());
auto itemName = itemNode["Name"].as<std::string>();
m_apItemToGameItem[apItemId] = {
id,
importance,
itemName
};
}
// add temporary replacement IDs for items not included in the base rando
m_apItemToGameItem[16] = { // Water Bombs (3)
0x16,
randomizer::logic::item::Importance::JUNK,
"Water Bombs 5"
};
m_apItemToGameItem[20] = { // Bomblings (3)
0x1A,
randomizer::logic::item::Importance::JUNK,
"Bomblings 5"
};
}
void ArchipelagoContext::LoadTempLocationInfo() {
auto locDataTree = LOAD_EMBED_YAML(RANDO_DATA_PATH "locations.yaml");
for (const auto& locNode : locDataTree) {
const auto& metadata = locNode["Metadata"];
auto locationName = locNode["Name"].as<std::string>();
if (!metadata.IsMap()) {
DuskLog.warn("Location {} missing correct Metadata field!", locationName);
continue;
}
if (!metadata["APLocationId"]) {
DuskLog.warn("Location {} missing APLocationId field!", locationName);
continue;
}
auto apLocationId = metadata["APLocationId"].as<int>();
if (apLocationId == -1)
continue;
m_apLocToGameLoc.push_back({
apLocationId,
locationName
});
}
}
void ArchipelagoContext::itemRecvImpl(int id, bool notify) {
if (!m_apItemToGameItem.contains(id)) {
DuskLog.warn("Got an invalid Item Id: {}", id);
return;
}
m_isAllowUpdateLocations = true; // guards against triggering UpdateCheckedLocations
auto& item = m_apItemToGameItem[id];
if (notify && item.importance == randomizer::logic::item::Importance::MAJOR) {
DuskLog.info("[AP] Adding Item: {}", item.itemName);
g_randomizerState.addItemToEventQueue(verifyProgressiveItem(item.itemId));
}else {
DuskLog.info("[AP] Silently Adding Item: {}", item.itemName);
execItemGet(item.itemId);
}
m_isAllowUpdateLocations = false;
}
int ArchipelagoContext::getItemIdFromApId(int apId) {
if (!m_apItemToGameItem.contains(apId)) {
DuskLog.warn("Got an invalid Item Id: {}", apId);
return -1;
}
auto& item = m_apItemToGameItem[apId];
return item.itemId;
}
std::string ArchipelagoContext::getLocationNameFromApId(int apId) const {
for (const auto& entry : m_apLocToGameLoc) {
if (entry.apId == apId)
return entry.locName;
}
return "";
}
bool ArchipelagoContext::tryKillPlayer() {
if (!m_isNeedPlayerDeath)
return false;
auto linkActor = daAlink_getAlinkActorClass();
if (!linkActor)
return false;
switch (linkActor->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 (linkActor->checkEventRun())
break;
// Ensure that link is not currently in a message-based event.
if (linkActor->getEventId() != 0)
break;
dComIfGs_setLife(0);
m_isNeedPlayerDeath = false;
return true;
}
default:
break;
}
return false;
}
ArchipelagoContext::ArchipelagoContext() = default;
void ArchipelagoContext::SetServerIp(const std::string_view& ip) {
getSettings().archipelago.serverIP.setValue(std::string(ip));
}
void ArchipelagoContext::SetSlotName(const std::string_view& name) {
getSettings().archipelago.slotName.setValue(std::string(name));
}
void ArchipelagoContext::SetPassword(const std::string_view& pass) {
getSettings().archipelago.serverPass.setValue(std::string(pass));
}
const std::string& ArchipelagoContext::GetServerIp() {
return getSettings().archipelago.serverIP.getValue();
}
const std::string& ArchipelagoContext::GetSlotName() {
return getSettings().archipelago.slotName.getValue();
}
const std::string& ArchipelagoContext::GetPassword() {
return getSettings().archipelago.serverPass.getValue();
}
std::string ArchipelagoContext::GetArchipelagoSeedName() {
if (IsConnected()) {
auto& roomInfo = instance().m_roomInfo;
return fmt::format("AP_{}", roomInfo.seed_name);
}else {
DuskLog.fatal("Archipelago was not connected when attempting to get seed name!");
}
}
void ArchipelagoContext::GetSeedDirectoryPath(std::filesystem::path& outPath) {
if (IsConnected()) {
auto& roomInfo = instance().m_roomInfo;
outPath = ui::GetRandomizerPath() / "archipelago" / GetArchipelagoSeedName();
}
}
bool ArchipelagoContext::IsSeedHashArchipelago(const std::string& seedStr) {
return seedStr.starts_with("AP_");
}
bool ArchipelagoContext::IsCurrentSeedHash(const std::string& seedStr) {
return GetArchipelagoSeedName() == seedStr;
}
bool ArchipelagoContext::ConnectToServer(bool isBlocking) {
config::Save();
instance().LoadTempItemInfo();
instance().LoadTempLocationInfo();
AP_SetLogCallback([](const std::string& msg) {
DuskLog.info("{}", msg);
});
AP_Init(GetServerIp().c_str(), "Twilight Princess", GetSlotName().c_str(), GetPassword().c_str());
AP_NetworkVersion ver{0, 6,7};
AP_SetClientVersion(&ver);
AP_SetDeathLinkSupported(true);
AP_SetDeathLinkRecvCallback([](std::string source, std::string cause) {
DuskLog.info("Player {} sent death link. Cause: {}", source, cause);
RequestPlayerDeath(true);
});
AP_SetItemClearCallback([]() {
DuskLog.info("Item Clear Callback Called!");
instance().m_isNeedResetInv = true;
});
AP_SetItemRecvCallback([](AP_NetworkItem& item, bool notify) {
DuskLog.debug("Item Receive Callback Called! Item: {} Notify: {}", item.item, notify);
HandleItemReceived(item, notify);
});
AP_SetLocationCheckedCallback([](int loc) {
DuskLog.info("Location Checked Callback Called! Location: {}", loc);
SetLocationChecked(loc, true);
});
AP_SetLocationInfoCallback([](std::vector<AP_NetworkItem> items) {
DuskLog.info("Got {} Location Scouts from Server.", items.size());
HandleReceiveLocationScout(items);
});
AP_RegisterSlotDataRawCallback("Settings", [](std::string data) {
DuskLog.info("Got Settings from Slot Data.");
instance().m_SettingsFile = data;
});
AP_RegisterSlotDataRawCallback("World Version", [](std::string data) {
DuskLog.info("TP APWorld Version: {}", data);
});
AP_Start();
// above func spawns a websocket thread, but there isn't really a good way to ensure a connection
// attempt has been made except to wait for that thread to tick once
if (isBlocking) {
// wait for ws thread to run a frame before checking for status
std::this_thread::sleep_for(std::chrono::seconds(1));
while (AP_GetConnectionStatus() == AP_ConnectionStatus::Connecting)
std::this_thread::yield();
if (!IsConnected()) {
DuskLog.error("Failed to connect to Archipelago Server!");
return false;
}
}
std::thread messageThread = std::thread(MessageThreadFunc);
messageThread.detach();
return true;
}
void ArchipelagoContext::DisconnectFromServer() {
AP_Shutdown();
}
bool ArchipelagoContext::IsConnected() {
auto status = AP_GetConnectionStatus();
return status == AP_ConnectionStatus::Connected || status == AP_ConnectionStatus::Authenticated;
}
void ArchipelagoContext::MessageThreadFunc() {
DuskLog.info("AP Thread started.");
if (IsConnected()) {
AP_GetRoomInfo(&instance().m_roomInfo);
instance().m_isEnableDeathLink = AP_IsDeathLinkEnabled();
RequestAllLocationScout();
}
while (IsConnected()) {
if (AP_IsMessagePending())
ParseMessageData();
}
DuskLog.info("AP Thread ended.");
}
void ArchipelagoContext::Execute() {
if (!IsConnected())
return;
// reset player inventory if server requested it
if (instance().m_isNeedResetInv) {
HandleResetInventory();
instance().m_isNeedResetInv = false;
return; // end execution early so next frame can re-add inventory if needed
}
// process death links
if (instance().tryKillPlayer()) {
// if successful, don't bother processing item queue or location checks
return;
}
// drain pending item queue here
instance().m_queueMutex.lock();
if (!instance().m_receivedItemsQueue.empty()) {
for (auto item : instance().m_receivedItemsQueue) {
instance().itemRecvImpl(item.first, item.second);
}
instance().m_receivedItemsQueue.clear();
}
instance().m_queueMutex.unlock();
// update location checks here if we need to
if (instance().m_isUpdateLocations) {
UpdateCheckedLocations();
instance().m_isUpdateLocations = false;
}
}
void ArchipelagoContext::HandleItemReceived(AP_NetworkItem& netItem, bool notify) {
int relativeId = netItem.item - ARCHI_ITEM_OFFSET;
if (!notify && ((relativeId >= 0 && relativeId <= 6) || relativeId == 7)) {
// skip rupee refills so players cant abuse disconnect/reconnect
return;
}
if (netItem.location != -1 && IsLocationChecked(netItem.location)) {
// no need to handle item if its location has already been checked
return;
}
instance().m_queueMutex.lock();
instance().m_receivedItemsQueue.push_back({relativeId, notify});
instance().m_queueMutex.unlock();
}
void ArchipelagoContext::HandleResetInventory() {
DuskLog.info("Resetting Inventory.");
// NOTE: this does not clear ALL things from save, so if a player managed to do something while disconnected from the archi, it might mess with things
auto& playerInfo = g_dComIfG_gameInfo.info.getPlayer();
// reset items
playerInfo.getItem().init();
playerInfo.getGetItem().init();
// reset collect (poes, shards, swords)
playerInfo.getCollect().init();
playerInfo.getPlayerStatusA().setMaxLife(15);
playerInfo.getPlayerStatusA().setWalletSize(WALLET);
// dont reset rupees, and instead reject rupee updates while refilling inv
// sync all location collect flags with current collection status obtained from initial room connection
UpdateAllLocationState();
// clear all item-related flags
dComIfGs_offEventBit(0x2580); // Power up dominion rod
// shadow crystal
dComIfGs_offEventBit(0xD04); // Can transform at will
dComIfGs_offEventBit(0x501); // Midna Charge Unlocked
// hidden skills
dComIfGs_offEventBit(0x2904); // ENDING BLOW
dComIfGs_offEventBit(0x2908); // SHIELD ATTACK
dComIfGs_offEventBit(0x2902); // BACK SLICE
dComIfGs_offEventBit(0x2901); // HELM SPLITTER
dComIfGs_offEventBit(0x2A80); // MORTAL DRAW
dComIfGs_offEventBit(0x2A40); // JUMP STRIKE
dComIfGs_offEventBit(0x2A20); // GREAT SPIN
}
void ArchipelagoContext::HandleReceiveLocationScout(const std::vector<AP_NetworkItem>& items) {
for (const auto& item : items) {
int parsedItemId;
std::string parsedItemName;
if (item.player == AP_GetPlayerID()) {
int adjustedId = item.item - ARCHI_ITEM_OFFSET;
if (instance().m_apItemToGameItem.contains(adjustedId)) {
auto& itemInfo = instance().m_apItemToGameItem[adjustedId];
parsedItemId = itemInfo.itemId;
parsedItemName = itemInfo.itemName;
}else {
parsedItemId = -1;
parsedItemName = "Unknown";
}
}else {
parsedItemId = dItemNo_Randomizer_ARCHIPELAGO_ITEM_e;
parsedItemName = "Archipelago Item";
}
int locationId = item.location - ARCHI_ITEM_OFFSET;
auto locName = instance().getLocationNameFromApId(locationId);
if (locName.empty()) {
DuskLog.info("No location with ID {} found.", locationId);
continue;
}
bool collected = false;
if (instance().m_initLocationCollectState.contains(item.location))
collected = instance().m_initLocationCollectState[item.location];
instance().m_locationItemInfo[locName] = {
parsedItemId,
parsedItemName,
locName,
item.location,
collected
};
}
}
// TODO: atm this is a sort of lazy solution to not having direct access to what location was checked when an execItemGet is called
// so eventually finding a way to properly associate locations with their respective item get funcs would benefit this system
void ArchipelagoContext::UpdateCheckedLocations() {
auto& world = instance().m_archiWorld;
bool changed = false;
for (auto location : world->GetAllLocations()) {
// skip locations that aren't progression, which are locations that just aren't randomized
if (!location->IsProgression()) {
continue;
}
auto locName = location->GetName();
if (!instance().m_locationItemInfo.contains(locName)) {
DuskLog.debug("No item found for ({}).", locName);
continue;
}
auto& cachedLocData = instance().m_locationItemInfo[locName];
bool isCollected = isLocationObtained(location);
if (isCollected && !cachedLocData.collected) {
cachedLocData.collected = true;
AP_SendItem(cachedLocData.apLocationId);
changed = true;
}
}
if (!changed) {
DuskLog.warn("No locations had any changes! this might not be normal.");
}
}
void ArchipelagoContext::SetNeedUpdateLocations(bool update) {
if (!instance().m_isAllowUpdateLocations)
instance().m_isUpdateLocations = update;
}
bool ArchipelagoContext::IsLocationChecked(int locId) {
auto& world = instance().m_archiWorld;
for (const auto& [locName, locInfo] : instance().m_locationItemInfo) {
if (locInfo.apLocationId == locId) {
if (locInfo.collected)
return true;
if (auto location = world->GetLocation(locInfo.locationName, true)) {
return isLocationObtained(location);
}
DuskLog.error("Failed to obtain location: {}", locName);
return false;
}
}
return false;
}
void ArchipelagoContext::SetLocationChecked(int locId, bool collected) {
// func was ran before location scouts could be sent out, cache result until scouts return.
if (!IsReceivedLocationScouts()) {
instance().m_initLocationCollectState[locId] = collected;
return;
}
auto& world = instance().m_archiWorld;
for (auto& [locName, locInfo] : instance().m_locationItemInfo) {
if (locInfo.apLocationId == locId) {
locInfo.collected = collected;
// update location flags if possible
auto location = world->GetLocation(locInfo.locationName, true);
if (!location || !location->IsProgression())
return;
setLocationCollected(location, collected);
return;
}
}
DuskLog.warn("No location found for locId {}.", locId);
}
void ArchipelagoContext::UpdateLocationState(int locId, bool collected) {
auto& world = instance().m_archiWorld;
for (const auto& [locName, locInfo] : instance().m_locationItemInfo) {
if (locInfo.apLocationId == locId) {
auto location = world->GetLocation(locInfo.locationName, true);
if (!location || !location->IsProgression())
continue;
setLocationCollected(location, collected);
return;
}
}
DuskLog.warn("No location found for locId {}.", locId);
}
void ArchipelagoContext::UpdateAllLocationState() {
auto& world = instance().m_archiWorld;
for (const auto& [locName, locInfo] : instance().m_locationItemInfo) {
auto location = world->GetLocation(locInfo.locationName, true);
if (!location || !location->IsProgression())
continue;
setLocationCollected(location, locInfo.collected);
}
}
bool ArchipelagoContext::IsReceivedLocationScouts() {
return !instance().m_locationItemInfo.empty();
}
void ArchipelagoContext::TryHandleDeathLink() {
if (instance().m_isEnableDeathLink && !instance().m_isFromDeathLink) {
// TODO: come up with better death messages
AP_DeathLinkSend("%YOU% was unable to become the Hero of Twilight.");
}
}
void ArchipelagoContext::RequestAllLocationScout(bool isHint) {
std::set<int64_t> locations;
// TEMP: apworld has 475 locations with ids in sequential order, so add them all individually to location set
// (eventually we will iterate through locations.yaml for a better data-driven solution)
for (int i = 0; i < 475; i++) {
locations.insert(ARCHI_ITEM_OFFSET + i);
}
AP_SendLocationScouts(locations, isHint);
}
void ArchipelagoContext::RequestPlayerDeath(bool isDeathLink) {
instance().m_isNeedPlayerDeath = true;
instance().m_isFromDeathLink = isDeathLink;
}
bool ArchipelagoContext::GenerateConfigFromAP(randomizer::seedgen::config::Config& config, const std::string& settingsStr) {
YAML::Node apConfigYaml;
try {
apConfigYaml = YAML::Load(settingsStr);
}catch (YAML::BadFile& e) {
DuskLog.warn("Failed to load AP Config YAML file!");
return false;
}
config.SetSeed("Archipelago");
randomizer::seedgen::settings::Settings& settings = config.GetSettings();
// update settings using ap config
for (const auto& apSettingEntry : apConfigYaml) {
auto apSettingName = apSettingEntry.first.as<std::string>();
auto apSettingValue = apSettingEntry.second.as<std::string>();
const auto& settingConvert = GetAPSettingNameConvert(apSettingName);
if (!settingConvert.apName.empty()) {
auto& setting = settings.GetMap().at(settingConvert.dusklightName);
setting.SetCurrentOption(settingConvert.tryGetOptionConvert(apSettingValue));
} else if (apSettingName == "Castle Requirements") {
auto& setting = settings.GetMap().at("Hyrule Barrier Requirements");
// ap assumes max mirror shards/fused shadows/dungeons, so update those settings as well
if(apSettingValue == "Open")
setting.SetCurrentOption("Open");
else if(apSettingValue == "Vanilla")
setting.SetCurrentOption("Vanilla");
else if(apSettingValue == "Fused Shadows") {
setting.SetCurrentOption("Fused Shadows");
settings.GetMap().at("Hyrule Barrier Fused Shadows").SetCurrentOption("3");
}else if(apSettingValue == "Mirror Shards") {
setting.SetCurrentOption("Mirror Shards");
settings.GetMap().at("Hyrule Barrier Mirror Shards").SetCurrentOption("4");
}else if(apSettingValue == "All Dungeons") {
setting.SetCurrentOption("Dungeons");
settings.GetMap().at("Hyrule Barrier Dungeons").SetCurrentOption("8");
}
}else if (apSettingName == "Temple of Time Entrance Requirements") {
auto& setting = settings.GetMap().at("Sacred Grove Does Not Require Skull Kid");
auto& setting2 = settings.GetMap().at("Temple of Time Sword Requirement");
if(apSettingValue == "Closed") {
setting.SetCurrentOption("Off");
setting2.SetCurrentOption("Master Sword");
}else if (apSettingValue == "Open Grove") {
setting.SetCurrentOption("On");
setting2.SetCurrentOption("Master Sword");
}else if (apSettingValue == "Open") {
setting.SetCurrentOption("On");
setting2.SetCurrentOption("None");
}
}else {
DuskLog.debug("Missing Setting: {} Value: {}", apSettingName, apSettingValue);
}
}
return true;
}
int ArchipelagoContext::GetItemAtLocation(const std::string& locName) {
if (!instance().m_locationItemInfo.contains(locName)) {
DuskLog.warn("No item found for ({}).", locName);
return 0;
}
return instance().m_locationItemInfo[locName].itemId;
}
int ArchipelagoContext::GetItemAtLocation(int locId) {
for (const auto& [locName, locInfo] : instance().m_locationItemInfo) {
if (locInfo.apLocationId == locId) {
return locInfo.itemId;
}
}
return 0;
}
void ArchipelagoContext::CreateArchipelagoWorld() {
std::filesystem::path workingDir;
GetSeedDirectoryPath(workingDir);
auto trackerRando = randomizer::Randomizer(workingDir);
trackerRando.GenerateTrackerWorld(false);
instance().m_archiWorld = std::move(trackerRando.GetWorlds().front());
}
void ArchipelagoContext::FillArchipelagoWorld() {
auto& world = instance().m_archiWorld;
if (world == nullptr) {
DuskLog.error("Archipelago world was not created!");
return;
}
auto& locationInfo = instance().m_locationItemInfo;
// fill all locations with data pulled from archi session
for (auto location : world->GetAllLocations()) {
// skip locations that aren't progression, which are locations that just aren't randomized
if (!location->IsProgression()) {
location->SetCurrentItem(location->GetOriginalItem());
continue;
}
auto locName = location->GetName();
if (!locationInfo.contains(locName)) {
if (!location->HasCategories("Warp Portal") &&
!location->HasCategories("Placeholder") &&
!location->HasCategories("Hint Sign"))
DuskLog.warn("Missing archipelago location data for: {}", locName);
auto origItem = location->GetOriginalItem();
// set location to original item
if (origItem->GetID() != -1) // ensure item is not nothing
location->SetCurrentItem(origItem);
else
DuskLog.info("Location ({}) does not have an original item!", locName);
continue;
}
auto& locInfo = locationInfo[locName];
if (locInfo.itemId != -1) {
location->SetCurrentItem(world->GetItem(locInfo.itemId));
}else {
DuskLog.info("Skipping location ({}) as item is -1.", locName);
}
}
}
void ArchipelagoContext::CreateRandomizerContext() {
auto& world = instance().m_archiWorld;
// Set hint texts before writing context
randomizer::logic::hints::GenerateAllHints(world);
// TODO: generate archipelago item get text replacements
auto randoData = WriteSeedData(world.get());
randoData.mHash = GetArchipelagoSeedName();
randomizer_GetContext() = randoData;
std::filesystem::path workingDir;
GetSeedDirectoryPath(workingDir);
auto writeToFileResult = randoData.WriteToFile(workingDir / "seed.dat");
if (writeToFileResult.has_value()) {
DuskLog.error("Failed to create Rando Data. Reason: {}", writeToFileResult.value());
return;
}
}
void ArchipelagoContext::LoadRandomizerContext() {
randomizer_GetContext() = RandomizerContext();
std::filesystem::path workingDir;
GetSeedDirectoryPath(workingDir);
randomizer_GetContext().LoadFromPath(workingDir / "seed.dat");
randomizer_GetContext().mHash = GetArchipelagoSeedName();
}
void ArchipelagoContext::GenerateLocalWorldData() {
bool createContext = false;
std::filesystem::path workingDir;
GetSeedDirectoryPath(workingDir);
if (std::filesystem::exists(workingDir)) {
instance().m_config.LoadFromFile(workingDir / "settings.yaml", workingDir / "preferences.yaml");
}else {
std::filesystem::create_directories(workingDir);
// creates base yamls at directory if they dont exist yet
instance().m_config.LoadFromFile(workingDir / "settings.yaml", workingDir / "preferences.yaml");
if (instance().m_SettingsFile.empty()) {
DuskLog.fatal("Settings Data was not sent to client! Unable to generate world data.");
return;
}
GenerateConfigFromAP(instance().m_config, instance().m_SettingsFile);
instance().m_config.WriteToFile(workingDir / "settings.yaml", workingDir / "preferences.yaml");
createContext = true;
}
CreateArchipelagoWorld();
FillArchipelagoWorld();
if (createContext) {
CreateRandomizerContext();
}else {
LoadRandomizerContext();
}
}
} // dusk::archi