From 9cd31099e29589b1afb3d60f2b14a9d11116c3b5 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Sun, 30 Nov 2025 19:17:00 -0600 Subject: [PATCH] Additional Anchor functionality: (#5999) - Returned support for custom tunic colors - Ocarina playback now audible - Fixed movement translation issue when climbing or going through crawlspaces - Fixed issue preventing some items from being visible in Dummy hands (namely ocarina) - Fixed stick length not correctly syncing --- .../GameInteractor_HookTable.h | 1 + .../game-interactor/GameInteractor_Hooks.cpp | 4 ++ .../game-interactor/GameInteractor_Hooks.h | 1 + .../vanilla-behavior/GIVanillaBehavior.h | 9 +++ soh/soh/Network/Anchor/Anchor.cpp | 2 + soh/soh/Network/Anchor/Anchor.h | 9 +++ soh/soh/Network/Anchor/DummyPlayer.cpp | 29 ++++++++- soh/soh/Network/Anchor/HookHandlers.cpp | 27 ++++++++ soh/soh/Network/Anchor/Menu.cpp | 5 +- soh/soh/Network/Anchor/Packets/OcarinaSfx.cpp | 65 +++++++++++++++++++ .../Network/Anchor/Packets/PlayerUpdate.cpp | 6 ++ .../Anchor/Packets/UpdateClientState.cpp | 2 +- soh/src/code/code_800EC960.c | 1 + soh/src/code/z_player_lib.c | 5 +- 14 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 soh/soh/Network/Anchor/Packets/OcarinaSfx.cpp diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index e8953835d..5da302fe8 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -26,6 +26,7 @@ DEFINE_HOOK(OnPlayerUpdate, ()); DEFINE_HOOK(OnSetDoAction, (uint16_t action)); DEFINE_HOOK(OnPlayerSfx, (u16 sfxId)); DEFINE_HOOK(OnOcarinaSongAction, ()); +DEFINE_HOOK(OnOcarinaNote, (uint8_t note, float modulator, int8_t bend)); DEFINE_HOOK(OnCuccoOrChickenHatch, ()); DEFINE_HOOK(OnShopSlotChange, (uint8_t cursorIndex, int16_t price)); DEFINE_HOOK(OnDungeonKeyUsed, (uint16_t mapIndex)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 575b8a5ca..2ffb7099e 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -102,6 +102,10 @@ void GameInteractor_ExecuteOnOcarinaSongAction() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteOnOcarinaNote(uint8_t note, float modulator, int8_t bend) { + GameInteractor::Instance->ExecuteHooks(note, modulator, bend); +} + void GameInteractor_ExecuteOnCuccoOrChickenHatch() { GameInteractor::Instance->ExecuteHooks(); } diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index 472453135..fe3533f73 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -29,6 +29,7 @@ void GameInteractor_ExecuteOnPlayerUpdate(); void GameInteractor_ExecuteOnSetDoAction(uint16_t action); void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId); void GameInteractor_ExecuteOnOcarinaSongAction(); +void GameInteractor_ExecuteOnOcarinaNote(uint8_t note, float modulator, int8_t bend); void GameInteractor_ExecuteOnCuccoOrChickenHatch(); bool GameInteractor_ShouldActorInit(void* actor); void GameInteractor_ExecuteOnActorInit(void* actor); diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index 8806b0a63..d0b017d96 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -2343,6 +2343,15 @@ typedef enum { // - `*BgHidanKowarerukabe` VB_FIRE_TEMPLE_BOMBABLE_WALL_BREAK, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*Player` + // - `*Color_RGB8` + VB_APPLY_TUNIC_COLOR, + } GIVanillaBehavior; #endif diff --git a/soh/soh/Network/Anchor/Anchor.cpp b/soh/soh/Network/Anchor/Anchor.cpp index e996ac6a0..5852e62e0 100644 --- a/soh/soh/Network/Anchor/Anchor.cpp +++ b/soh/soh/Network/Anchor/Anchor.cpp @@ -110,6 +110,8 @@ void Anchor::ProcessIncomingPacketQueue() { HandlePacket_GameComplete(payload); else if (packetType == GIVE_ITEM) HandlePacket_GiveItem(payload); + else if (packetType == OCARINA_SFX) + HandlePacket_OcarinaSfx(payload); else if (packetType == PLAYER_SFX) HandlePacket_PlayerSfx(payload); else if (packetType == UPDATE_TEAM_STATE) diff --git a/soh/soh/Network/Anchor/Anchor.h b/soh/soh/Network/Anchor/Anchor.h index 93c404617..9eb103230 100644 --- a/soh/soh/Network/Anchor/Anchor.h +++ b/soh/soh/Network/Anchor/Anchor.h @@ -35,6 +35,8 @@ typedef struct { s32 linkAge; PosRot posRot; Vec3s jointTable[24]; + u8 movementFlags; + Vec3s prevTransl; Vec3s upperLimbRot; s8 currentBoots; s8 currentShield; @@ -46,8 +48,12 @@ typedef struct { s8 heldItemAction; u8 modelGroup; s8 invincibilityTimer; + f32 unk_85C; s16 unk_862; s8 actionVar1; + u8 ocarinaNote; + f32 ocarinaModulator; + s8 ocarinaBend; // Ptr to the dummy player Player* player; @@ -84,6 +90,7 @@ class Anchor : public Network { void HandlePacket_EntranceDiscovered(nlohmann::json payload); void HandlePacket_GameComplete(nlohmann::json payload); void HandlePacket_GiveItem(nlohmann::json payload); + void HandlePacket_OcarinaSfx(nlohmann::json payload); void HandlePacket_PlayerSfx(nlohmann::json payload); void HandlePacket_PlayerUpdate(nlohmann::json payload); void HandlePacket_RequestTeamState(nlohmann::json payload); @@ -111,6 +118,7 @@ class Anchor : public Network { inline static const std::string GAME_COMPLETE = "GAME_COMPLETE"; inline static const std::string GIVE_ITEM = "GIVE_ITEM"; inline static const std::string HANDSHAKE = "HANDSHAKE"; + inline static const std::string OCARINA_SFX = "OCARINA_SFX"; inline static const std::string PLAYER_SFX = "PLAYER_SFX"; inline static const std::string PLAYER_UPDATE = "PLAYER_UPDATE"; inline static const std::string REQUEST_TEAM_STATE = "REQUEST_TEAM_STATE"; @@ -148,6 +156,7 @@ class Anchor : public Network { void SendPacket_GameComplete(); void SendPacket_GiveItem(u16 modId, s16 getItemId); void SendPacket_Handshake(); + void SendPacket_OcarinaSfx(uint8_t note, float modulator, int8_t bend); void SendPacket_PlayerSfx(u16 sfxId); void SendPacket_PlayerUpdate(); void SendPacket_RequestTeamState(); diff --git a/soh/soh/Network/Anchor/DummyPlayer.cpp b/soh/soh/Network/Anchor/DummyPlayer.cpp index 3bc085413..8e5df3c8e 100644 --- a/soh/soh/Network/Anchor/DummyPlayer.cpp +++ b/soh/soh/Network/Anchor/DummyPlayer.cpp @@ -122,6 +122,8 @@ void DummyPlayer_Update(Actor* actor, PlayState* play) { Math_Vec3s_Copy(&actor->shape.rot, &client.posRot.rot); Math_Vec3f_Copy(&actor->world.pos, &client.posRot.pos); player->skelAnime.jointTable = client.jointTable; + player->skelAnime.movementFlags = client.movementFlags; + Math_Vec3s_Copy(&player->skelAnime.prevTransl, &client.prevTransl); player->currentBoots = client.currentBoots; player->currentShield = client.currentShield; player->currentTunic = client.currentTunic; @@ -131,15 +133,38 @@ void DummyPlayer_Update(Actor* actor, PlayState* play) { player->heldItemAction = client.heldItemAction; player->invincibilityTimer = client.invincibilityTimer; player->unk_862 = client.unk_862; + player->unk_85C = client.unk_85C; player->av1.actionVar1 = client.actionVar1; - if (player->modelGroup != client.modelGroup) { + // Apply animation movement (Copied from Player_ApplyAnimMovementScaledByAge) + Vec3f diff; + SkelAnime_UpdateTranslation(&player->skelAnime, &diff, player->actor.shape.rot.y); + + if (player->skelAnime.movementFlags & 1) { + if (!LINK_IS_ADULT) { + diff.x *= 0.64f; + diff.z *= 0.64f; + } + + player->actor.world.pos.x += diff.x * player->actor.scale.x; + player->actor.world.pos.z += diff.z * player->actor.scale.z; + } + + if (player->skelAnime.movementFlags & 2) { + if (!(player->skelAnime.movementFlags & 4)) { + diff.y *= player->ageProperties->unk_08; + } + + player->actor.world.pos.y += diff.y * player->actor.scale.y; + } + + if (player->modelGroup != Player_ActionToModelGroup(player, player->itemAction)) { // Hack to account for usage of gSaveContext s32 originalAge = gSaveContext.linkAge; gSaveContext.linkAge = client.linkAge; u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0]; gSaveContext.equips.buttonItems[0] = client.buttonItem0; - Player_SetModelGroup(player, client.modelGroup); + Player_SetModelGroup(player, Player_ActionToModelGroup(player, player->itemAction)); gSaveContext.linkAge = originalAge; gSaveContext.equips.buttonItems[0] = originalButtonItem0; } diff --git a/soh/soh/Network/Anchor/HookHandlers.cpp b/soh/soh/Network/Anchor/HookHandlers.cpp index 9a4622c29..c620ee30e 100644 --- a/soh/soh/Network/Anchor/HookHandlers.cpp +++ b/soh/soh/Network/Anchor/HookHandlers.cpp @@ -98,6 +98,8 @@ void Anchor::RegisterHooks() { COND_HOOK(OnGameFrameUpdate, isConnected, [&]() { ProcessIncomingPacketQueue(); }); COND_HOOK(OnPlayerSfx, isConnected, [&](u16 sfxId) { SendPacket_PlayerSfx(sfxId); }); + COND_HOOK(OnOcarinaNote, isConnected, + [&](uint8_t note, float modulator, int8_t bend) { SendPacket_OcarinaSfx(note, modulator, bend); }); COND_HOOK(OnLoadGame, isConnected, [&](s16 fileNum) { justLoadedSave = true; }); @@ -152,6 +154,31 @@ void Anchor::RegisterHooks() { SendPacket_UpdateDungeonItems(); }); + COND_VB_SHOULD(VB_APPLY_TUNIC_COLOR, isConnected, { + Actor* myPlayer = (Actor*)GET_PLAYER(gPlayState); + Actor* actor = va_arg(args, Actor*); + Color_RGB8* color = va_arg(args, Color_RGB8*); + + if (actor == myPlayer) { + Color_RGBA8 ownColor = CVarGetColor(CVAR_REMOTE_ANCHOR("Color.Value"), { 100, 255, 100 }); + color->r = ownColor.r; + color->g = ownColor.g; + color->b = ownColor.b; + return; + } + + uint32_t clientId = Anchor::Instance->GetDummyPlayerClientId(actor); + + if (!Anchor::Instance->clients.contains(clientId)) { + return; + } + + AnchorClient& client = Anchor::Instance->clients[clientId]; + color->r = client.color.r; + color->g = client.color.g; + color->b = client.color.b; + }); + // #endregion // #region Hooks that are purely to sync actor states across the clients, not super essential diff --git a/soh/soh/Network/Anchor/Menu.cpp b/soh/soh/Network/Anchor/Menu.cpp index 9c4530431..77b544fb3 100644 --- a/soh/soh/Network/Anchor/Menu.cpp +++ b/soh/soh/Network/Anchor/Menu.cpp @@ -46,7 +46,10 @@ void AnchorMainMenu(WidgetInfo& info) { } UIWidgets::PopStyleInput(); - ImGui::Text("Name"); + ImGui::Text("Name & Color"); + static Color_RGBA8 defaultColor = { 100, 255, 100, 255 }; + UIWidgets::CVarColorPicker("##Color", CVAR_REMOTE_ANCHOR("Color"), defaultColor); + ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (UIWidgets::InputString("##Name", &anchorName, UIWidgets::InputOptions().Color(THEME_COLOR))) { CVarSetString(CVAR_REMOTE_ANCHOR("Name"), anchorName.c_str()); diff --git a/soh/soh/Network/Anchor/Packets/OcarinaSfx.cpp b/soh/soh/Network/Anchor/Packets/OcarinaSfx.cpp new file mode 100644 index 000000000..72eee3c5c --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/OcarinaSfx.cpp @@ -0,0 +1,65 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include + +extern "C" { +#include "macros.h" +#include "functions.h" +#include "variables.h" +extern PlayState* gPlayState; +extern f32 D_80130F28; +} + +/** + * OCARINA_SFX + * + * Ocarina effects, only sent to other clients in the same scene as the player + */ + +void Anchor::SendPacket_OcarinaSfx(uint8_t note, float modulator, int8_t bend) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + + payload["type"] = OCARINA_SFX; + payload["note"] = note; + payload["modulator"] = modulator; + payload["bend"] = bend; + payload["quiet"] = true; + + for (auto& [clientId, client] : clients) { + if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded && !client.self) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_OcarinaSfx(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + uint8_t note = payload["note"].get(); + float modulator = payload["modulator"].get(); + int8_t bend = payload["bend"].get(); + + if (!clients.contains(clientId) || !clients[clientId].player) { + return; + } + + auto& client = clients[clientId]; + client.ocarinaModulator = modulator; + client.ocarinaBend = bend; + + if ((note != 0xFF) && (client.ocarinaNote != note)) { + Audio_QueueCmdS8(0x6 << 24 | SEQ_PLAYER_SFX << 16 | 0xD07, client.ocarinaBend - 1); + Audio_QueueCmdS8(0x6 << 24 | SEQ_PLAYER_SFX << 16 | 0xD05, note); + Audio_PlaySoundGeneral(NA_SE_OC_OCARINA, &client.player->actor.projectedPos, 4, &client.ocarinaModulator, + &D_80130F28, &gSfxDefaultReverb); + } else if ((client.ocarinaNote != 0xFF) && (note == 0xFF)) { + Audio_StopSfxById(NA_SE_OC_OCARINA); + } + + client.ocarinaNote = note; +} diff --git a/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp index a4ee17b76..c05de0a23 100644 --- a/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp +++ b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp @@ -50,6 +50,8 @@ void Anchor::SendPacket_PlayerUpdate() { jointArray.push_back(joint.y); jointArray.push_back(joint.z); } + payload["prevTransl"] = player->skelAnime.prevTransl; + payload["movementFlags"] = player->skelAnime.movementFlags; payload["jointTable"] = jointArray; payload["upperLimbRot"] = player->upperLimbRot; payload["currentBoots"] = player->currentBoots; @@ -63,6 +65,7 @@ void Anchor::SendPacket_PlayerUpdate() { payload["modelGroup"] = player->modelGroup; payload["invincibilityTimer"] = player->invincibilityTimer; payload["unk_862"] = player->unk_862; + payload["unk_85C"] = player->unk_85C; payload["actionVar1"] = player->av1.actionVar1; payload["quiet"] = true; @@ -94,6 +97,8 @@ void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) { client.jointTable[i].y = jointArray[i * 3 + 1]; client.jointTable[i].z = jointArray[i * 3 + 2]; } + client.movementFlags = payload["movementFlags"].get(); + client.prevTransl = payload["prevTransl"].get(); client.upperLimbRot = payload["upperLimbRot"].get(); client.currentBoots = payload["currentBoots"].get(); client.currentShield = payload["currentShield"].get(); @@ -106,6 +111,7 @@ void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) { client.modelGroup = payload["modelGroup"].get(); client.invincibilityTimer = payload["invincibilityTimer"].get(); client.unk_862 = payload["unk_862"].get(); + client.unk_85C = payload["unk_85C"].get(); client.actionVar1 = payload["actionVar1"].get(); } } diff --git a/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp index 7b3c180bb..3099da84a 100644 --- a/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp +++ b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp @@ -23,7 +23,7 @@ extern PlayState* gPlayState; nlohmann::json Anchor::PrepClientState() { nlohmann::json payload; payload["name"] = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), ""); - payload["color"] = CVarGetColor24(CVAR_REMOTE_ANCHOR("Color"), { 100, 255, 100 }); + payload["color"] = CVarGetColor24(CVAR_REMOTE_ANCHOR("Color.Value"), { 100, 255, 100 }); payload["clientVersion"] = clientVersion; payload["teamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); payload["online"] = true; diff --git a/soh/src/code/code_800EC960.c b/soh/src/code/code_800EC960.c index 2da5b4d2b..0cc9debe8 100644 --- a/soh/src/code/code_800EC960.c +++ b/soh/src/code/code_800EC960.c @@ -1677,6 +1677,7 @@ void func_800ED458(s32 arg0) { } else if ((sPrevOcarinaNoteVal != 0xFF) && (sCurOcarinaBtnVal == 0xFF)) { Audio_StopSfxById(NA_SE_OC_OCARINA); } + GameInteractor_ExecuteOnOcarinaNote(sCurOcarinaBtnVal, D_80130F24, D_80130F10); } } diff --git a/soh/src/code/z_player_lib.c b/soh/src/code/z_player_lib.c index 58829aee8..49d9380bf 100644 --- a/soh/src/code/z_player_lib.c +++ b/soh/src/code/z_player_lib.c @@ -7,6 +7,7 @@ #include "overlays/actors/ovl_Demo_Effect/z_demo_effect.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/Enhancements/randomizer/draw.h" #include "soh/ResourceManagerHelpers.h" @@ -1076,7 +1077,9 @@ void Player_DrawImpl(PlayState* play, void** skeleton, Vec3s* jointTable, s32 dL color = &sTemp; } - gDPSetEnvColor(POLY_OPA_DISP++, color->r, color->g, color->b, 0); + if (GameInteractor_Should(VB_APPLY_TUNIC_COLOR, true, data, color)) { + gDPSetEnvColor(POLY_OPA_DISP++, color->r, color->g, color->b, 0); + } // If we have a custom link model, always use the most detailed LOD if (Player_IsCustomLinkModel()) {