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
This commit is contained in:
Garrett Cox
2025-11-30 19:17:00 -06:00
committed by GitHub
parent bc48fa84fd
commit 9cd31099e2
14 changed files with 161 additions and 5 deletions

View File

@@ -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));

View File

@@ -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>();
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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)

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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());

View File

@@ -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;
}

View File

@@ -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>();
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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()) {