diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp index 3277370bace..33c77a1a05b 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp @@ -460,7 +460,7 @@ int16_t LinkIsInArea(const EntranceData* entrance) { return -1; } -bool IsEntranceDiscovered(s16 index) { +bool EntranceTracker_IsEntranceDiscovered(s16 index) { bool isDiscovered = Entrance_GetIsEntranceDiscovered(index); if (!isDiscovered) { // If the pair included one of the hyrule field <-> zora's river entrances, @@ -993,6 +993,33 @@ void EntranceTrackerWindow::DrawElement() { doAreaScroll = true; } + bool isDiscovered = EntranceTracker_IsEntranceDiscovered(entrance.index); + + bool showOverride = (!destToggle ? showTo : showFrom) || isDiscovered; + bool showOriginal = (!destToggle ? showFrom : showTo) || isDiscovered; + + const char* origSrcAreaName = spoilerEntranceGroupNames[original->srcGroup].c_str(); + const char* origTypeName = groupTypeNames[original->type].c_str(); + const char* rplcSrcAreaName = spoilerEntranceGroupNames[override->srcGroup].c_str(); + const char* rplcTypeName = groupTypeNames[override->type].c_str(); + + const char* origSrcName = showOriginal ? original->source.c_str() : ""; + const char* rplcDstName = showOverride ? override->destination.c_str() : ""; + + // Filter for entrances by group name, type, source/destination names, and meta tags + if ((!locationSearch.IsActive() && (showOriginal || showOverride || !collapseUndiscovered)) || + ((showOriginal && + (locationSearch.PassFilter(origSrcName) || locationSearch.PassFilter(origSrcAreaName) || + locationSearch.PassFilter(origTypeName) || locationSearch.PassFilter(original->metaTag.c_str()))) || + (showOverride && + (locationSearch.PassFilter(rplcDstName) || locationSearch.PassFilter(rplcSrcAreaName) || + locationSearch.PassFilter(rplcTypeName) || locationSearch.PassFilter(override->metaTag.c_str()))))) { + + // Detect if a scroll should happen and remember the scene for that scroll + if (!doAreaScroll && + (lastSceneOrEntranceDetected != LinkIsInArea(original) && LinkIsInArea(original) != -1)) { + lastSceneOrEntranceDetected = LinkIsInArea(original); + doAreaScroll = true; displayEntrances.push_back(entrance); } else if (!isDiscovered) { undiscovered++; @@ -1013,6 +1040,7 @@ void EntranceTrackerWindow::DrawElement() { const EntranceData* original = GetEntranceData(entrance.index); const EntranceData* override = GetEntranceData(entrance.override); + bool isDiscovered = EntranceTracker_IsEntranceDiscovered(entrance.index); bool isDiscovered = IsEntranceDiscovered(entrance.index); bool showOverride = (!destToggle ? showTo : showFrom) || isDiscovered; @@ -1079,6 +1107,23 @@ void EntranceTrackerWindow::InitElement() { [](int32_t fileNum) { ClearEntranceTrackingData(); }); } +const char* EntranceTracker_GetGroupName(SpoilerEntranceGroup group) { + const size_t idx = static_cast(group); + const size_t count = sizeof(spoilerEntranceGroupNames) / sizeof(spoilerEntranceGroupNames[0]); + if (idx >= count) { + return "Unknown"; + } + return spoilerEntranceGroupNames[idx].c_str(); +} + +const char* EntranceTracker_GetTypeName(TrackerEntranceType type) { + const size_t idx = static_cast(type); + const size_t count = sizeof(groupTypeNames) / sizeof(groupTypeNames[0]); + if (idx >= count) { + return "Unknown"; + } + return groupTypeNames[idx].c_str(); +} void RegisterCheckTrackerWidgets() { backgroundColorWidget = { .name = "Background Color##EntranceTracker", .type = WidgetType::WIDGET_CVAR_COLOR_PICKER }; diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.h b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.h index 036630e90d6..411a3685367 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.h +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.h @@ -110,6 +110,10 @@ void InitEntranceTrackingData(); s16 GetLastEntranceOverride(); s16 GetCurrentGrottoId(); const EntranceData* GetEntranceData(s16); +bool EntranceTracker_IsEntranceDiscovered(s16 index); +const char* EntranceTracker_GetGroupName(SpoilerEntranceGroup group); +const char* EntranceTracker_GetTypeName(TrackerEntranceType type); +void EntranceTracker_LoadFromPreset(nlohmann::json info); void LoadFromPreset(nlohmann::json info); class EntranceTrackerSettingsWindow final : public Ship::GuiWindow { diff --git a/soh/soh/Network/Sail/Sail.cpp b/soh/soh/Network/Sail/Sail.cpp index ddaf059c589..4b9737fee20 100644 --- a/soh/soh/Network/Sail/Sail.cpp +++ b/soh/soh/Network/Sail/Sail.cpp @@ -2,13 +2,226 @@ #include #include #include +#include "global.h" #include "soh/OTRGlobals.h" +#include "soh/Enhancements/randomizer/SeedContext.h" +#include "soh/Enhancements/randomizer/entrance.h" +#include "soh/Enhancements/randomizer/randomizer_entrance_tracker.h" +#include "soh/Enhancements/randomizer/randomizerTypes.h" #include "soh/util.h" template bool IsType(const SrcType* src) { return dynamic_cast(src) != nullptr; } +static bool sPendingInitialSync = false; + +static void Sail_GetEntranceSceneRoomSpawn(const EntranceData* data, int32_t* scene, int32_t* room, int32_t* spawn) { + if (scene) { + *scene = -1; + } + if (room) { + *room = -1; + } + if (spawn) { + *spawn = -1; + } + + if (data == nullptr || data->scenes.empty()) { + return; + } + + const auto& info = data->scenes.front(); + if (scene) { + *scene = info.scene; + } + if (spawn) { + *spawn = info.spawn; + } + if (room) { + *room = (info.scene == SCENE_THIEVES_HIDEOUT && info.spawn >= 0) ? info.spawn : -1; + } +} + +static nlohmann::json Sail_BuildEntranceScenes(const EntranceData* data) { + nlohmann::json scenes = nlohmann::json::array(); + if (data == nullptr || data->scenes.empty()) { + return scenes; + } + + for (const auto& info : data->scenes) { + nlohmann::json entry; + entry["scene"] = info.scene; + entry["sceneName"] = SohUtils::GetSceneName(info.scene); + entry["spawn"] = info.spawn; + entry["room"] = (info.scene == SCENE_THIEVES_HIDEOUT && info.spawn >= 0) ? info.spawn : -1; + scenes.push_back(entry); + } + + return scenes; +} + +static bool Sail_ShouldSkipEntrance(const EntranceData* original, const EntranceData* overrideData, s16 index, + bool hideReverse, bool discoveredOnly) { + if (original == nullptr || overrideData == nullptr) { + return true; + } + + if (original->metaTag.ends_with("bw") || overrideData->metaTag.ends_with("bw")) { + return true; + } + + const bool decoupled = + OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_DECOUPLED_ENTRANCES) == RO_GENERIC_ON; + + if ((original->type == ENTRANCE_TYPE_DUNGEON || original->type == ENTRANCE_TYPE_GROTTO || + original->type == ENTRANCE_TYPE_INTERIOR) && + (original->oneExit != 1 && !decoupled && hideReverse)) { + return true; + } + + if (discoveredOnly && !EntranceTracker_IsEntranceDiscovered(index)) { + return true; + } + + return false; +} + +static void Sail_SendSeedInfo(Sail* sail) { + if (sail == nullptr || !sail->isConnected || !GameInteractor::IsSaveLoaded()) { + return; + } + + bool entranceRando = false; + bool decoupledEntrances = false; + if (IS_RANDO) { + entranceRando = + OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SHUFFLE_ENTRANCES) == RO_GENERIC_ON; + decoupledEntrances = + OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_DECOUPLED_ENTRANCES) == RO_GENERIC_ON; + } + + nlohmann::json seedPayload; + seedPayload["type"] = "seed_info"; + seedPayload["entranceRando"] = entranceRando; + seedPayload["decoupledEntrances"] = decoupledEntrances; + sail->SendJsonToRemote(seedPayload); +} + +static void Sail_SendCurrentScene(Sail* sail) { + if (sail == nullptr || !sail->isConnected || !GameInteractor::IsSaveLoaded()) { + return; + } + + const int32_t entranceIndex = static_cast(gSaveContext.entranceIndex); + const int32_t entranceTableIndex = entranceIndex + static_cast(gSaveContext.sceneSetupIndex); + const EntranceInfo entranceInfo = gEntranceTable[entranceTableIndex]; + const int32_t roomNum = gPlayState ? static_cast(gPlayState->roomCtx.curRoom.num) : -1; + + const s16 lastEntranceIndex = GetLastEntranceOverride(); + const s16 lastOverrideEntrance = + lastEntranceIndex >= 0 ? Entrance_PeekNextIndexOverride(lastEntranceIndex) : -1; + + nlohmann::json currentScenePayload; + currentScenePayload["type"] = "current_scene"; + currentScenePayload["sceneNum"] = static_cast(entranceInfo.scene); + currentScenePayload["spawn"] = static_cast(entranceInfo.spawn); + currentScenePayload["room"] = roomNum; + currentScenePayload["lastEntranceIndex"] = static_cast(entranceIndex); + currentScenePayload["lastOverrideEntrance"] = static_cast(lastOverrideEntrance); + + sail->SendJsonToRemote(currentScenePayload); +} + +static void Sail_SendEntranceMap(Sail* sail) { + if (sail == nullptr || !sail->isConnected || !GameInteractor::IsSaveLoaded()) { + return; + } + + if (!IS_RANDO || + OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SHUFFLE_ENTRANCES) != RO_GENERIC_ON) { + return; + } + + auto entranceCtx = OTRGlobals::Instance->gRandoContext->GetEntranceShuffler(); + if (entranceCtx == nullptr) { + return; + } + + const bool hideReverse = CVarGetInteger(CVAR_TRACKER_ENTRANCE("HideReverseEntrances"), 1); + + nlohmann::json payload; + payload["type"] = "entrance_map"; + payload["connections"] = nlohmann::json::array(); + + for (size_t i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) { + EntranceOverride entrance = entranceCtx->entranceOverrides[i]; + if (Entrance_EntranceIsNull(&entrance)) { + break; + } + + const EntranceData* original = GetEntranceData(entrance.index); + const EntranceData* overrideData = GetEntranceData(entrance.override); + + if (Sail_ShouldSkipEntrance(original, overrideData, entrance.index, hideReverse, true)) { + continue; + } + + int32_t fromScene = -1; + int32_t fromRoom = -1; + int32_t fromSpawn = -1; + int32_t toScene = -1; + int32_t toRoom = -1; + int32_t toSpawn = -1; + + Sail_GetEntranceSceneRoomSpawn(original, &fromScene, &fromRoom, &fromSpawn); + Sail_GetEntranceSceneRoomSpawn(overrideData, &toScene, &toRoom, &toSpawn); + + nlohmann::json entry; + entry["fromEntrance"] = static_cast(entrance.index); + entry["toEntrance"] = static_cast(entrance.override); + entry["fromReverseEntrance"] = static_cast(original->reverseIndex); + entry["toReverseEntrance"] = static_cast(overrideData->reverseIndex); + entry["fromScene"] = fromScene; + entry["fromSceneName"] = SohUtils::GetSceneName(fromScene); + entry["fromRoom"] = fromRoom; + entry["fromSpawn"] = fromSpawn; + entry["toScene"] = toScene; + entry["toSceneName"] = SohUtils::GetSceneName(toScene); + entry["toRoom"] = toRoom; + entry["spawn"] = toSpawn; + entry["toSpawn"] = toSpawn; + entry["fromName"] = original->source; + entry["toName"] = overrideData->destination; + entry["fromGroupId"] = static_cast(original->srcGroup); + entry["fromGroupName"] = EntranceTracker_GetGroupName(original->srcGroup); + entry["toGroupId"] = static_cast(overrideData->dstGroup); + entry["toGroupName"] = EntranceTracker_GetGroupName(overrideData->dstGroup); + entry["fromTypeId"] = static_cast(original->type); + entry["fromTypeName"] = EntranceTracker_GetTypeName(original->type); + entry["toTypeId"] = static_cast(overrideData->type); + entry["toTypeName"] = EntranceTracker_GetTypeName(overrideData->type); + entry["fromOneExit"] = static_cast(original->oneExit); + entry["toOneExit"] = static_cast(overrideData->oneExit); + entry["fromIsOneWay"] = original->type == ENTRANCE_TYPE_ONE_WAY; + entry["toIsOneWay"] = overrideData->type == ENTRANCE_TYPE_ONE_WAY; + entry["fromReverseIsNull"] = original->reverseIndex < 0; + entry["toReverseIsNull"] = overrideData->reverseIndex < 0; + entry["fromMetaTag"] = original->metaTag; + entry["toMetaTag"] = overrideData->metaTag; + entry["fromSource"] = original->source; + entry["fromDestination"] = original->destination; + entry["toSource"] = overrideData->source; + entry["toDestination"] = overrideData->destination; + entry["fromScenes"] = Sail_BuildEntranceScenes(original); + entry["toScenes"] = Sail_BuildEntranceScenes(overrideData); + + payload["connections"].push_back(entry); + } + + sail->SendJsonToRemote(payload); +} + void Sail::Enable() { Network::Enable(CVarGetString(CVAR_REMOTE_SAIL("Host"), "127.0.0.1"), CVarGetInteger(CVAR_REMOTE_SAIL("Port"), 43384)); @@ -16,6 +229,14 @@ void Sail::Enable() { void Sail::OnConnected() { RegisterHooks(); + if (GameInteractor::IsSaveLoaded()) { + Sail_SendSeedInfo(this); + Sail_SendCurrentScene(this); + Sail_SendEntranceMap(this); + sPendingInitialSync = false; + } else { + sPendingInitialSync = true; + } } void Sail::OnDisconnected() { @@ -338,19 +559,24 @@ void Sail::RegisterHooks() { if (!isConnected || !GameInteractor::IsSaveLoaded()) return; - nlohmann::json payload; - payload["id"] = std::rand(); - payload["type"] = "hook"; - payload["hook"]["type"] = "OnTransitionEnd"; - payload["hook"]["sceneNum"] = sceneNum; + static_cast(sceneNum); - SendJsonToRemote(payload); + // Treat transition end as the sync point: send the tracker state the website needs. + Sail_SendSeedInfo(this); + Sail_SendCurrentScene(this); + Sail_SendEntranceMap(this); + sPendingInitialSync = false; }); COND_HOOK(OnLoadGame, isConnected, [&](int32_t fileNum) { if (!isConnected || !GameInteractor::IsSaveLoaded()) return; + Sail_SendSeedInfo(this); + Sail_SendCurrentScene(this); + Sail_SendEntranceMap(this); + sPendingInitialSync = false; + nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook";