From 5f0c0c8e2f3c82e0abd120fb2289659cf6cd4b77 Mon Sep 17 00:00:00 2001 From: Sean Latham Date: Mon, 23 Mar 2026 02:34:06 +0100 Subject: [PATCH] Added minimap icons for other players in Anchor (#6372) Icon size for other players reduced by 25% --- .../GameInteractor_HookTable.h | 1 + .../game-interactor/GameInteractor_Hooks.cpp | 4 + .../game-interactor/GameInteractor_Hooks.h | 1 + soh/soh/Network/Anchor/Anchor.h | 1 + soh/soh/Network/Anchor/HookHandlers.cpp | 152 ++++++++++++++++++ soh/soh/Network/Anchor/JsonConversions.hpp | 1 + soh/soh/Network/Anchor/Menu.cpp | 18 +++ .../Anchor/Packets/UpdateClientState.cpp | 3 + soh/src/code/z_map_exp.c | 5 + 9 files changed, 186 insertions(+) diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index 5d3e99c79..2cceebc48 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -63,6 +63,7 @@ DEFINE_HOOK(OnDialogMessage, ()); DEFINE_HOOK(OnPresentTitleCard, ()); DEFINE_HOOK(OnInterfaceUpdate, ()); DEFINE_HOOK(OnKaleidoscopeUpdate, (int16_t inDungeonScene)); +DEFINE_HOOK(OnMinimapDrawCompassIcons, ()); DEFINE_HOOK(OnPresentFileSelect, ()); DEFINE_HOOK(OnUpdateFileSelectSelection, (uint16_t optionIndex)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 3375d30c7..b754e9c94 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -298,6 +298,10 @@ void GameInteractor_ExecuteOnKaleidoscopeUpdate(int16_t inDungeonScene) { GameInteractor::Instance->ExecuteHooks(inDungeonScene); } +void GameInteractor_ExecuteOnMinimapDrawCompassIcons() { + GameInteractor::Instance->ExecuteHooks(); +} + // MARK: - Main Menu void GameInteractor_ExecuteOnPresentFileSelect() { diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index e998812f5..c93568712 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -69,6 +69,7 @@ void GameInteractor_ExecuteOnDialogMessage(); void GameInteractor_ExecuteOnPresentTitleCard(); void GameInteractor_ExecuteOnInterfaceUpdate(); void GameInteractor_ExecuteOnKaleidoscopeUpdate(int16_t inDungeonScene); +void GameInteractor_ExecuteOnMinimapDrawCompassIcons(); // MARK: - Main Menu void GameInteractor_ExecuteOnPresentFileSelect(); diff --git a/soh/soh/Network/Anchor/Anchor.h b/soh/soh/Network/Anchor/Anchor.h index 8476d8c36..a7277a0a7 100644 --- a/soh/soh/Network/Anchor/Anchor.h +++ b/soh/soh/Network/Anchor/Anchor.h @@ -29,6 +29,7 @@ typedef struct { bool isSaveLoaded; bool isGameComplete; s16 sceneNum; + s8 curRoomNum; s32 entranceIndex; // Only available in PLAYER_UPDATE packets diff --git a/soh/soh/Network/Anchor/HookHandlers.cpp b/soh/soh/Network/Anchor/HookHandlers.cpp index c620ee30e..7df8a70f3 100644 --- a/soh/soh/Network/Anchor/HookHandlers.cpp +++ b/soh/soh/Network/Anchor/HookHandlers.cpp @@ -1,6 +1,9 @@ #include "Anchor.h" #include +#include "soh/Enhancements/cosmetics/cosmeticsTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/frame_interpolation.h" +#include "soh/OTRGlobals.h" extern "C" { #include "variables.h" @@ -28,8 +31,10 @@ extern "C" { #include "src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.h" #include "src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.h" #include "src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.h" +#include "objects/gameplay_keep/gameplay_keep.h" extern PlayState* gPlayState; +extern MapData* gMapData; void func_8086ED70(BgBombwall* bgBombwall, PlayState* play); void BgBreakwall_Wait(BgBreakwall* bgBreakwall, PlayState* play); @@ -48,6 +53,8 @@ void BgYdanSp_FloorWebIdle(BgYdanSp* bgYdanSp, PlayState* play); void BgYdanSp_WallWebIdle(BgYdanSp* bgYdanSp, PlayState* play); void BgYdanSp_BurnWeb(BgYdanSp* bgYdanSp, PlayState* play); void EnDoor_Idle(EnDoor* enDoor, PlayState* play); +float OTRGetDimensionFromLeftEdge(float v); +float OTRGetDimensionFromRightEdge(float v); } void Anchor::RegisterHooks() { @@ -393,4 +400,149 @@ void Anchor::RegisterHooks() { }); // #endregion + + // #region Hooks for visual effects that don't affect gameplay + + struct CompassIcon { + Vec3f pos; + Vec3s rot; + float scale; + Color_RGB8 color; + }; + + COND_HOOK(OnMinimapDrawCompassIcons, isConnected, [&]() { + if (!CVarGetInteger(CVAR_REMOTE_ANCHOR("ShowOtherPlayersOnMinimap"), 1) || + Anchor::Instance->roomState.showLocationsMode == 0) { + return; + } + + std::vector compassIcons; + + bool isInDungeon = gPlayState->sceneNum == SCENE_DEKU_TREE || gPlayState->sceneNum == SCENE_DODONGOS_CAVERN || + gPlayState->sceneNum == SCENE_JABU_JABU || gPlayState->sceneNum == SCENE_FOREST_TEMPLE || + gPlayState->sceneNum == SCENE_FIRE_TEMPLE || gPlayState->sceneNum == SCENE_WATER_TEMPLE || + gPlayState->sceneNum == SCENE_SPIRIT_TEMPLE || gPlayState->sceneNum == SCENE_SHADOW_TEMPLE || + gPlayState->sceneNum == SCENE_BOTTOM_OF_THE_WELL || gPlayState->sceneNum == SCENE_ICE_CAVERN; + std::string teamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + + // When transitioning to a new room via a door, curRoom.num updates immediately but the minimap still shows the + // previous room while fading out + s8 displayedRoomNum = + gPlayState->roomCtx.prevRoom.num >= 0 ? gPlayState->roomCtx.prevRoom.num : gPlayState->roomCtx.curRoom.num; + + for (auto& [clientId, client] : Anchor::Instance->clients) { + // Show compass icons for other players in the current scene. Also require them to be in the current room + // within dungeons. If showLocationsMode isn't all players (2), only show compass icons for players of the + // same team + if (!client.self && client.online && client.player && client.sceneNum == gPlayState->sceneNum && + (!isInDungeon || client.curRoomNum == displayedRoomNum) && + (Anchor::Instance->roomState.showLocationsMode == 2 || client.teamId == teamId)) { + compassIcons.push_back( + CompassIcon{ client.player->actor.world.pos, client.player->actor.shape.rot, 0.3f, client.color }); + } + } + + // The local player's compass icon is always last so it gets drawn above the others + Player* player = GET_PLAYER(gPlayState); + compassIcons.push_back(CompassIcon{ player->actor.world.pos, player->actor.shape.rot, 0.4f, + CVarGetColor24(CVAR_REMOTE_ANCHOR("Color.Value"), { 100, 255, 100 }) }); + + // Adapted internals of Minimap_DrawCompassIcons() + s16 leftMinimapMargin = CVarGetInteger(CVAR_COSMETIC("HUD.Margin.L"), 0); + s16 rightMinimapMargin = CVarGetInteger(CVAR_COSMETIC("HUD.Margin.R"), 0); + s16 bottomMinimapMargin = CVarGetInteger(CVAR_COSMETIC("HUD.Margin.B"), 0); + + s16 xMarginsMinimap; + s16 yMarginsMinimap; + if (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.UseMargins"), 0) != 0) { + if (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosType"), 0) == ORIGINAL_LOCATION) { + xMarginsMinimap = rightMinimapMargin; + } + yMarginsMinimap = bottomMinimapMargin; + } else { + xMarginsMinimap = 0; + yMarginsMinimap = 0; + } + + s16 mapWidth = isInDungeon ? R_DGN_MINIMAP_X : R_OW_MINIMAP_X; + s16 mapStartPosX = isInDungeon ? 96 : gMapData->owMinimapWidth[R_MAP_INDEX]; + + OPEN_DISPS(gPlayState->state.gfxCtx); + Gfx_SetupDL_42Overlay(gPlayState->state.gfxCtx); + + for (auto& compassIcon : compassIcons) { + gSPMatrix(OVERLAY_DISP++, &gMtxClear, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gDPSetCombineLERP(OVERLAY_DISP++, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0, + PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0); + gDPSetEnvColor(OVERLAY_DISP++, 0, 0, 0, 255); + gDPSetCombineMode(OVERLAY_DISP++, G_CC_PRIMITIVE, G_CC_PRIMITIVE); + + // The compass offset value is a factor of 10 compared to N64 screen pixels and originates in the center of + // the screen Compute the additional mirror offset value by normalizing the original offset position and + // taking it's distance to the center of the map, duplicating that result and casting back to a factor of 10 + s16 mirrorOffset = + ((mapWidth / 2) - ((R_COMPASS_OFFSET_X / 10) - (mapStartPosX - SCREEN_WIDTH / 2))) * 2 * 10; + + s16 tempX = (s16)compassIcon.pos.x; + s16 tempZ = (s16)compassIcon.pos.z; + tempX /= R_COMPASS_SCALE_X * (CVarGetInteger(CVAR_ENHANCEMENT("MirroredWorld"), 0) ? -1 : 1); + tempZ /= R_COMPASS_SCALE_Y; + + s16 tempXOffset = + R_COMPASS_OFFSET_X + (CVarGetInteger(CVAR_ENHANCEMENT("MirroredWorld"), 0) ? mirrorOffset : 0); + if (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosType"), 0) != ORIGINAL_LOCATION) { + if (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosType"), 0) == ANCHOR_LEFT) { + if (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.UseMargins"), 0) != 0) { + xMarginsMinimap = leftMinimapMargin; + }; + Matrix_Translate( + OTRGetDimensionFromLeftEdge((tempXOffset + (xMarginsMinimap * 10) + tempX + + (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosX"), 0) * 10)) / + 10.0f), + (R_COMPASS_OFFSET_Y + ((yMarginsMinimap * 10) * -1) - tempZ + + ((CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosY"), 0) * 10) * -1)) / + 10.0f, + 0.0f, MTXMODE_NEW); + } else if (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosType"), 0) == ANCHOR_RIGHT) { + if (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.UseMargins"), 0) != 0) { + xMarginsMinimap = rightMinimapMargin; + }; + Matrix_Translate( + OTRGetDimensionFromRightEdge((tempXOffset + (xMarginsMinimap * 10) + tempX + + (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosX"), 0) * 10)) / + 10.0f), + (R_COMPASS_OFFSET_Y + ((yMarginsMinimap * 10) * -1) - tempZ + + ((CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosY"), 0) * 10) * -1)) / + 10.0f, + 0.0f, MTXMODE_NEW); + } else if (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosType"), 0) == ANCHOR_NONE) { + Matrix_Translate( + (tempXOffset + tempX + (CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosX"), 0) * 10) / 10.0f), + (R_COMPASS_OFFSET_Y + ((yMarginsMinimap * 10) * -1) - tempZ + + ((CVarGetInteger(CVAR_COSMETIC("HUD.Minimap.PosY"), 0) * 10) * -1)) / + 10.0f, + 0.0f, MTXMODE_NEW); + } + } else { + Matrix_Translate(OTRGetDimensionFromRightEdge((tempXOffset + (xMarginsMinimap * 10) + tempX) / 10.0f), + (R_COMPASS_OFFSET_Y + ((yMarginsMinimap * 10) * -1) - tempZ) / 10.0f, 0.0f, + MTXMODE_NEW); + } + Matrix_Scale(compassIcon.scale, compassIcon.scale, compassIcon.scale, MTXMODE_APPLY); + Matrix_RotateX(-1.6f, MTXMODE_APPLY); + s16 rotation = ((0x7FFF - compassIcon.rot.y) / 0x400) * + (CVarGetInteger(CVAR_ENHANCEMENT("MirroredWorld"), 0) ? -1 : 1); + Matrix_RotateY(rotation / 10.0f, MTXMODE_APPLY); + gSPMatrix(OVERLAY_DISP++, MATRIX_NEWMTX(gPlayState->state.gfxCtx), + G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + + gDPSetPrimColor(OVERLAY_DISP++, 0, 0xFF, compassIcon.color.r, compassIcon.color.g, compassIcon.color.b, + 255); + gSPDisplayList(OVERLAY_DISP++, (Gfx*)gCompassArrowDL); + } + + CLOSE_DISPS(gPlayState->state.gfxCtx); + }); + + // #endregion } diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp index 8b28ba40e..69ad13136 100644 --- a/soh/soh/Network/Anchor/JsonConversions.hpp +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -62,6 +62,7 @@ inline void from_json(const json& j, AnchorClient& client) { client.isSaveLoaded = j.value("isSaveLoaded", false); client.isGameComplete = j.value("isGameComplete", false); client.sceneNum = j.value("sceneNum", (s16)SCENE_ID_MAX); + client.curRoomNum = j.value("curRoomNum", (s8)-1); client.entranceIndex = j.value("entranceIndex", (s32)0); client.self = j.value("self", false); } diff --git a/soh/soh/Network/Anchor/Menu.cpp b/soh/soh/Network/Anchor/Menu.cpp index 77b544fb3..4a974532e 100644 --- a/soh/soh/Network/Anchor/Menu.cpp +++ b/soh/soh/Network/Anchor/Menu.cpp @@ -139,6 +139,24 @@ void AnchorMainMenu(WidgetInfo& info) { ImGui::SameLine(); UIWidgets::WindowButton("Toggle Anchor Room Window", CVAR_WINDOW("AnchorRoom"), SohGui::mAnchorRoomWindow); + + ImGui::Spacing(); + + bool hideLocations = Anchor::Instance->roomState.showLocationsMode == 0; + ImGui::BeginDisabled(hideLocations); + UIWidgets::CVarCheckbox( + "Show Other Players on Minimap", CVAR_REMOTE_ANCHOR("ShowOtherPlayersOnMinimap"), + UIWidgets::CheckboxOptions() + .Color(THEME_COLOR) + .DefaultValue(true) + .Tooltip(!hideLocations + ? "Other players will appear on the minimap in areas where you have the compass. " + "Visibility is restricted according to the Show Locations mode for the room." + : "Cannot show other players because the room's Show Locations mode is set to None.")); + ImGui::EndDisabled(); + + ImGui::Spacing(); + if (!SohGui::mAnchorRoomWindow->IsVisible()) { SohGui::mAnchorRoomWindow->DrawElement(); } diff --git a/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp index 7beb83ce0..5b7d6e178 100644 --- a/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp +++ b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp @@ -33,12 +33,14 @@ nlohmann::json Anchor::PrepClientState() { payload["isSaveLoaded"] = true; payload["isGameComplete"] = gSaveContext.ship.stats.gameComplete; payload["sceneNum"] = gPlayState->sceneNum; + payload["curRoomNum"] = gPlayState->roomCtx.curRoom.num; payload["entranceIndex"] = gSaveContext.entranceIndex; } else { payload["seed"] = 0; payload["isSaveLoaded"] = false; payload["isGameComplete"] = false; payload["sceneNum"] = SCENE_ID_MAX; + payload["curRoomNum"] = -1; payload["entranceIndex"] = 0x00; } @@ -68,6 +70,7 @@ void Anchor::HandlePacket_UpdateClientState(nlohmann::json payload) { clients[clientId].isSaveLoaded = client.isSaveLoaded; clients[clientId].isGameComplete = client.isGameComplete; clients[clientId].sceneNum = client.sceneNum; + clients[clientId].curRoomNum = client.curRoomNum; clients[clientId].entranceIndex = client.entranceIndex; } } diff --git a/soh/src/code/z_map_exp.c b/soh/src/code/z_map_exp.c index 44b5b43b5..6d8222e5a 100644 --- a/soh/src/code/z_map_exp.c +++ b/soh/src/code/z_map_exp.c @@ -7,6 +7,7 @@ #include #include "soh/OTRGlobals.h" #include "soh/Enhancements/cosmetics/cosmeticsTypes.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" MapData* gMapData; @@ -761,6 +762,10 @@ void Minimap_DrawCompassIcons(PlayState* play) { } CLOSE_DISPS(play->state.gfxCtx); + + if (play->interfaceCtx.minimapAlpha >= 0xAA) { + GameInteractor_ExecuteOnMinimapDrawCompassIcons(); + } } void Minimap_Draw(PlayState* play) {