summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGarrett Cox <garrettjcox@gmail.com>2025-11-30 19:17:00 -0600
committerGitHub <noreply@github.com>2025-11-30 18:17:00 -0700
commit9cd31099e29589b1afb3d60f2b14a9d11116c3b5 (patch)
tree495d0c3b75970618f654babb46cc2a32f37e9675
parentbc48fa84fd0f3980171a52f61082549e4fd39439 (diff)
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
-rw-r--r--soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h1
-rw-r--r--soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp4
-rw-r--r--soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h1
-rw-r--r--soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h9
-rw-r--r--soh/soh/Network/Anchor/Anchor.cpp2
-rw-r--r--soh/soh/Network/Anchor/Anchor.h9
-rw-r--r--soh/soh/Network/Anchor/DummyPlayer.cpp29
-rw-r--r--soh/soh/Network/Anchor/HookHandlers.cpp27
-rw-r--r--soh/soh/Network/Anchor/Menu.cpp5
-rw-r--r--soh/soh/Network/Anchor/Packets/OcarinaSfx.cpp65
-rw-r--r--soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp6
-rw-r--r--soh/soh/Network/Anchor/Packets/UpdateClientState.cpp2
-rw-r--r--soh/src/code/code_800EC960.c1
-rw-r--r--soh/src/code/z_player_lib.c5
14 files changed, 161 insertions, 5 deletions
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<GameInteractor::OnOcarinaSongAction>();
}
+void GameInteractor_ExecuteOnOcarinaNote(uint8_t note, float modulator, int8_t bend) {
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOcarinaNote>(note, modulator, bend);
+}
+
void GameInteractor_ExecuteOnCuccoOrChickenHatch() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnCuccoOrChickenHatch>();
}
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 <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+
+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<uint32_t>();
+ uint8_t note = payload["note"].get<uint8_t>();
+ float modulator = payload["modulator"].get<float>();
+ int8_t bend = payload["bend"].get<int8_t>();
+
+ 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<u8>();
+ client.prevTransl = payload["prevTransl"].get<Vec3s>();
client.upperLimbRot = payload["upperLimbRot"].get<Vec3s>();
client.currentBoots = payload["currentBoots"].get<s8>();
client.currentShield = payload["currentShield"].get<s8>();
@@ -106,6 +111,7 @@ void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) {
client.modelGroup = payload["modelGroup"].get<u8>();
client.invincibilityTimer = payload["invincibilityTimer"].get<s8>();
client.unk_862 = payload["unk_862"].get<s16>();
+ client.unk_85C = payload["unk_85C"].get<f32>();
client.actionVar1 = payload["actionVar1"].get<s8>();
}
}
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()) {