Merge pull request #61 from jeromkiller/AddArchipelagoClientLib

Add archipelago client lib
This commit is contained in:
aMannus
2025-05-18 13:31:04 +02:00
committed by GitHub
27 changed files with 2577 additions and 6 deletions

View File

@@ -1,6 +1,24 @@
![Ship of Harkinian](docs/shiptitle.darkmode.png#gh-dark-mode-only)
![Ship of Harkinian](docs/shiptitle.lightmode.png#gh-light-mode-only)
## Fork Overview
This Repo serves as a proof of concept for SoH to connect to Archipellago, the current implementation tries to implement the existing OoT AP world.
Mainly to check out the SoH Repo and to have something to develop against while the others on the discord are working on an AP world for SoH.
I'm not entierly happy with this implementation, but I'd like to create a bespoke Client to connect to AP with instead of relying on the current 3rd party clien't I've just thrown in to try out.
You can currently connect to the multiworld server, scout the items.
If you have no randomizer genereted (the `Randomizer` folder is empty) `Connect` to the multiworld from the main screen, click the `Scout` button to load all of the item locations and finally press the `Link up` button, you'll be able to start a randomizer save file.
with the items populated with the location from the server.
Sending and Recieving should be implemented, Saving the game doesn't save the current item index localy (the library is a bit anoying with that).
Not all checks have been mapped, and some may be mapped incorrectly.
The victory condition should be implemented but has largely gone untested.
Results may varry
## Website
Official Website: https://www.shipofharkinian.com/

26
differences.md Normal file
View File

@@ -0,0 +1,26 @@
# Differences between AP and Hos
AP Has a seperate option for shuffle beans and combines Medigoron and Carpet salesman
SoH combines the three and lumps Granny into the setting as well
## Missing in Hos
[ ] HoS rainbow bridge does not support the "Heart count" option in Archipellago
[ ] Enhance map and compass to give info, couldn't find it in rando settings, but may be a qol thing
[ ] Option for preplanted beans
[ ] Option for Shadow temple torch count
[ ] Deadly Bonks Option (would be implemented in logic too, for crates)
[ ] Starting at Certain time of day
[ ] "Zelda" option for Kakoriko Open setting
## Missing in AP
No Greg (the green rupee)
## missing slot data
[ ] Entrance mapping for entrance randomzier
[ ] Gossip Stone hints
[ ] Cosmetic settings
[ ] Dungeon Shortcuts when set to count
[ ] Used MQ dungeons when set to count
[ ] Shop sanity Item Prizes

View File

@@ -39,6 +39,7 @@
#include "objects/object_link_child/object_link_child.h"
#include "soh_assets.h"
#include "kaleido.h"
#include "soh/Enhancements/randomizer/archipelago.h"
extern "C" {
#include <z64.h>
@@ -782,6 +783,7 @@ void RegisterBossDefeatTimestamps() {
case ACTOR_BOSS_GANON2:
gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] = GAMEPLAYSTAT_TOTAL_TIME;
gSaveContext.ship.stats.gameComplete = true;
ArchipelagoClient::getInstance().send_game_won();
break;
case ACTOR_BOSS_GANONDROF:
gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_PHANTOM_GANON] = GAMEPLAYSTAT_TOTAL_TIME;

View File

@@ -0,0 +1,250 @@
#include "archipelago.h"
#include "soh/SohGui/UIWidgets.hpp"
#include "soh/util.h"
#include <apuuid.hpp>
#include <apclient.hpp>
#include <fstream>
#include <filesystem>
#include <spdlog/spdlog.h>
#include <iostream>
#include "randomizerTypes.h"
#include "static_data.h"
#include "../game-interactor/GameInteractor.h"
ArchipelagoClient::ArchipelagoClient() {
std::string uuid = ap_get_uuid("uuid");
ItemRecievedCallback = nullptr;
game_won = false;
namespace apc = AP_Client_consts;
CVarSetInteger("archipelago_connected", 0);
strncpy(server_address, CVarGetString(apc::SETTING_ADDRESS, apc::DEFAULT_SERVER_NAME), apc::MAX_ADDRESS_LENGTH);
strncpy(slot_name, CVarGetString(apc::SETTING_NAME, ""), apc::MAX_PLAYER_NAME_LENGHT);
// call poll every frame
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameFrameUpdate>([](){ArchipelagoClient::getInstance().poll();});
}
ArchipelagoClient& ArchipelagoClient::getInstance() {
static ArchipelagoClient Client;
return Client;
}
bool ArchipelagoClient::start_client() {
if(apclient != NULL) {
apclient.reset();
}
apclient = std::unique_ptr<APClient>(new APClient(uuid, AP_Client_consts::AP_GAME_NAME, server_address));
apclient->set_room_info_handler([&]() {
std::list<std::string> tags;
// tags.push_back("DeathLink"); // todo, implement deathlink
apclient->ConnectSlot(slot_name, password, 0b001, tags);
});
apclient->set_items_received_handler([&](const std::list<APClient::NetworkItem>& items) {
for(const APClient::NetworkItem& item : items) {
on_item_recieved(item.item, false); // todo get rid of notify, since it doesn't work for us right now anyway
}
});
apclient->set_location_info_handler([&](const std::list<APClient::NetworkItem>& items) {
scouted_items.clear();
for(const APClient::NetworkItem& item: items) {
ApItem apItem;
const std::string game = apclient->get_player_game(item.player);
apItem.itemName = apclient->get_item_name(item.item, game);
apItem.locationName = apclient->get_location_name(item.location, game);
apItem.playerName = apclient->get_player_alias(item.player);
apItem.flags = item.flags;
apItem.index = item.index;
scouted_items.push_back(apItem);
const std::string itemName = apItem.itemName;
const std::string playerName = apItem.playerName;
const std::string locationName = apItem.locationName;
SPDLOG_TRACE("Location scouted: {} for {} in location {}", itemName, playerName, locationName);
}
}); // todo maybe move these functions to a lambda, since they don't have to be static anymore
apclient->set_location_checked_handler([&](const std::list<int64_t> locations) {
// todo implement me
});
save_data();
return true;
}
void ArchipelagoClient::start_location_scouts() {
std::set<int64_t> missing_loc_set = apclient->get_missing_locations();
std::set<int64_t> found_loc_set = apclient->get_checked_locations();
std::list<int64_t> location_list;
for(const int64_t loc_id : missing_loc_set) {
location_list.emplace_back(loc_id);
}
for(const int64_t loc_id : found_loc_set) {
location_list.emplace_back(loc_id);
}
apclient->LocationScouts(location_list);
}
void ArchipelagoClient::save_data() {
CVarSetString(AP_Client_consts::SETTING_ADDRESS, server_address);
CVarSetString(AP_Client_consts::SETTING_NAME, slot_name);
}
bool ArchipelagoClient::isConnected() {
return apclient->get_state() == APClient::State::SLOT_CONNECTED;
}
void ArchipelagoClient::check_location(RandomizerCheck SoH_check_id) {
//std::string_view ap_name = Rando::StaticData::SohCheckToAP[SoH_check_id];
std::string ap_name = Rando::StaticData::GetLocation(SoH_check_id)->GetName();
if(ap_name.empty()) {
return;
}
int64_t ap_item_id = apclient->get_location_id(std::string(ap_name));
SPDLOG_TRACE("Checked: {}({}), sending to AP server", ap_name, ap_item_id);
// currently not sending, because i only get so many real chances
if(!isConnected()) {
return;
}
apclient->LocationChecks({ap_item_id});
}
void ArchipelagoClient::addItemRecievedCallback(std::function<void(const std::string&)> callback) {
ItemRecievedCallback = callback;
}
void ArchipelagoClient::removeItemRecievedCallback(std::function<void(const std::string&)> old_callback) {
ItemRecievedCallback = nullptr;
}
void ArchipelagoClient::on_connected() {
// todo implement me
SPDLOG_TRACE("AP Connected!!");
}
//void ArchipelagoClient::on_couldntConnect(AP_ConnectionStatus connection_status) {
// // todo implement me
//}
void ArchipelagoClient::on_item_recieved(int64_t recieved_item_id, bool notify_player) {
// call each callback
const std::string item_name = apclient->get_item_name(recieved_item_id, AP_Client_consts::AP_GAME_NAME);
ArchipelagoClient& ap_client = ArchipelagoClient::getInstance();
if(ap_client.ItemRecievedCallback) {
SPDLOG_TRACE("item recieved: {}, notify: {}", item_name, notify_player);
ap_client.ItemRecievedCallback.operator()(item_name); // somehow passing it through the itemname breaks it????
}
}
void ArchipelagoClient::send_game_won() {
if(!game_won) {
apclient->StatusUpdate(APClient::ClientStatus::GOAL);
game_won = true;
}
}
void ArchipelagoClient::poll() {
if(apclient == nullptr) {
return;
}
apclient->poll();
}
const std::string& ArchipelagoClient::get_slot_name() const {
if(apclient == NULL) {
return "";
}
return apclient->get_slot();
}
char* ArchipelagoClient::get_server_address_buff() {
return server_address;
}
char* ArchipelagoClient::get_slot_name_buff() {
return slot_name;
}
char* ArchipelagoClient::get_password_buff() {
return password;
}
const std::map<std::string, int>& ArchipelagoClient::get_slot_data() {
return slot_data;
}
const std::vector<ArchipelagoClient::ApItem>& ArchipelagoClient::get_scouted_items() {
return scouted_items;
}
void ArchipelagoWindow::ArchipelagoDrawConnectPage() {
ArchipelagoClient& AP_client = ArchipelagoClient::getInstance();
ImGui::SeparatorText("Connection info");
ImGui::InputText("Server Address", AP_client.get_server_address_buff(), AP_Client_consts::MAX_ADDRESS_LENGTH);
ImGui::InputText("Slot Name", AP_client.get_slot_name_buff(), AP_Client_consts::MAX_PLAYER_NAME_LENGHT);
ImGui::InputText("Password (leave blank for no password)", AP_client.get_password_buff(), AP_Client_consts::MAX_PASSWORD_LENGTH, ImGuiInputTextFlags_Password);
static char connected_text[25] = "Disconnected";
if(ImGui::Button("Connect")) {
bool success = AP_client.start_client();
}
ImGui::SameLine();
ImGui::Text(connected_text);
APClient::State con_state = APClient::State::DISCONNECTED;
if(AP_client.apclient) {
con_state = AP_client.apclient->get_state();
}
switch (con_state) {
case APClient::State::DISCONNECTED: {
strncpy(connected_text, "Disconnected!", 25);
break;
}
case APClient::State::SOCKET_CONNECTING: {
strncpy(connected_text, "Socket Connecting!", 25);
break;
}
case APClient::State::SOCKET_CONNECTED: {
strncpy(connected_text, "Socket Connected!", 25);
break;
}
case APClient::State::ROOM_INFO: {
strncpy(connected_text, "Room info Recieved!", 25);
break;
}
case APClient::State::SLOT_CONNECTED: {
strncpy(connected_text, "Slot Connected!", 25);
break;
}
};
if(ImGui::Button("scout")) {
AP_client.start_location_scouts();
}
ImGui::SameLine();
if(ImGui::Button("link up")) {
CVarSetInteger("archipelago_connected", 1);
}
};
void ArchipelagoWindow::DrawElement() {
ArchipelagoDrawConnectPage();
UIWidgets::PaddedSeparator();
if(ImGui::Button("give blue ruppie")) {
ArchipelagoClient::getInstance().on_item_recieved(66077, true);
}
};

View File

@@ -0,0 +1,100 @@
#pragma once
#include "archipelago_settings_window.h"
#include "randomizerTypes.h"
#include "static_data.h"
#include <vector>
// forward declerations
class APClient;
namespace AP_Client_consts {
static constexpr int MAX_ADDRESS_LENGTH = 64;
static constexpr int MAX_PLAYER_NAME_LENGHT = 17;
static constexpr int MAX_PASSWORD_LENGTH = 32;
static constexpr char const* DEFAULT_SERVER_NAME = "archipelago.gg:<port number>";
static constexpr char const* SETTING_ADDRESS = "AP_server_address";
static constexpr char const* SETTING_NAME = "AP_slot_name";
static constexpr char const* AP_GAME_NAME = "Ocarina of Time (SoH)";
}
class ArchipelagoClient{
public:
struct ApItem {
std::string itemName;
std::string locationName;
std::string playerName;
unsigned int flags;
int index;
};
static ArchipelagoClient& getInstance();
bool start_client();
bool stop_client();
void start_location_scouts();
// getters
const std::string& get_slot_name() const;
char* get_server_address_buff();
char* get_slot_name_buff();
char* get_password_buff();
const std::map<std::string, int>& get_slot_data();
const std::vector<ApItem>& get_scouted_items();
bool isConnected();
void check_location(RandomizerCheck SoH_check_id);
// callback slots
void addItemRecievedCallback(std::function<void(const std::string&)> callback);
void removeItemRecievedCallback(std::function<void(const std::string&)> old_callback);
// todo move me back down when done testing
void on_item_recieved(int64_t recieved_item_id, bool notify_player);
void send_game_won();
void poll();
std::unique_ptr<APClient> apclient;
protected:
ArchipelagoClient();
private:
ArchipelagoClient(ArchipelagoClient &) = delete;
void operator=(const ArchipelagoClient &) = delete;
std::string uuid;
static std::shared_ptr<ArchipelagoClient> instance; // is this even used?
static bool initialized;
char server_address[AP_Client_consts::MAX_ADDRESS_LENGTH];
char slot_name[AP_Client_consts::MAX_PLAYER_NAME_LENGHT];
char password[AP_Client_consts::MAX_PLAYER_NAME_LENGHT];
bool game_won;
std::map<std::string, int> slot_data;
std::set<int64_t> locations;
std::vector<ApItem> scouted_items;
void save_data();
// callback functions
void on_connected();
void on_location_checked(int64_t location_id);
void on_deathlink_recieved() { }; // TODO: implement me
// callbacks
std::function<void(const std::string&)> ItemRecievedCallback;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
#pragma once
#ifndef ARCHIPELAGO_H
#define ARCHIPELAGO_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif // ARCHIPELAGO_H
#include <libultraship/libultraship.h>
#ifdef __cplusplus
class ArchipelagoClient;
class ArchipelagoWindow : public Ship::GuiWindow {
public:
using GuiWindow::GuiWindow;
void InitElement() override {};
void DrawElement() override;
void UpdateElement() override{};
private:
//std::shared_ptr<ArchipelagoClient AP_client = nullptr;
void ArchipelagoDrawConnectPage();
};
#endif

View File

@@ -12,6 +12,7 @@
#include "macros.h"
#include "3drando/hints.hpp"
#include "../kaleido.h"
#include "archipelago.h"
#include <fstream>
#include <spdlog/spdlog.h>
@@ -344,6 +345,31 @@ void Context::SetSpoilerLoaded(const bool spoilerLoaded) {
mSpoilerLoaded = spoilerLoaded;
}
void Context::AddRecievedArchipelagoItem(const std::string& ap_item_id) {
mAPrecieveQueue.emplace(ap_item_id);
SPDLOG_TRACE("Item Pushed {}", ap_item_id);
}
GetItemEntry Context::GetArchipelagoGIEntry() {
SPDLOG_TRACE("Trying to get Item Entry");
if(mAPrecieveQueue.empty()) {
// something must have gone wrong here, just give a rupee
return ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, GI_HEART);
}
// get the first item from the archipelago queue
std::string recieved_ap_item = mAPrecieveQueue.front();
RandomizerGet item_id = StaticData::itemNameToEnum[recieved_ap_item];
//RandomizerGet item_id = StaticData::APitemToSoh[recieved_ap_item];
assert(item_id != RG_NONE);
Item& item = StaticData::RetrieveItem(item_id);
SPDLOG_TRACE("Found item! {}, {}", recieved_ap_item, (int)item_id);
GetItemEntry item_entry = item.GetGIEntry_Copy();
mAPrecieveQueue.pop();
return item_entry; // todo: add custom text maybe?
}
GetItemEntry Context::GetFinalGIEntry(const RandomizerCheck rc, const bool checkObtainability,
const GetItemID ogItemId) {
const auto itemLoc = GetItemLocation(rc);
@@ -409,6 +435,19 @@ void Context::ParseSpoiler(const char* spoilerFileName) {
} catch (...) { LUSLOG_ERROR("Failed to load Spoiler File: %s", spoilerFileName); }
}
void Context::ParseArchipelago() {
mSeedGenerated = false;
mSpoilerLoaded = false;
ArchipelagoClient& ap_client = ArchipelagoClient::getInstance();
Rando::Settings::GetInstance()->ParseArchipelago(ap_client.get_slot_data());
ParseArchipelagoItemsLocations(ap_client.get_scouted_items());
// lets see if counting AP_loaded as spoiler loaded does the trick
mSpoilerLoaded = true;
mSeedGenerated = false;
}
void Context::ParseHashIconIndexesJson(nlohmann::json spoilerFileJson) {
nlohmann::json hashJson = spoilerFileJson["file_hash"];
int index = 0;
@@ -419,6 +458,7 @@ void Context::ParseHashIconIndexesJson(nlohmann::json spoilerFileJson) {
}
void Context::ParseItemLocationsJson(nlohmann::json spoilerFileJson) {
// first fill all the items with their vanilla location
nlohmann::json locationsJson = spoilerFileJson["locations"];
for (auto it = locationsJson.begin(); it != locationsJson.end(); ++it) {
RandomizerCheck rc = StaticData::locationNameToEnum[it.key()];
@@ -441,6 +481,37 @@ void Context::ParseItemLocationsJson(nlohmann::json spoilerFileJson) {
}
}
void Context::ParseArchipelagoItemsLocations(const std::vector<ArchipelagoClient::ApItem>& scouted_items) {
const std::string SlotName = ArchipelagoClient::getInstance().get_slot_name();
// init the item table with regular items first
for(int rc = 1; rc <= RC_MAX; rc++) {
// This may not even be needed
const RandomizerGet vanillaItem = StaticData::GetLocation(static_cast<RandomizerCheck>(rc))->GetVanillaItem();
itemLocationTable[rc].SetPlacedItem(vanillaItem);
}
for(const ArchipelagoClient::ApItem& ap_item: scouted_items) {
//const RandomizerCheck rc = StaticData::APcheckToSoh.find(ap_item.locationName)->second;
const RandomizerCheck rc = StaticData::locationNameToEnum[ap_item.locationName];
if(SlotName == ap_item.playerName) {
// our item
SPDLOG_TRACE("Populated item {} at location {}", ap_item.itemName, ap_item.locationName);
const RandomizerGet item = StaticData::itemNameToEnum[ap_item.itemName];
//const RandomizerGet item = StaticData::APitemToSoh.find(ap_item.itemName)->second;
itemLocationTable[rc].SetPlacedItem(item);
} else {
// other player item
itemLocationTable[rc].SetPlacedItem(RG_ARCHIPELAGO_ITEM);
// i'll have to figure out custom names at some point, this currently does nothing
//overrides[rc] = ItemOverride(rc, RG_DEKU_NUTS_5);
//std::string getText = ap_item.playerName + "'s " + ap_item.itemName;
//overrides[rc].SetTrickName(Text(getText, getText, getText));
}
}
}
void Context::WriteHintJson(nlohmann::ordered_json& spoilerFileJson) {
for (Hint hint : hintTable) {
hint.logHint(spoilerFileJson);

View File

@@ -8,12 +8,14 @@
#include "hint.h"
#include "fishsanity.h"
#include "trial.h"
#include "archipelago.h"
#include <memory>
#include <array>
#include <map>
#include <nlohmann/json.hpp>
/**
* @brief Singleton for storing and accessing dynamic Randomizer-related data
*
@@ -105,6 +107,8 @@ class Context {
*/
RandoOptionLACSCondition LACSCondition() const;
GetItemEntry GetFinalGIEntry(RandomizerCheck rc, bool checkObtainability = true, GetItemID ogItemId = GI_NONE);
void AddRecievedArchipelagoItem(const std::string& ap_item_id);
GetItemEntry GetArchipelagoGIEntry();
void ParseSpoiler(const char* spoilerFileName);
void ParseHashIconIndexesJson(nlohmann::json spoilerFileJson);
void ParseItemLocationsJson(nlohmann::json spoilerFileJson);
@@ -121,6 +125,10 @@ class Context {
bool allLocationsReachable = false;
RandomizerArea GetAreaFromString(std::string str);
void ParseArchipelago();
void ParseArchipelagoSettings(const std::map<std::string, int>& slot_data);
void ParseArchipelagoItemsLocations(const std::vector<ArchipelagoClient::ApItem>& slot_data);
/**
* @brief Get the hash for the current seed.
*
@@ -181,5 +189,6 @@ class Context {
std::string mHash;
std::string mSeedString;
uint32_t mFinalSeed = 0;
std::queue<std::string> mAPrecieveQueue = {};
};
} // namespace Rando

View File

@@ -16,6 +16,7 @@
#include "soh/Notification/Notification.h"
#include "soh/SaveManager.h"
#include "soh/Enhancements/randomizer/ShuffleFairies.h"
#include "archipelago.h"
extern "C" {
#include "macros.h"
@@ -221,6 +222,12 @@ static std::queue<RandomizerCheck> randomizerQueuedChecks;
static RandomizerCheck randomizerQueuedCheck = RC_UNKNOWN_CHECK;
static GetItemEntry randomizerQueuedItemEntry = GET_ITEM_NONE;
void ArchipelagoOnRecieveItem(const std::string& ap_item_name) {
SPDLOG_TRACE("Recieve item handler called! {}", ap_item_name);
randomizerQueuedChecks.push(RC_ARCHIPELAGO_RECIEVED_ITEM);
Rando::Context::GetInstance()->AddRecievedArchipelagoItem(ap_item_name);
}
void RandomizerOnFlagSetHandler(int16_t flagType, int16_t flag) {
// Consume adult trade items
if (RAND_GET_OPTION(RSK_SHUFFLE_ADULT_TRADE) && flagType == FLAG_RANDOMIZER_INF) {
@@ -296,12 +303,18 @@ void RandomizerOnPlayerUpdateForRCQueueHandler() {
return;
}
GetItemEntry getItemEntry;
RandomizerCheck rc = randomizerQueuedChecks.front();
auto loc = Rando::Context::GetInstance()->GetItemLocation(rc);
RandomizerGet vanillaRandomizerGet = Rando::StaticData::GetLocation(rc)->GetVanillaItem();
GetItemID vanillaItem = (GetItemID)Rando::StaticData::RetrieveItem(vanillaRandomizerGet).GetItemID();
GetItemEntry getItemEntry =
Rando::Context::GetInstance()->GetFinalGIEntry(rc, true, (GetItemID)vanillaRandomizerGet);
if(rc == RC_ARCHIPELAGO_RECIEVED_ITEM) {
getItemEntry = Rando::Context::GetInstance()->GetArchipelagoGIEntry();
} else {
RandomizerGet vanillaRandomizerGet = Rando::StaticData::GetLocation(rc)->GetVanillaItem();
GetItemID vanillaItem = (GetItemID)Rando::StaticData::RetrieveItem(vanillaRandomizerGet).GetItemID();
getItemEntry = Rando::Context::GetInstance()->GetFinalGIEntry(rc, true, (GetItemID)vanillaRandomizerGet);
}
SPDLOG_TRACE("RC found!");
if (loc->HasObtained()) {
SPDLOG_INFO("RC {} already obtained, skipping", static_cast<uint32_t>(rc));
@@ -360,11 +373,21 @@ void RandomizerOnItemReceiveHandler(GetItemEntry receivedItemEntry) {
if (randomizerQueuedCheck == RC_UNKNOWN_CHECK)
return;
SPDLOG_TRACE("Dropped into recieve handler!");
auto loc = Rando::Context::GetInstance()->GetItemLocation(randomizerQueuedCheck);
if (randomizerQueuedItemEntry.modIndex == receivedItemEntry.modIndex &&
randomizerQueuedItemEntry.itemId == receivedItemEntry.itemId) {
SPDLOG_INFO("Item received mod {} item {} from RC {}", receivedItemEntry.modIndex, receivedItemEntry.itemId,
static_cast<uint32_t>(randomizerQueuedCheck));
// todo maybe move to seperate function
// let arhipelago know we got this check
if(randomizerQueuedCheck != RC_ARCHIPELAGO_RECIEVED_ITEM) {
ArchipelagoClient& ap_client = ArchipelagoClient::getInstance();
ap_client.check_location(randomizerQueuedCheck);
}
loc->SetCheckStatus(RCSHOW_COLLECTED);
CheckTracker::SpoilAreaFromCheck(randomizerQueuedCheck);
CheckTracker::RecalculateAllAreaTotals();
@@ -2423,6 +2446,8 @@ void RandomizerRegisterHooks() {
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnVanillaBehavior>(
shuffleFreestandingOnVanillaBehaviorHook);
ArchipelagoClient::getInstance().removeItemRecievedCallback(ArchipelagoOnRecieveItem);
onFlagSetHook = 0;
onSceneFlagSetHook = 0;
onPlayerUpdateForRCQueueHook = 0;
@@ -2537,5 +2562,7 @@ void RandomizerRegisterHooks() {
if (RAND_GET_OPTION(RSK_SHUFFLE_FAIRIES)) {
ShuffleFairies_RegisterHooks();
}
ArchipelagoClient::getInstance().addItemRecievedCallback(ArchipelagoOnRecieveItem);
});
}

View File

@@ -405,6 +405,9 @@ void Rando::StaticData::InitItemTable() {
itemTable[RG_MAGIC_DOUBLE] = Item(RG_MAGIC_DOUBLE, Text{ "Enhanced Magic Meter", "Jauge de Magie améliorée", "Verbessertes Magisches Maß" }, ITEMTYPE_ITEM, 0x8A, true, LOGIC_PROGRESSIVE_MAGIC, RHT_MAGIC_DOUBLE, RG_MAGIC_DOUBLE, OBJECT_GI_MAGICPOT, GID_MAGIC_LARGE, 0xE8, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_LESSER, MOD_RANDOMIZER);
itemTable[RG_TRIFORCE_PIECE] = Item(RG_TRIFORCE_PIECE, Text{ "Triforce Piece", "Triforce Piece", "Triforce-Fragment" }, ITEMTYPE_ITEM, 0xDF, true, LOGIC_TRIFORCE_PIECES, RHT_TRIFORCE_PIECE, RG_TRIFORCE_PIECE, OBJECT_GI_BOMB_2, GID_TRIFORCE_PIECE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER);
// Archipelago
itemTable[RG_ARCHIPELAGO_ITEM] = Item(RG_ARCHIPELAGO_ITEM, Text{"AP Item", "AP Item", "AP Item"}, ITEMTYPE_EVENT, GI_RUPEE_GREEN, false, LOGIC_NONE, RHT_NONE, RG_ARCHIPELAGO_ITEM, OBJECT_GI_LETTER, GID_LETTER_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0, CHEST_ANIM_SHORT, ITEM_CATEGORY_JUNK, MOD_RANDOMIZER);
// clang-format on
// Init itemNameToEnum

View File

@@ -133,6 +133,8 @@ bool ItemLocation::HasObtained() const {
}
void ItemLocation::SetCheckStatus(RandomizerCheckStatus status_) {
if(rc == RC_ARCHIPELAGO_RECIEVED_ITEM) // never count the AP recieve trigger as 'collected'
return;
status = status_;
}

View File

@@ -38,6 +38,7 @@
#include "soh/util.h"
#include "fishsanity.h"
#include "randomizerTypes.h"
#include "archipelago.h"
#include "soh/Notification/Notification.h"
extern std::map<RandomizerCheckArea, std::string> rcAreaNames;
@@ -5172,7 +5173,7 @@ CustomMessage Randomizer::GetGoronMessage(u16 index) {
void Randomizer::CreateCustomMessages() {
// RANDTODO: Translate into french and german and replace GIMESSAGE_UNTRANSLATED
// with GIMESSAGE(getItemID, itemID, english, german, french).
const std::array<GetItemMessage, 112> getItemMessages = { {
const std::array<GetItemMessage, 113> getItemMessages = { {
GIMESSAGE(RG_GREG_RUPEE, ITEM_MASK_GORON, "You found %gGreg%w!", "%gGreg%w! Du hast ihn wirklich gefunden!",
"Félicitation! Vous avez trouvé %gGreg%w!"),
GIMESSAGE(RG_MASTER_SWORD, ITEM_SWORD_MASTER, "You found the %gMaster Sword%w!",
@@ -5539,6 +5540,7 @@ void Randomizer::CreateCustomMessages() {
GIMESSAGE(RG_DEKU_NUT_BAG, ITEM_NUT, "You found the %rDeku Nut Bag%w!&You can now hold Deku Nuts!",
"Du hast eine %rDeku-Nuß-Tasche%w&gefunden! Nun kannst Du &%yDeku-Nüsse%w halten!",
"Vous avez trouvé le %rSac de Noix& Mojo%w!&Vous pouvez maintenant porter des&Noix Mojo!"),
GIMESSAGE_NO_GERMAN(RG_ARCHIPELAGO_ITEM, ITEM_BEAN, "You found an %rAP_ITEM%w", "Je m'apelle not %rNot Translated%w"),
} };
CreateGetItemMessages(getItemMessages);
CreateRupeeMessages();
@@ -5656,6 +5658,11 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) {
return Return_Item_Entry(giEntry, RG_NONE);
}
//if it's an archipelago item, don't give anything
if(item == RG_ARCHIPELAGO_ITEM) {
return Return_Item_Entry(giEntry, RG_NONE);
}
// bottle items
if (item >= RG_BOTTLE_WITH_RED_POTION && item <= RG_BOTTLE_WITH_BIG_POE) {
for (u16 i = 0; i < 4; i++) {
@@ -5878,6 +5885,7 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) {
(OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED) + 1)) {
gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] = GAMEPLAYSTAT_TOTAL_TIME;
gSaveContext.ship.stats.gameComplete = 1;
ArchipelagoClient::getInstance().send_game_won();
Flags_SetRandomizerInf(RAND_INF_GRANT_GANONS_BOSSKEY);
Play_PerformSave(play);
Notification::Emit({

View File

@@ -14,6 +14,7 @@
typedef enum {
MOD_NONE,
MOD_RANDOMIZER,
MOD_ARCHIPELAGO // no actually used yet i think
} ModIndex;
typedef enum {
TABLE_VANILLA = MOD_NONE,
@@ -3472,6 +3473,7 @@ typedef enum {
RC_DEKU_TREE_QUEEN_GOHMA_GRASS_8,
// End Grass
RC_ARCHIPELAGO_RECIEVED_ITEM,
RC_MAX
} RandomizerCheck;
@@ -4019,6 +4021,7 @@ typedef enum {
RG_BACK_TOWER_KEY,
RG_HYLIA_LAB_KEY,
RG_FISHING_HOLE_KEY,
RG_ARCHIPELAGO_ITEM,
// Logic Only
RG_DISTANT_SCARECROW,
RG_STICKS,

View File

@@ -2906,6 +2906,96 @@ void Settings::ParseJson(nlohmann::json spoilerFileJson) {
}
}
void Settings::ParseArchipelago(const std::map<std::string, int>& slot_data) {
for(const auto& slot_it : slot_data) {
const std::string& APname = slot_it.first;
int value = slot_it.second;
// remap value if needed
if(APname == "open_forest") {
if(value == 0) { // open and closed options swapped
value = 2;
} else if (value == 2) {
value = 0;
}
}
if(APname == "open_kakoriko") {
if(value == 2) {
value = 0; // closed
} else {
value = 1; // count the "zelda" option as open
}
}
else if(APname == "open_door_of_time") {
if(value == 1) {
value = 2; // AP doesn't have the song only option
}
}
else if(APname == "zora_fountain") {
if(value == 0) { // open and closed options swapped
value = 2;
} else if (value == 2) {
value = 0;
}
}
else if(APname == "bridge") {
if(value == 0) { // always open and vanilla are swapped
value = 1;
} else if (value == 1) {
value = 0;
} else if (value == 6) { // no support for ap hearts option
value = 1; // revert to always open so its at least beatable
} else if (value == 5) {
value++; // Token option is one over, because AP doesn't have dungeon blue warp count as option
}
}
else if(APname == "shopsanity_prices") {
value = 1; // default shopsanity to cheap ballanced for now
}
else if(APname == "start_with_consumables") { // just setting this shouldn't magically work I think
if(value == 1) {
const RandomizerSettingKey stick_index = StaticData::optionNameToEnum["Start with Stick Ammo"];
mContext->GetOption(stick_index).Set(mOptions[stick_index].GetValueFromText("Yes"));
const RandomizerSettingKey nut_index = StaticData::optionNameToEnum["Start with Stick Ammo"];
mContext->GetOption(nut_index).Set(mOptions[nut_index].GetValueFromText("Yes"));
} else {
const RandomizerSettingKey stick_index = StaticData::optionNameToEnum["Start with Stick Ammo"];
mContext->GetOption(stick_index).Set(mOptions[stick_index].GetValueFromText("No"));
const RandomizerSettingKey nut_index = StaticData::optionNameToEnum["Start with Stick Ammo"];
mContext->GetOption(nut_index).Set(mOptions[nut_index].GetValueFromText("No"));
}
}
const std::string& setting_name = std::string(StaticData::APsettingToHoSsetting[APname]);
const RandomizerSettingKey index = StaticData::optionNameToEnum[setting_name];
mContext->GetOption(index).Set(value);
SPDLOG_INFO("Parsed Setting {}: ({}, {})", APname, (int)index, value);
}
// maybe we have to set a couple of settings manually, if ap doesn't set them
{
//const RandomizerSettingKey index = StaticData::optionNameToEnum["Starting Age"];
//mContext->GetOption(index).Set(mOptions[index].GetValueFromText("Random"));
}
{
const RandomizerSettingKey index = StaticData::optionNameToEnum["Ganon's Trials"];
mContext->GetOption(index).Set(mOptions[index].GetValueFromText("Set Number"));
}
{
const RandomizerSettingKey index = StaticData::optionNameToEnum["Logic"];
mContext->GetOption(index).Set(mOptions[index].GetValueFromText("Glitchless"));
}
{
const RandomizerSettingKey index = StaticData::optionNameToEnum["Starting Hearts"];
mContext->GetOption(index).Set(2);
}
{
const RandomizerSettingKey index = StaticData::optionNameToEnum["Token Shuffle"];
mContext->GetOption(index).Set(mOptions[index].GetValueFromText("Off"));
}
//const RandomizerSettingKey index = StaticData::optionNameToEnum["Sleeping Waterfall"];
//mContext->GetOption(index).Set(0);
}
void Settings::AssignContext(std::shared_ptr<Context> ctx) {
mContext = ctx;
}

View File

@@ -106,6 +106,8 @@ class Settings {
* @param spoilerFileJson
*/
void ParseJson(nlohmann::json spoilerFileJson);
void ParseArchipelago(const std::map<std::string, int>& slot_data);
std::map<RandomizerArea, std::vector<RandomizerTrick>> mTricksByArea = {};
/**

View File

@@ -1,6 +1,7 @@
#include <unordered_map>
#include "static_data.h"
#include <spdlog/spdlog.h>
#include "archipelago_mappings.h"
namespace Rando {
@@ -304,4 +305,110 @@ std::unordered_map<u32, RandomizerHint> StaticData::grottoChestParamsToHint{
};
std::array<HintText, RHT_MAX> StaticData::hintTextTable = {};
const std::unordered_map<std::string_view, RandomizerGet>generate_APitemToSoh_mapping() {
std::unordered_map<std::string_view, RandomizerGet> mapping;
for(const auto& pairing : ap_item_mapping_pairs) {
mapping[pairing.first] = pairing.second;
}
return mapping;
}
const std::unordered_map<std::string_view, RandomizerCheck>generate_APcheckToSoh_mapping() {
std::unordered_map<std::string_view, RandomizerCheck> mapping;
for(const auto& pairing : ap_check_mapping_pairs) {
mapping[pairing.first] = pairing.second;
}
return mapping;
}
const std::unordered_map<RandomizerCheck, std::string_view>generate_SohcheckToAP_mapping() {
std::unordered_map<RandomizerCheck, std::string_view> mapping;
for(const auto& pairing : ap_check_mapping_pairs) {
mapping[pairing.second] = pairing.first;
}
return mapping;
}
std::unordered_map<std::string_view, RandomizerGet> StaticData::APitemToSoh = generate_APitemToSoh_mapping();
std::unordered_map<std::string_view, RandomizerCheck> StaticData::APcheckToSoh = generate_APcheckToSoh_mapping();
std::unordered_map<RandomizerCheck, std::string_view> StaticData::SohCheckToAP = generate_SohcheckToAP_mapping();
std::unordered_map<std::string_view, std::string_view> StaticData::APsettingToHoSsetting = {
{ "open_forest", "Closed Forest" },
{ "open_kakoriko", "Kakariko Gate" },
{ "open_door_of_time", "Door of Time" },
{ "zora_fountain", "Zora's Fountain" },
{ "gerudo_fortress", "Fortress Carpenters" },
{ "bridge", "Rainbow Bridge" }, // TODO underlying options may not overlap
{ "bridge_stones", "Bridge Stone Count" },
{ "bridge_medallions", "Bridge Medallion Count" },
{ "bridge_rewards", "Bridge Reward Count" },
{ "bridge_tokens", "Bridge Token Count" },
{ "bridge_hearts", "NOT_SUPPORTED" },
{ "shuffle_ganon_bosskey", "Ganon's Boss Key" },
{ "ganon_bosskey_medallions", "GCBK Medallion Count" },
{ "ganon_bosskey_stones", "GCBK Stone Count" },
{ "ganon_bosskey_rewards", "GCBK Reward Count" },
{ "ganon_bosskey_tokens", "GCBK Token Count" },
{ "ganon_bosskey_hearts", "NOT_SUPPORTED" },
{ "trials", "Ganon's Trials Count" },
{ "triforce_hunt", "Triforce Hunt" },
{ "triforce_goal", "Triforce Hunt Required Pieces" },
{ "extra_triforce_percentage", "CUSTOM_IMPLEMENTATION" }, // TODO calc "Triforce Hunt Required Pieces" from percentage, Actually not really required to make the game run I think
{ "shopsanity", "Shop Shuffle" },
{ "shop_slots", "Shops Item Count" },
{ "shopsanity_prices", "Shops Prices" }, // Item Prizes not in slot data, anything above starting wallet will be lowered to max 99
{ "tokensanity", "Token Shuffle" },
{ "dungeon_shortcuts", "NOT_SUPPORTED" }, // TODO could be implemented manually through
{ "mq_dungeons_mode", "NOT_SUPORTED" }, // Not sure if we can figure this one out
{ "mq_dungeons_count", "NOT_SUPPORTED" }, // Slot data doesn't expose the master quest dungeons used
{ "shuffle_interior_entrances", "NOT_SUPPORTED" }, // Mapping not in Slot Data
{ "shuffle_grotto_entrances", "NOT_SUPPORTED" }, // Mapping not in Slot Data
{ "shuffle_dungeon_entrances", "NOT_SUPPORTED" }, // Mapping not in Slot Data
{ "shuffle_overworld_entrances", "NOT_SUPPORTED" }, // Mapping not in Slot Data
{ "shuffle_bosses", "NOT_SUPPORTED" }, // Mapping not in Slot Data
{ "key_rings", "Key Rings" }, // slot data not exposed when set to random, however may not matter if you just can just recieve the key ring, may only be needed for logic
{ "enhance_map_compass", "NOT_SUPPORTED" }, // Can't find it in rando settings, may be a qol setting
{ "shuffle_mapcompass", "Maps/Compasses" }, // NOT REQUIRED
{ "shuffle_smallkeys", "Small Key Shuffle" }, // NOT REQUIRED
{ "shuffle_hideoutkeys", "Gerudo Fortress Keys" }, // NOT REQUIRED
{ "shuffle_bosskeys", "Boss Key Shuffle" }, // NOT REQUIRED
{ "logic_rules", "Logic" }, // NOT REQUIRED
{ "logic_no_night_tokens_without_suns_song", "Night Skulltula's Expect Sun's Song" }, // NOT REQUIRED
{ "warp_songs", "NOT_SUPORTED" }, // slot data not exposed
{ "shuffle_song_items", "Shuffle Songs" }, // NOT REQUIRED
{ "shuffle_medigoron_carpet_salesman", "NOT_SUPPORTED" }, // NOT REQURIED, , Should set "Shuffle Merchants" option (This option also sets granny)
{ "shuffle_frog_song_rupees", "Shuffle Frog Song Rupees" }, // NOT REQUIRED
{ "shuffle_scrubs", "Scrubs Shuffle" }, // NOT REQUIRED
{ "shuffle_child_trade", "NOT_SUPPORTED" }, // NOT REQUIRED
{ "shuffle_freestanding_items", "NOT_SUPPORTED" }, // NOT REQUIRED
{ "shuffle_pots", "Shuffle Pots" }, // NOT REQUIRED
{ "shuffle_crates", "MAYBE_SUPPORTED_TODO" }, // Maybe Requred, TODO TEST
{ "shuffle_cows", "Shuffle Cows" }, // NOT REQUIRED
{ "shuffle_beehives", "Shuffle Beehives" }, // NOT REQUIRED
{ "shuffle_kokiri_sword", "Shuffle Kokiri Sword" }, // NOT REQUIRED
{ "shuffle_ocarinas", "Shuffle Ocarinas" }, // NOT REQUIRED
{ "shuffle_gerudo_card", "Shuffle Gerudo Membership Card" }, //NOT REQUIRED
{ "shuffle_beans", "NOT_SUPPORTED" }, // NOT REQUIRED, Should set "Shuffle Merchants" option (This option also sets granny)
{ "starting_age", "Selected Starting Age" }, // should this also set "Seelcted Starting Age"
{ "bombchus_in_logic", "NOT_SUPPORTED" }, // NOT REQUIRED, Probably Implemented as a trick
{ "spawn_positions", "NOT_SUPPORTED" },
{ "owl_drops", "NOT_SUPPORTRED" },
{ "no_epona_race", "Skip Epona Race" },
{ "skip_some_minigame_phases", "NOT_IMPLEMENTED" }, // should be under quality of life options
{ "complete_mask_quest", "Complete Mask Quest" },
{ "free_scarecrow", "Skip Scarecrow's Song" },
{ "plant_beans", "NOT_SUPPORTRED" },
{ "chicken_count", "Cuccos to return" },
{ "big_poe_count", "Big Poe Target Count" },
{ "fae_torch_count", "NOT_SUPPORTED" },
{ "blue_fire_arrows", "Blue Fire Arrows" },
{ "damage_multiplier", "Damage Multiplier" },
{ "deadly_bonks", "NOT_SUPPORTED" },
{ "starting_tod", "NOT_SUPPORTED" },
{ "junk_ice_traps", "Ice Traps" }, // NOT REQUIRED
{ "start_with_consumables", "CUSTOM_IMPLEMENTATION" }, // might be able to just set "Start with Stick Ammo" and "Start with Nut Ammo", TODO check starting consumables
{ "adult_trade_start", "NOT_SUPPORTED" }
};
} // namespace Rando

View File

@@ -78,6 +78,10 @@ class StaticData {
static std::unordered_map<u32, RandomizerHint> grottoChestParamsToHint;
static std::array<HintText, RHT_MAX> hintTextTable;
static std::unordered_map<std::string_view, RandomizerGet> APitemToSoh;
static std::unordered_map<std::string_view, RandomizerCheck> APcheckToSoh;
static std::unordered_map<RandomizerCheck, std::string_view> SohCheckToAP;
static std::unordered_map<std::string_view, std::string_view> APsettingToHoSsetting;
StaticData();
~StaticData();
};

View File

@@ -15,6 +15,7 @@
#include "assets/textures/parameter_static/parameter_static.h"
#include <soh/SohGui/SohGui.hpp>
#include "soh/SohGui/UIWidgets.hpp"
#include "soh/Enhancements/randomizer/archipelago.h"
extern "C" {
extern SaveContext gSaveContext;
@@ -351,6 +352,7 @@ void HandleDragAndDrop(std::vector<SplitObject>& objectList, int targetIndex, co
void TimeSplitCompleteSplits() {
gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] = GAMEPLAYSTAT_TOTAL_TIME;
gSaveContext.ship.stats.gameComplete = true;
ArchipelagoClient::getInstance().send_game_won();
}
void TimeSplitsSkipSplit(uint32_t index) {

View File

@@ -2593,4 +2593,8 @@ void SoH_ProcessDroppedFiles(std::string filePath) {
return;
}
}
extern "C" void parse_archipelago() {
OTRGlobals::Instance->gRandoContext->ParseArchipelago();
}
// #endregion

View File

@@ -166,6 +166,7 @@ void CheckTracker_OnMessageClose();
GetItemID RetrieveGetItemIDFromItemID(ItemID itemID);
RandomizerGet RetrieveRandomizerGetFromItemID(ItemID itemID);
void parse_archipelago();
#endif
#ifdef __cplusplus

View File

@@ -94,6 +94,7 @@ std::shared_ptr<ItemTrackerSettingsWindow> mItemTrackerSettingsWindow;
std::shared_ptr<ItemTrackerWindow> mItemTrackerWindow;
std::shared_ptr<TimeSplitWindow> mTimeSplitWindow;
std::shared_ptr<PlandomizerWindow> mPlandomizerWindow;
std::shared_ptr<ArchipelagoWindow> mArchipelagoWindow;
std::shared_ptr<RandomizerSettingsWindow> mRandomizerSettingsWindow;
std::shared_ptr<SohModalWindow> mModalWindow;
std::shared_ptr<Notification::Window> mNotificationWindow;
@@ -195,6 +196,9 @@ void SetupGuiElements() {
mPlandomizerWindow =
std::make_shared<PlandomizerWindow>(CVAR_WINDOW("PlandomizerEditor"), "Plandomizer Editor", ImVec2(850, 760));
gui->AddGuiWindow(mPlandomizerWindow);
mArchipelagoWindow =
std::make_shared<ArchipelagoWindow>(CVAR_WINDOW("ArchipelagoWindow"), "Archipelago", ImVec2(850, 760));
gui->AddGuiWindow(mArchipelagoWindow);
mModalWindow = std::make_shared<SohModalWindow>(CVAR_WINDOW("ModalWindow"), "Modal Window");
gui->AddGuiWindow(mModalWindow);
mModalWindow->Show();
@@ -237,6 +241,7 @@ void Destroy() {
mInputViewerSettings = nullptr;
mTimeSplitWindow = nullptr;
mPlandomizerWindow = nullptr;
mArchipelagoWindow = nullptr;
mTimeDisplayWindow = nullptr;
}

View File

@@ -29,6 +29,7 @@
#include "soh/Enhancements/randomizer/randomizer_settings_window.h"
#include "soh/Enhancements/timesplits/TimeSplits.h"
#include "soh/Enhancements/randomizer/Plandomizer.h"
#include "soh/Enhancements/randomizer/archipelago.h"
#include "SohModals.h"
namespace SohGui {

View File

@@ -41,6 +41,7 @@
#include "soh/Enhancements/enemyrandomizer.h"
#include "soh/Enhancements/timesplits/TimeSplits.h"
#include "soh/Enhancements/randomizer/Plandomizer.h"
#include "soh/Enhancements/randomizer/archipelago.h"
#include "soh/Enhancements/TimeDisplay/TimeDisplay.h"
// FA icons are kind of wonky, if they worked how I expected them to the "+ 2.0f" wouldn't be needed, but

View File

@@ -100,6 +100,15 @@ void SohMenu::AddMenuRandomizer() {
.WindowName("Plandomizer Editor")
.Options(WindowButtonOptions().Tooltip("Enables the separate Randomizer Settings Window."));
// Archipelago
path.sidebarName = "Archipelago";
AddSidebarEntry("Randomizer", path.sidebarName, 1);
AddWidget(path, "Popout Archipelago Development Window", WIDGET_WINDOW_BUTTON)
.CVar(CVAR_WINDOW("ArchipelagoWindow"))
.RaceDisable(false)
.WindowName("Archipelago")
.Options(WindowButtonOptions().Tooltip("Enables the Archipelago development Window."));
// Item Tracker
path.sidebarName = "Item Tracker";
AddSidebarEntry("Randomizer", path.sidebarName, 1);

View File

@@ -1030,6 +1030,7 @@ void DrawSeedHashSprites(FileChooseContext* this) {
u8 generating;
int retries = 0;
bool fileSelectSpoilerFileLoaded = false;
bool fileSelectarchipelagoloaded = false;
void FileChoose_UpdateRandomizer() {
if (CVarGetInteger(CVAR_GENERAL("RandoGenerating"), 0) != 0 && generating == 0) {
@@ -1073,6 +1074,13 @@ void FileChoose_UpdateRandomizer() {
remove(fileLoc);
}
}
if (CVarGetInteger("archipelago_connected", 0) != 0
&& !fileSelectarchipelagoloaded) {
parse_archipelago();
fileSelectarchipelagoloaded = true;
Audio_PlayFanfare(NA_BGM_HORSE_GOAL);
}
}
static s16 sLastFileChooseButtonIndex;
@@ -3931,6 +3939,7 @@ void FileChoose_Init(GameState* thisx) {
this->questType[1] = MIN_QUEST;
this->questType[2] = MIN_QUEST;
fileSelectSpoilerFileLoaded = false;
fileSelectarchipelagoloaded = false;
CVarSetInteger(CVAR_GENERAL("OnFileSelectNameEntry"), 0);
SREG(30) = 1;