summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGarrett Cox <garrettjcox@gmail.com>2024-10-20 01:31:29 -0500
committerGarrett Cox <garrettjcox@gmail.com>2024-12-16 09:18:29 -0600
commit31ee15dcc8b50a9a2d32d39262a294754039a54f (patch)
treefe5bd73d09d8e2ad18a585e62a493ef3369225dd
parentadd5347126aa7e7cbd10cc138b74491725e65700 (diff)
WIP
-rw-r--r--soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h9
-rw-r--r--soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp26
-rw-r--r--soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h8
-rw-r--r--soh/soh/Enhancements/randomizer/hook_handlers.cpp4
-rw-r--r--soh/soh/Enhancements/randomizer/item_location.cpp2
-rw-r--r--soh/soh/Enhancements/randomizer/randomizer_entrance.c3
-rw-r--r--soh/soh/Network/Anchor/Anchor.cpp394
-rw-r--r--soh/soh/Network/Anchor/Anchor.h176
-rw-r--r--soh/soh/Network/Anchor/AnchorRoomWindow.cpp118
-rw-r--r--soh/soh/Network/Anchor/DummyPlayer.cpp216
-rw-r--r--soh/soh/Network/Anchor/JsonConversions.hpp186
-rw-r--r--soh/soh/Network/Anchor/Packets/AllClientState.cpp74
-rw-r--r--soh/soh/Network/Anchor/Packets/ConsumeAdultTradeItem.cpp46
-rw-r--r--soh/soh/Network/Anchor/Packets/DamagePlayer.cpp62
-rw-r--r--soh/soh/Network/Anchor/Packets/DisableAnchor.cpp18
-rw-r--r--soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp37
-rw-r--r--soh/soh/Network/Anchor/Packets/GameComplete.cpp48
-rw-r--r--soh/soh/Network/Anchor/Packets/GiveItem.cpp104
-rw-r--r--soh/soh/Network/Anchor/Packets/Handshake.cpp26
-rw-r--r--soh/soh/Network/Anchor/Packets/PlayerSfx.cpp51
-rw-r--r--soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp120
-rw-r--r--soh/soh/Network/Anchor/Packets/RequestTeamState.cpp37
-rw-r--r--soh/soh/Network/Anchor/Packets/RequestTeleport.cpp36
-rw-r--r--soh/soh/Network/Anchor/Packets/ServerMessage.cpp21
-rw-r--r--soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp53
-rw-r--r--soh/soh/Network/Anchor/Packets/SetFlag.cpp50
-rw-r--r--soh/soh/Network/Anchor/Packets/TeleportTo.cpp63
-rw-r--r--soh/soh/Network/Anchor/Packets/UnsetFlag.cpp51
-rw-r--r--soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp43
-rw-r--r--soh/soh/Network/Anchor/Packets/UpdateClientState.cpp77
-rw-r--r--soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp42
-rw-r--r--soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp47
-rw-r--r--soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp259
-rw-r--r--soh/soh/OTRGlobals.cpp9
-rw-r--r--soh/soh/SaveManager.cpp2
-rw-r--r--soh/soh/SohGui.cpp13
-rw-r--r--soh/soh/SohMenuBar.cpp2
-rw-r--r--soh/soh/UIWidgets.cpp4
-rw-r--r--soh/soh/UIWidgets.hpp2
-rw-r--r--soh/soh/cvar_prefixes.h3
-rw-r--r--soh/soh/util.cpp1
-rw-r--r--soh/src/code/z_actor.c40
-rw-r--r--soh/src/code/z_en_item00.c7
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c6
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c6
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c4
-rw-r--r--soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c11
-rw-r--r--soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c2
-rw-r--r--soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c6
-rw-r--r--soh/src/overlays/actors/ovl_En_Door/z_en_door.c7
-rw-r--r--soh/src/overlays/actors/ovl_En_Si/z_en_si.c7
-rw-r--r--soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c7
-rw-r--r--soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c7
-rw-r--r--soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c4
-rw-r--r--soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c6
67 files changed, 2668 insertions, 43 deletions
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h
index a2d5c56ec..b2789b14d 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h
@@ -18,8 +18,11 @@ DEFINE_HOOK(OnFlagSet, (int16_t flagType, int16_t flag));
DEFINE_HOOK(OnFlagUnset, (int16_t flagType, int16_t flag));
DEFINE_HOOK(OnSceneSpawnActors, ());
DEFINE_HOOK(OnPlayerUpdate, ());
+DEFINE_HOOK(OnPlayerSfx, (u16 sfxId));
DEFINE_HOOK(OnOcarinaSongAction, ());
DEFINE_HOOK(OnShopSlotChange, (uint8_t cursorIndex, int16_t price));
+DEFINE_HOOK(OnDungeonKeyUsed, (uint16_t mapIndex));
+DEFINE_HOOK(ShouldActorInit, (void* actor, bool* result));
DEFINE_HOOK(OnActorInit, (void* actor));
DEFINE_HOOK(OnActorUpdate, (void* actor));
DEFINE_HOOK(OnActorKill, (void* actor));
@@ -32,7 +35,7 @@ DEFINE_HOOK(OnPlayerBottleUpdate, (int16_t contents));
DEFINE_HOOK(OnPlayDestroy, ());
DEFINE_HOOK(OnPlayDrawEnd, ());
DEFINE_HOOK(OnVanillaBehavior, (GIVanillaBehavior flag, bool* result, va_list originalArgs));
-DEFINE_HOOK(OnSaveFile, (int32_t fileNum));
+DEFINE_HOOK(OnSaveFile, (int32_t fileNum, int32_t sectionID));
DEFINE_HOOK(OnLoadFile, (int32_t fileNum));
DEFINE_HOOK(OnDeleteFile, (int32_t fileNum));
@@ -59,3 +62,7 @@ DEFINE_HOOK(OnSetGameLanguage, ());
DEFINE_HOOK(OnFileDropped, (std::string filePath));
DEFINE_HOOK(OnAssetAltChange, ());
DEFINE_HOOK(OnKaleidoUpdate, ());
+
+DEFINE_HOOK(OnRandoSetCheckStatus, (RandomizerCheck rc, RandomizerCheckStatus status));
+DEFINE_HOOK(OnRandoSetIsSkipped, (RandomizerCheck rc, bool isSkipped));
+DEFINE_HOOK(OnRandoEntranceDiscovered, (u16 entranceIndex, u8 isReversedEntrance));
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
index 39fc298a8..ad9ad90a4 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
@@ -64,6 +64,10 @@ void GameInteractor_ExecuteOnPlayerUpdate() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerUpdate>();
}
+void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId) {
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerSfx>(sfxId);
+}
+
void GameInteractor_ExecuteOnOcarinaSongAction() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOcarinaSongAction>();
}
@@ -72,6 +76,19 @@ void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t pr
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnShopSlotChange>(cursorIndex, price);
}
+void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex) {
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnDungeonKeyUsed>(mapIndex);
+}
+
+bool GameInteractor_ShouldActorInit(void* actor) {
+ bool result = true;
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::ShouldActorInit>(actor, &result);
+ GameInteractor::Instance->ExecuteHooksForID<GameInteractor::ShouldActorInit>(((Actor*)actor)->id, actor, &result);
+ GameInteractor::Instance->ExecuteHooksForPtr<GameInteractor::ShouldActorInit>((uintptr_t)actor, actor, &result);
+ GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::ShouldActorInit>(actor, &result);
+ return result;
+}
+
void GameInteractor_ExecuteOnActorInit(void* actor) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnActorInit>(actor);
GameInteractor::Instance->ExecuteHooksForID<GameInteractor::OnActorInit>(((Actor*)actor)->id, actor);
@@ -154,8 +171,8 @@ bool GameInteractor_Should(GIVanillaBehavior flag, u32 result, ...) {
// MARK: - Save Files
-void GameInteractor_ExecuteOnSaveFile(int32_t fileNum) {
- GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum);
+void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID) {
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum, sectionID);
}
void GameInteractor_ExecuteOnLoadFile(int32_t fileNum) {
@@ -255,3 +272,8 @@ void GameInteractor_RegisterOnAssetAltChange(void (*fn)(void)) {
void GameInteractor_ExecuteOnKaleidoUpdate() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnKaleidoUpdate>();
}
+
+// MARK: - Rando
+void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) {
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnRandoEntranceDiscovered>(entranceIndex, isReversedEntrance);
+}
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
index 3438d269d..f484bc95d 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
@@ -18,7 +18,9 @@ void GameInteractor_ExecuteOnFlagSet(int16_t flagType, int16_t flag);
void GameInteractor_ExecuteOnFlagUnset(int16_t flagType, int16_t flag);
void GameInteractor_ExecuteOnSceneSpawnActors();
void GameInteractor_ExecuteOnPlayerUpdate();
+void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId);
void GameInteractor_ExecuteOnOcarinaSongAction();
+bool GameInteractor_ShouldActorInit(void* actor);
void GameInteractor_ExecuteOnActorInit(void* actor);
void GameInteractor_ExecuteOnActorUpdate(void* actor);
void GameInteractor_ExecuteOnActorKill(void* actor);
@@ -30,12 +32,13 @@ void GameInteractor_ExecuteOnPlayerHealthChange(int16_t amount);
void GameInteractor_ExecuteOnPlayerBottleUpdate(int16_t contents);
void GameInteractor_ExecuteOnOcarinaSongAction();
void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t price);
+void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex);
void GameInteractor_ExecuteOnPlayDestroy();
void GameInteractor_ExecuteOnPlayDrawEnd();
bool GameInteractor_Should(GIVanillaBehavior flag, uint32_t result, ...);
// MARK: - Save Files
-void GameInteractor_ExecuteOnSaveFile(int32_t fileNum);
+void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID);
void GameInteractor_ExecuteOnLoadFile(int32_t fileNum);
void GameInteractor_ExecuteOnDeleteFile(int32_t fileNum);
@@ -69,6 +72,9 @@ void GameInteractor_RegisterOnAssetAltChange(void (*fn)(void));
//Mark: - Pause Menu
void GameInteractor_ExecuteOnKaleidoUpdate();
+// MARK: - Rando
+void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance);
+
#ifdef __cplusplus
}
#endif
diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp
index 0bdd2b0cc..6a6ac103d 100644
--- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp
+++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp
@@ -995,12 +995,12 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l
if (item00->itemEntry.modIndex == MOD_NONE) {
Notification::Emit({
.itemIcon = GetTextureForItemId(item00->itemEntry.itemId),
- .message = "You found ",
+ .message = "You found",
.suffix = SohUtils::GetItemName(item00->itemEntry.itemId),
});
} else if (item00->itemEntry.modIndex == MOD_RANDOMIZER) {
Notification::Emit({
- .message = "You found ",
+ .message = "You found",
.suffix = Rando::StaticData::RetrieveItem((RandomizerGet)item00->itemEntry.getItemId).GetName().english,
});
}
diff --git a/soh/soh/Enhancements/randomizer/item_location.cpp b/soh/soh/Enhancements/randomizer/item_location.cpp
index 9f0f84509..1a11c087c 100644
--- a/soh/soh/Enhancements/randomizer/item_location.cpp
+++ b/soh/soh/Enhancements/randomizer/item_location.cpp
@@ -130,6 +130,7 @@ bool ItemLocation::HasObtained() const {
void ItemLocation::SetCheckStatus(RandomizerCheckStatus status_) {
status = status_;
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnRandoSetCheckStatus>(rc, status);
}
RandomizerCheckStatus ItemLocation::GetCheckStatus() {
@@ -138,6 +139,7 @@ RandomizerCheckStatus ItemLocation::GetCheckStatus() {
void ItemLocation::SetIsSkipped(bool isSkipped_) {
isSkipped = isSkipped_;
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnRandoSetIsSkipped>(rc, isSkipped);
}
bool ItemLocation::GetIsSkipped() {
diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c
index 7a492fe9c..cb770c54f 100644
--- a/soh/soh/Enhancements/randomizer/randomizer_entrance.c
+++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c
@@ -15,6 +15,7 @@
#include "global.h"
#include "entrance.h"
+#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
extern PlayState* gPlayState;
@@ -778,6 +779,8 @@ void Entrance_SetEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) {
return;
}
+ GameInteractor_ExecuteOnRandoEntranceDiscovered(entranceIndex, isReversedEntrance);
+
u16 bitsPerIndex = sizeof(u32) * 8;
u32 idx = entranceIndex / bitsPerIndex;
if (idx < SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT) {
diff --git a/soh/soh/Network/Anchor/Anchor.cpp b/soh/soh/Network/Anchor/Anchor.cpp
new file mode 100644
index 000000000..7d1b701ab
--- /dev/null
+++ b/soh/soh/Network/Anchor/Anchor.cpp
@@ -0,0 +1,394 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/OTRGlobals.h"
+#include "soh/Enhancements/nametag.h"
+
+extern "C" {
+#include "variables.h"
+#include "functions.h"
+extern PlayState* gPlayState;
+}
+
+// MARK: - Overrides
+
+void Anchor::Enable() {
+ Network::Enable(CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.proxysaw.dev"), CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383));
+ ownClientId = CVarGetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), 0);
+ roomState.ownerClientId = 0;
+}
+
+void Anchor::Disable() {
+ Network::Disable();
+
+ clients.clear();
+ RefreshClientActors();
+}
+
+void Anchor::OnConnected() {
+ SendPacket_Handshake();
+ RegisterHooks();
+
+ if (IsSaveLoaded()) {
+ SendPacket_RequestTeamState();
+ }
+}
+
+void Anchor::OnDisconnected() {
+ RegisterHooks();
+}
+
+void Anchor::SendJsonToRemote(nlohmann::json payload) {
+ if (!isConnected) {
+ return;
+ }
+
+ payload["clientId"] = ownClientId;
+ if (!payload.contains("quiet")) {
+ SPDLOG_INFO("[Anchor] Sending payload:\n{}", payload.dump());
+ }
+ Network::SendJsonToRemote(payload);
+}
+
+void Anchor::OnIncomingJson(nlohmann::json payload) {
+ // If it doesn't contain a type, it's not a valid payload
+ if (!payload.contains("type")) {
+ return;
+ }
+
+ // If it's not a quiet payload, log it
+ if (!payload.contains("quiet")) {
+ SPDLOG_INFO("[Anchor] Received payload:\n{}", payload.dump());
+ }
+
+ std::string packetType = payload["type"].get<std::string>();
+
+ // Ignore packets from mismatched clients, except for ALL_CLIENT_STATE or UPDATE_CLIENT_STATE
+ if (packetType != ALL_CLIENT_STATE && packetType != UPDATE_CLIENT_STATE) {
+ if (payload.contains("clientId")) {
+ uint32_t clientId = payload["clientId"].get<uint32_t>();
+ if (clients.contains(clientId) && clients[clientId].clientVersion != clientVersion) {
+ return;
+ }
+ }
+ }
+
+ // packetType here is a string so we can't use a switch statement
+ if (packetType == ALL_CLIENT_STATE) HandlePacket_AllClientState(payload);
+ else if (packetType == CONSUME_ADULT_TRADE_ITEM) HandlePacket_ConsumeAdultTradeItem(payload);
+ else if (packetType == DAMAGE_PLAYER) HandlePacket_DamagePlayer(payload);
+ else if (packetType == DISABLE_ANCHOR) HandlePacket_DisableAnchor(payload);
+ else if (packetType == ENTRANCE_DISCOVERED) HandlePacket_EntranceDiscovered(payload);
+ else if (packetType == GAME_COMPLETE) HandlePacket_GameComplete(payload);
+ else if (packetType == GIVE_ITEM) HandlePacket_GiveItem(payload);
+ else if (packetType == PLAYER_SFX) HandlePacket_PlayerSfx(payload);
+ else if (packetType == PLAYER_UPDATE) HandlePacket_PlayerUpdate(payload);
+ else if (packetType == UPDATE_TEAM_STATE) HandlePacket_UpdateTeamState(payload);
+ else if (packetType == REQUEST_TEAM_STATE) HandlePacket_RequestTeamState(payload);
+ else if (packetType == REQUEST_TELEPORT) HandlePacket_RequestTeleport(payload);
+ else if (packetType == SERVER_MESSAGE) HandlePacket_ServerMessage(payload);
+ else if (packetType == SET_CHECK_STATUS) HandlePacket_SetCheckStatus(payload);
+ else if (packetType == SET_FLAG) HandlePacket_SetFlag(payload);
+ else if (packetType == TELEPORT_TO) HandlePacket_TeleportTo(payload);
+ else if (packetType == UNSET_FLAG) HandlePacket_UnsetFlag(payload);
+ else if (packetType == UPDATE_BEANS_COUNT) HandlePacket_UpdateBeansCount(payload);
+ else if (packetType == UPDATE_CLIENT_STATE) HandlePacket_UpdateClientState(payload);
+ else if (packetType == UPDATE_ROOM_STATE) HandlePacket_UpdateRoomState(payload);
+ else if (packetType == UPDATE_DUNGEON_ITEMS) HandlePacket_UpdateDungeonItems(payload);
+}
+
+// Macros to let us easily register and unregister functions when the anchor is enabled/disabled
+#define HOOK(hook, condition, body) \
+ static HOOK_ID hook = 0; \
+ GameInteractor::Instance->UnregisterGameHook<GameInteractor::hook>(hook); \
+ hook = 0; \
+ if (condition) { \
+ hook = GameInteractor::Instance->RegisterGameHook<GameInteractor::hook>(body); \
+ }
+
+#define HOOK_FOR_ID(hook, condition, id, body) \
+ static HOOK_ID hook = 0; \
+ GameInteractor::Instance->UnregisterGameHookForID<GameInteractor::hook>(hook); \
+ hook = 0; \
+ if (condition) { \
+ hook = GameInteractor::Instance->RegisterGameHookForID<GameInteractor::hook>(id, body); \
+ }
+
+void Anchor::RegisterHooks() {
+ HOOK(OnSceneSpawnActors, isConnected, [&]() {
+ SendPacket_UpdateClientState();
+
+ if (IsSaveLoaded()) {
+ RefreshClientActors();
+ }
+ });
+
+ HOOK(OnPresentFileSelect, isConnected, [&]() {
+ SendPacket_UpdateClientState();
+ });
+
+ HOOK_FOR_ID(ShouldActorInit, isConnected, ACTOR_PLAYER, [&](void* actorRef, bool* should) {
+ Actor* actor = (Actor*)actorRef;
+
+ if (refreshingActors) {
+ // By the time we get here, the actor was already added to the ACTORCAT_PLAYER list, so we need to move it
+ Actor_ChangeCategory(gPlayState, &gPlayState->actorCtx, actor, ACTORCAT_NPC);
+ actor->id = ACTOR_EN_OE2;
+ actor->category = ACTORCAT_NPC;
+ actor->init = DummyPlayer_Init;
+ actor->update = DummyPlayer_Update;
+ actor->draw = DummyPlayer_Draw;
+ actor->destroy = DummyPlayer_Destroy;
+ }
+ });
+
+ HOOK(OnPlayerUpdate, isConnected, [&]() {
+ if (justLoadedSave) {
+ justLoadedSave = false;
+ SendPacket_RequestTeamState();
+ }
+ SendPacket_PlayerUpdate();
+ });
+
+ HOOK(OnPlayerSfx, isConnected, [&](u16 sfxId) {
+ SendPacket_PlayerSfx(sfxId);
+ });
+
+ HOOK(OnLoadGame, isConnected, [&](s16 fileNum) {
+ justLoadedSave = true;
+ });
+
+ HOOK(OnSaveFile, isConnected, [&](s16 fileNum, int sectionID) {
+ if (sectionID == 0) {
+ SendPacket_UpdateTeamState();
+ }
+ });
+
+ HOOK(OnFlagSet, isConnected, [&](s16 flagType, s16 flag) {
+ SendPacket_SetFlag(SCENE_ID_MAX, flagType, flag);
+ });
+
+ HOOK(OnFlagUnset, isConnected, [&](s16 flagType, s16 flag) {
+ SendPacket_UnsetFlag(SCENE_ID_MAX, flagType, flag);
+ });
+
+ HOOK(OnSceneFlagSet, isConnected, [&](s16 sceneNum, s16 flagType, s16 flag) {
+ SendPacket_SetFlag(sceneNum, flagType, flag);
+ });
+
+ HOOK(OnSceneFlagUnset, isConnected, [&](s16 sceneNum, s16 flagType, s16 flag) {
+ SendPacket_UnsetFlag(sceneNum, flagType, flag);
+ });
+
+ HOOK(OnRandoSetCheckStatus, isConnected, [&](RandomizerCheck rc, RandomizerCheckStatus status) {
+ if (!isHandlingUpdateTeamState) {
+ SendPacket_SetCheckStatus(rc);
+ }
+ });
+
+ HOOK(OnRandoSetIsSkipped, isConnected, [&](RandomizerCheck rc, bool isSkipped) {
+ if (!isHandlingUpdateTeamState) {
+ SendPacket_SetCheckStatus(rc);
+ }
+ });
+
+ HOOK(OnRandoEntranceDiscovered, isConnected, [&](u16 entranceIndex, u8 isReversedEntrance) {
+ SendPacket_EntranceDiscovered(entranceIndex);
+ });
+
+ HOOK_FOR_ID(OnBossDefeat, isConnected, ACTOR_BOSS_GANON2, [&](void* refActor) {
+ SendPacket_GameComplete();
+ });
+
+ HOOK(OnItemReceive, isConnected, [&](GetItemEntry itemEntry) {
+ // Handle vanilla dungeon items a bit differently
+ if (itemEntry.modIndex == MOD_NONE && (itemEntry.itemId >= ITEM_KEY_BOSS && itemEntry.itemId <= ITEM_KEY_SMALL)) {
+ SendPacket_UpdateDungeonItems();
+ return;
+ }
+
+ SendPacket_GiveItem(itemEntry.tableId, itemEntry.getItemId);
+ });
+
+ HOOK(OnDungeonKeyUsed, isConnected, [&](uint16_t mapIndex) {
+ // Handle vanilla dungeon items a bit differently
+ SendPacket_UpdateDungeonItems();
+ });
+}
+
+// MARK: - Misc/Helpers
+
+// Kills all existing anchor actors and respawns them with the new client data
+void Anchor::RefreshClientActors() {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_NPC].head;
+
+ while (actor != NULL) {
+ if (actor->id == ACTOR_EN_OE2 && actor->update == DummyPlayer_Update) {
+ NameTag_RemoveAllForActor(actor);
+ Actor_Kill(actor);
+ }
+ actor = actor->next;
+ }
+
+ actorIndexToClientId.clear();
+ refreshingActors = true;
+ for (auto& [clientId, client] : clients) {
+ if (!client.online || client.self) {
+ continue;
+ }
+
+ actorIndexToClientId.push_back(clientId);
+ // We are using a hook `ShouldActorInit` to override the init/update/draw/destroy functions of the Player we spawn
+ // We quickly store a mapping of "index" to clientId, then within the init function we use this to get the clientId
+ // and store it on player->zTargetActiveTimer (unused s32 for the dummy) for convenience
+ auto dummy = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_PLAYER, client.posRot.pos.x,
+ client.posRot.pos.y, client.posRot.pos.z, client.posRot.rot.x, client.posRot.rot.y,
+ client.posRot.rot.z, actorIndexToClientId.size() - 1, false);
+ client.player = (Player*)dummy;
+ }
+ refreshingActors = false;
+}
+
+bool Anchor::IsSaveLoaded() {
+ if (gPlayState == nullptr) {
+ return false;
+ }
+
+ if (GET_PLAYER(gPlayState) == nullptr) {
+ return false;
+ }
+
+ if (gSaveContext.fileNum < 0 || gSaveContext.fileNum > 2) {
+ return false;
+ }
+
+ if (gSaveContext.gameMode != GAMEMODE_NORMAL) {
+ return false;
+ }
+
+ return true;
+}
+
+// MARK: - UI
+
+void Anchor::DrawMenu() {
+ ImGui::PushID("Anchor");
+
+ std::string host = CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.proxysaw.dev");
+ uint16_t port = CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383);
+ std::string anchorTeamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ std::string anchorRoomId = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "");
+ std::string anchorName = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), "");
+ bool isFormValid = !SohUtils::IsStringEmpty(host) && port > 1024 && port < 65535 &&
+ !SohUtils::IsStringEmpty(anchorRoomId) && !SohUtils::IsStringEmpty(anchorName);
+
+ ImGui::SeparatorText("Anchor");
+ // UIWidgets::Tooltip("Anchor Stuff");
+ if (ImGui::IsItemClicked()) {
+ // ImGui::SetClipboardText("https://github.com/garrettjoecox/anchor");
+ }
+
+ ImGui::BeginDisabled(isEnabled);
+ ImGui::Text("Host & Port");
+ if (UIWidgets::InputString("##Host", &host)) {
+ CVarSetString(CVAR_REMOTE_ANCHOR("Host"), host.c_str());
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ }
+
+ ImGui::SameLine();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5);
+ if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) {
+ CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), port);
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ }
+
+ ImGui::Text("Tunic Color & Name");
+ static Color_RGBA8 color = CVarGetColor(CVAR_REMOTE_ANCHOR("Color"), { 100, 255, 100, 255 });
+ static ImVec4 colorVec = ImVec4(color.r / 255.0, color.g / 255.0, color.b / 255.0, 1);
+ if (ImGui::ColorEdit3("##Color", (float*)&colorVec,
+ ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) {
+ color.r = colorVec.x * 255.0;
+ color.g = colorVec.y * 255.0;
+ color.b = colorVec.z * 255.0;
+
+ CVarSetColor(CVAR_REMOTE_ANCHOR("Color"), color);
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ }
+ ImGui::SameLine();
+ ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
+ if (UIWidgets::InputString("##Name", &anchorName)) {
+ CVarSetString(CVAR_REMOTE_ANCHOR("Name"), anchorName.c_str());
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ }
+ ImGui::Text("Room ID");
+ ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
+ if (UIWidgets::InputString("##RoomId", &anchorRoomId, isEnabled ? ImGuiInputTextFlags_Password : 0)) {
+ CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), anchorRoomId.c_str());
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ }
+ ImGui::Text("Team ID (Items & Flags Shared)");
+ ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
+ if (UIWidgets::InputString("##TeamId", &anchorTeamId)) {
+ CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), anchorTeamId.c_str());
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ }
+ ImGui::EndDisabled();
+
+ ImGui::Spacing();
+
+ ImGui::BeginDisabled(!isFormValid);
+ const char* buttonLabel = isEnabled ? "Disable" : "Enable";
+ if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) {
+ if (isEnabled) {
+ CVarClear(CVAR_REMOTE_ANCHOR("Enabled"));
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ Disable();
+ } else {
+ CVarSetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 1);
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ Enable();
+ }
+ }
+ ImGui::EndDisabled();
+
+ if (isEnabled) {
+ ImGui::Spacing();
+ if (isConnected) {
+ ImGui::Text("Connected");
+
+ if (roomState.ownerClientId == ownClientId) {
+ if (ImGui::BeginMenu("Room Settings")) {
+ ImGui::Text("PvP Mode:");
+ static const char* pvpModes[3] = { "Off", "On", "On + Friendly Fire" };
+ if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), pvpModes, 1)) {
+ SendPacket_UpdateRoomState();
+ }
+ ImGui::Text("Show Locations For:");
+ static const char* showLocationsModes[3] = { "None", "Team Only", "All" };
+ if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), showLocationsModes, 1)) {
+ SendPacket_UpdateRoomState();
+ }
+ ImGui::Text("Allow Teleporting To:");
+ static const char* teleportModes[3] = { "None", "Team Only", "All" };
+ if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), teleportModes, 1)) {
+ SendPacket_UpdateRoomState();
+ }
+ ImGui::EndMenu();
+ }
+ }
+ } else {
+ ImGui::Text("Connecting...");
+ }
+ }
+
+ ImGui::PopID();
+}
+
+#endif
diff --git a/soh/soh/Network/Anchor/Anchor.h b/soh/soh/Network/Anchor/Anchor.h
new file mode 100644
index 000000000..eceb02c1d
--- /dev/null
+++ b/soh/soh/Network/Anchor/Anchor.h
@@ -0,0 +1,176 @@
+#ifdef ENABLE_REMOTE_CONTROL
+#ifndef NETWORK_ANCHOR_H
+#define NETWORK_ANCHOR_H
+#ifdef __cplusplus
+
+#include "soh/Network/Network.h"
+#include <libultraship/libultraship.h>
+
+extern "C" {
+#include "variables.h"
+#include "z64.h"
+}
+
+void DummyPlayer_Init(Actor* actor, PlayState* play);
+void DummyPlayer_Update(Actor* actor, PlayState* play);
+void DummyPlayer_Draw(Actor* actor, PlayState* play);
+void DummyPlayer_Destroy(Actor* actor, PlayState* play);
+
+typedef struct {
+ uint32_t clientId;
+ std::string name;
+ Color_RGB8 color;
+ std::string clientVersion;
+ std::string teamId;
+ bool online;
+ bool self;
+ uint32_t seed;
+ bool isSaveLoaded;
+ bool isGameComplete;
+ s16 sceneNum;
+ s32 entranceIndex;
+
+ // Only available in PLAYER_UPDATE packets
+ s32 linkAge;
+ PosRot posRot;
+ Vec3s jointTable[24];
+ Vec3s upperLimbRot;
+ s8 currentBoots;
+ s8 currentShield;
+ s8 currentTunic;
+ u32 stateFlags1;
+ u32 stateFlags2;
+ u8 buttonItem0;
+ s8 itemAction;
+ s8 heldItemAction;
+ u8 modelGroup;
+ s8 invincibilityTimer;
+ s16 unk_862;
+ s8 actionVar1;
+
+ // Ptr to the dummy player
+ Player* player;
+} AnchorClient;
+
+typedef struct {
+ uint32_t ownerClientId;
+ u8 pvpMode; // 0 = off, 1 = on, 2 = on with friendly fire
+ u8 showLocationsMode; // 0 = none, 1 = team, 2 = all
+ u8 teleportMode; // 0 = off, 1 = team, 2 = all
+} RoomState;
+
+class Anchor : public Network {
+ private:
+ bool refreshingActors = false;
+ bool justLoadedSave = false;
+ bool isHandlingUpdateTeamState = false;
+ uint32_t ownClientId;
+
+ nlohmann::json PrepClientState();
+ nlohmann::json PrepRoomState();
+ void RegisterHooks();
+ void RefreshClientActors();
+ void HandlePacket_AllClientState(nlohmann::json payload);
+ void HandlePacket_ConsumeAdultTradeItem(nlohmann::json payload);
+ void HandlePacket_DamagePlayer(nlohmann::json payload);
+ void HandlePacket_DisableAnchor(nlohmann::json payload);
+ void HandlePacket_EntranceDiscovered(nlohmann::json payload);
+ void HandlePacket_GameComplete(nlohmann::json payload);
+ void HandlePacket_GiveItem(nlohmann::json payload);
+ void HandlePacket_PlayerSfx(nlohmann::json payload);
+ void HandlePacket_PlayerUpdate(nlohmann::json payload);
+ void HandlePacket_RequestTeamState(nlohmann::json payload);
+ void HandlePacket_RequestTeleport(nlohmann::json payload);
+ void HandlePacket_ServerMessage(nlohmann::json payload);
+ void HandlePacket_SetCheckStatus(nlohmann::json payload);
+ void HandlePacket_SetFlag(nlohmann::json payload);
+ void HandlePacket_TeleportTo(nlohmann::json payload);
+ void HandlePacket_UnsetFlag(nlohmann::json payload);
+ void HandlePacket_UpdateBeansCount(nlohmann::json payload);
+ void HandlePacket_UpdateClientState(nlohmann::json payload);
+ void HandlePacket_UpdateDungeonItems(nlohmann::json payload);
+ void HandlePacket_UpdateRoomState(nlohmann::json payload);
+ void HandlePacket_UpdateTeamState(nlohmann::json payload);
+
+ public:
+ inline static const std::string clientVersion = (char*)gBuildVersion;
+
+ // Packet types //
+ inline static const std::string ALL_CLIENT_STATE = "ALL_CLIENT_STATE";
+ inline static const std::string CONSUME_ADULT_TRADE_ITEM = "CONSUME_ADULT_TRADE_ITEM";
+ inline static const std::string DAMAGE_PLAYER = "DAMAGE_PLAYER";
+ inline static const std::string DISABLE_ANCHOR = "DISABLE_ANCHOR";
+ inline static const std::string ENTRANCE_DISCOVERED = "ENTRANCE_DISCOVERED";
+ 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 PLAYER_SFX = "PLAYER_SFX";
+ inline static const std::string PLAYER_UPDATE = "PLAYER_UPDATE";
+ inline static const std::string REQUEST_TEAM_STATE = "REQUEST_TEAM_STATE";
+ inline static const std::string REQUEST_TELEPORT = "REQUEST_TELEPORT";
+ inline static const std::string SERVER_MESSAGE = "SERVER_MESSAGE";
+ inline static const std::string SET_CHECK_STATUS = "SET_CHECK_STATUS";
+ inline static const std::string SET_FLAG = "SET_FLAG";
+ inline static const std::string TELEPORT_TO = "TELEPORT_TO";
+ inline static const std::string UNSET_FLAG = "UNSET_FLAG";
+ inline static const std::string UPDATE_BEANS_COUNT = "UPDATE_BEANS_COUNT";
+ inline static const std::string UPDATE_CLIENT_STATE = "UPDATE_CLIENT_STATE";
+ inline static const std::string UPDATE_DUNGEON_ITEMS = "UPDATE_DUNGEON_ITEMS";
+ inline static const std::string UPDATE_ROOM_STATE = "UPDATE_ROOM_STATE";
+ inline static const std::string UPDATE_TEAM_STATE = "UPDATE_TEAM_STATE";
+
+ static Anchor* Instance;
+ std::map<uint32_t, AnchorClient> clients;
+ std::vector<uint32_t> actorIndexToClientId;
+ RoomState roomState;
+
+ void Enable();
+ void Disable();
+ void OnIncomingJson(nlohmann::json payload);
+ void OnConnected();
+ void OnDisconnected();
+ void DrawMenu();
+ void SendJsonToRemote(nlohmann::json packet);
+ bool IsSaveLoaded();
+
+ void SendPacket_ConsumeAdultTradeItem(u8 itemId);
+ void SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage);
+ void SendPacket_EntranceDiscovered(u16 entranceIndex);
+ void SendPacket_GameComplete();
+ void SendPacket_GiveItem(u16 modId, s16 getItemId);
+ void SendPacket_Handshake();
+ void SendPacket_PlayerSfx(u16 sfxId);
+ void SendPacket_PlayerUpdate();
+ void SendPacket_RequestTeamState();
+ void SendPacket_RequestTeleport(u32 clientId);
+ void SendPacket_SetCheckStatus(RandomizerCheck rc);
+ void SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag);
+ void SendPacket_TeleportTo(u32 clientId);
+ void SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag);
+ void SendPacket_UpdateBeansCount();
+ void SendPacket_UpdateClientState();
+ void SendPacket_UpdateDungeonItems();
+ void SendPacket_UpdateRoomState();
+ void SendPacket_UpdateTeamState();
+};
+
+typedef enum {
+ // Starting at 5 to continue from the last value in the PlayerDamageResponseType enum
+ DUMMY_PLAYER_HIT_RESPONSE_STUN = 5,
+ DUMMY_PLAYER_HIT_RESPONSE_FIRE,
+ DUMMY_PLAYER_HIT_RESPONSE_NORMAL,
+} DummyPlayerDamageResponseType;
+
+class AnchorRoomWindow : public Ship::GuiWindow {
+ public:
+ using GuiWindow::GuiWindow;
+
+ void InitElement() override {};
+ void DrawElement() override {};
+ void Draw() override;
+ void UpdateElement() override {};
+};
+
+#endif // __cplusplus
+#endif // NETWORK_ANCHOR_H
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/AnchorRoomWindow.cpp b/soh/soh/Network/Anchor/AnchorRoomWindow.cpp
new file mode 100644
index 000000000..7a683eca0
--- /dev/null
+++ b/soh/soh/Network/Anchor/AnchorRoomWindow.cpp
@@ -0,0 +1,118 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "Anchor.h"
+#include "soh/OTRGlobals.h"
+
+extern "C" {
+#include "variables.h"
+#include "functions.h"
+extern PlayState* gPlayState;
+}
+
+void AnchorRoomWindow::Draw() {
+ if (!Anchor::Instance->isConnected) {
+ return;
+ }
+
+
+ ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, CVarGetFloat(CVAR_SETTING("Notifications.BgOpacity"), 0.5f)));
+ ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
+
+ auto vp = ImGui::GetMainViewport();
+ ImGui::SetNextWindowViewport(vp->ID);
+
+ ImGui::Begin("Anchor Room", nullptr,
+ ImGuiWindowFlags_AlwaysAutoResize |
+ ImGuiWindowFlags_NoNav |
+ ImGuiWindowFlags_NoFocusOnAppearing |
+ ImGuiWindowFlags_NoResize |
+ ImGuiWindowFlags_NoDocking |
+ ImGuiWindowFlags_NoTitleBar |
+ ImGuiWindowFlags_NoScrollWithMouse |
+ ImGuiWindowFlags_NoScrollbar
+ );
+
+ // First build a list of teams
+ std::set<std::string> teams;
+ for (auto& [clientId, client] : Anchor::Instance->clients) {
+ teams.insert(client.teamId);
+ }
+
+ for (auto& team : teams) {
+ if (teams.size() > 1) {
+ ImGui::SeparatorText(team.c_str());
+ }
+ bool isOwnTeam = team == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ for (auto& [clientId, client] : Anchor::Instance->clients) {
+ if (client.teamId != team) {
+ continue;
+ }
+
+ if (client.clientId == Anchor::Instance->roomState.ownerClientId) {
+ ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", ICON_FA_GAVEL);
+ ImGui::SameLine();
+ }
+
+ if (client.self) {
+ ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.8f, 1.0f), "%s", CVarGetString(CVAR_REMOTE_ANCHOR("Name"), ""));
+ } else if (!client.online) {
+ ImGui::TextColored(ImVec4(1, 1, 1, 0.3f), "%s - offline", client.name.c_str());
+ continue;
+ } else {
+ ImGui::Text("%s", client.name.c_str());
+ }
+
+ if (Anchor::Instance->roomState.showLocationsMode == 2 || (Anchor::Instance->roomState.showLocationsMode == 1 && isOwnTeam)) {
+ if ((client.self ? Anchor::Instance->IsSaveLoaded() : client.isSaveLoaded)) {
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(1, 1, 1, 0.5f), "- %s", SohUtils::GetSceneName(client.self ? gPlayState->sceneNum : client.sceneNum).c_str());
+ }
+ }
+
+ if (
+ Anchor::Instance->IsSaveLoaded() && !client.self && client.isSaveLoaded &&
+ (Anchor::Instance->roomState.teleportMode == 2 || (Anchor::Instance->roomState.teleportMode == 1 && isOwnTeam))
+ ) {
+ ImGui::SameLine();
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
+ if (ImGui::Button(ICON_FA_LOCATION_ARROW, ImVec2(15.0f, 15.0f))) {
+ Anchor::Instance->SendPacket_RequestTeleport(client.clientId);
+ }
+ ImGui::PopStyleVar();
+ }
+
+ if (client.clientVersion != Anchor::clientVersion) {
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE);
+ if (ImGui::IsItemHovered()) {
+ ImGui::BeginTooltip();
+ ImGui::Text("Incompatible version! Will not work together!");
+ ImGui::Text("Yours: %s", Anchor::clientVersion.c_str());
+ ImGui::Text("Theirs: %s", client.clientVersion.c_str());
+ ImGui::EndTooltip();
+ }
+ }
+ uint32_t seed = IS_RANDO ? Rando::Context::GetInstance()->GetSettings()->GetSeed() : 0;
+ if (client.isSaveLoaded && Anchor::Instance->IsSaveLoaded() && client.seed != seed && client.online) {
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE);
+ if (ImGui::IsItemHovered()) {
+ ImGui::BeginTooltip();
+ ImGui::Text("Seed mismatch! Continuing will break things!");
+ ImGui::Text("Yours: %u", seed);
+ ImGui::Text("Theirs: %u", client.seed);
+ ImGui::EndTooltip();
+ }
+ }
+ }
+ }
+
+
+ ImGui::End();
+
+ ImGui::PopStyleVar();
+ ImGui::PopStyleColor(2);
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/DummyPlayer.cpp b/soh/soh/Network/Anchor/DummyPlayer.cpp
new file mode 100644
index 000000000..63c6daf03
--- /dev/null
+++ b/soh/soh/Network/Anchor/DummyPlayer.cpp
@@ -0,0 +1,216 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "Anchor.h"
+#include "soh/Enhancements/nametag.h"
+#include "soh/frame_interpolation.h"
+
+extern "C" {
+#include "macros.h"
+#include "variables.h"
+#include "functions.h"
+extern PlayState* gPlayState;
+
+void Player_UseItem(PlayState* play, Player* player, s32 item);
+void Player_Draw(Actor* actor, PlayState* play);
+}
+
+// Hijacking player->zTargetActiveTimer (unused s32 for the dummy) to store the clientId for convenience
+#define DUMMY_CLIENT_ID player->zTargetActiveTimer
+
+static DamageTable DummyPlayerDamageTable = {
+ /* Deku nut */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
+ /* Deku stick */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Slingshot */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Explosive */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Boomerang */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
+ /* Normal arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Hammer swing */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE),
+ /* Hookshot */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
+ /* Kokiri sword */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Master sword */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Giant's Knife */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Fire arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_FIRE),
+ /* Ice arrow */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_ICE_TRAP),
+ /* Light arrow */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK),
+ /* Unk arrow 1 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
+ /* Unk arrow 2 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
+ /* Unk arrow 3 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
+ /* Fire magic */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_FIRE),
+ /* Ice magic */ DMG_ENTRY(3, PLAYER_HIT_RESPONSE_ICE_TRAP),
+ /* Light magic */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK),
+ /* Shield */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
+ /* Mirror Ray */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
+ /* Kokiri spin */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Giant spin */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Master spin */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Kokiri jump */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Giant jump */ DMG_ENTRY(8, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Master jump */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
+ /* Unknown 1 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
+ /* Unblockable */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
+ /* Hammer jump */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE),
+ /* Unknown 2 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
+};
+
+void DummyPlayer_Init(Actor* actor, PlayState* play) {
+ Player* player = (Player*)actor;
+
+ uint32_t clientId = Anchor::Instance->actorIndexToClientId[actor->params];
+ DUMMY_CLIENT_ID = clientId;
+
+ if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
+ Actor_Kill(actor);
+ return;
+ }
+
+ AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
+
+ // Hack to account for usage of gSaveContext in Player_Init
+ s32 originalAge = gSaveContext.linkAge;
+ gSaveContext.linkAge = client.linkAge;
+
+ // #region modeled after EnTorch2_Init and Player_Init
+ actor->room = -1;
+ player->itemAction = player->heldItemAction = -1;
+ player->heldItemId = ITEM_NONE;
+ Player_UseItem(play, player, ITEM_NONE);
+ Player_SetModelGroup(player, Player_ActionToModelGroup(player, player->heldItemAction));
+ play->playerInit(player, play, gPlayerSkelHeaders[client.linkAge]);
+
+ play->func_11D54(player, play);
+ // #endregion
+
+ player->cylinder.base.acFlags = AC_ON | AC_TYPE_PLAYER;
+ player->cylinder.base.ocFlags2 = OC2_TYPE_1;
+ player->cylinder.info.bumperFlags = BUMP_ON | BUMP_HOOKABLE | BUMP_NO_HITMARK;
+ player->actor.flags |= ACTOR_FLAG_DRAGGED_BY_HOOKSHOT;
+ player->cylinder.dim.radius = 30;
+ player->actor.colChkInfo.damageTable = &DummyPlayerDamageTable;
+
+ gSaveContext.linkAge = originalAge;
+
+ NameTag_RegisterForActorWithOptions(actor, client.name.c_str(), { .yOffset = 30 });
+}
+
+void Math_Vec3s_Copy(Vec3s* dest, Vec3s* src) {
+ dest->x = src->x;
+ dest->y = src->y;
+ dest->z = src->z;
+}
+
+// Update the actor with new data from the client
+void DummyPlayer_Update(Actor* actor, PlayState* play) {
+ Player* player = (Player*)actor;
+
+ if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
+ Actor_Kill(actor);
+ return;
+ }
+
+ AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
+
+ if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) {
+ actor->world.pos.x = -9999.0f;
+ actor->world.pos.y = -9999.0f;
+ actor->world.pos.z = -9999.0f;
+ actor->shape.shadowAlpha = 0;
+ return;
+ }
+
+ actor->shape.shadowAlpha = 255;
+ Math_Vec3s_Copy(&player->upperLimbRot, &client.upperLimbRot);
+ Math_Vec3s_Copy(&actor->shape.rot, &client.posRot.rot);
+ Math_Vec3f_Copy(&actor->world.pos, &client.posRot.pos);
+ player->skelAnime.jointTable = client.jointTable;
+ player->currentBoots = client.currentBoots;
+ player->currentShield = client.currentShield;
+ player->currentTunic = client.currentTunic;
+ player->stateFlags1 = client.stateFlags1;
+ player->stateFlags2 = client.stateFlags2;
+ player->itemAction = client.itemAction;
+ player->heldItemAction = client.heldItemAction;
+ player->invincibilityTimer = client.invincibilityTimer;
+ player->unk_862 = client.unk_862;
+ player->av1.actionVar1 = client.actionVar1;
+
+ if (player->modelGroup != client.modelGroup) {
+ // 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);
+ gSaveContext.linkAge = originalAge;
+ gSaveContext.equips.buttonItems[0] = originalButtonItem0;
+ }
+
+ if (
+ Anchor::Instance->roomState.pvpMode == 0 ||
+ (Anchor::Instance->roomState.pvpMode == 1 && client.teamId == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"))
+ ) {
+ return;
+ }
+
+ if (player->cylinder.base.acFlags & AC_HIT && player->invincibilityTimer == 0) {
+ Anchor::Instance->SendPacket_DamagePlayer(client.clientId, player->actor.colChkInfo.damageEffect, player->actor.colChkInfo.damage);
+ if (player->actor.colChkInfo.damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) {
+ Actor_SetColorFilter(&player->actor, 0, 0xFF, 0, 24);
+ } else {
+ player->invincibilityTimer = 20;
+ }
+ }
+
+ Collider_UpdateCylinder(&player->actor, &player->cylinder);
+
+ if (!(player->stateFlags2 & PLAYER_STATE2_FROZEN)) {
+ if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_HANGING_OFF_LEDGE | PLAYER_STATE1_CLIMBING_LEDGE | PLAYER_STATE1_ON_HORSE))) {
+ CollisionCheck_SetOC(play, &play->colChkCtx, &player->cylinder.base);
+ }
+
+ if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_DAMAGED)) && (player->invincibilityTimer <= 0)) {
+ CollisionCheck_SetAC(play, &play->colChkCtx, &player->cylinder.base);
+
+ if (player->invincibilityTimer < 0) {
+ CollisionCheck_SetAT(play, &play->colChkCtx, &player->cylinder.base);
+ }
+ }
+ }
+
+ if (player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_IN_ITEM_CS | PLAYER_STATE1_IN_CUTSCENE)) {
+ player->actor.colChkInfo.mass = MASS_IMMOVABLE;
+ } else {
+ player->actor.colChkInfo.mass = 50;
+ }
+
+ Collider_ResetCylinderAC(play, &player->cylinder.base);
+}
+
+void DummyPlayer_Draw(Actor* actor, PlayState* play) {
+ Player* player = (Player*)actor;
+
+ if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
+ Actor_Kill(actor);
+ return;
+ }
+
+ AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
+
+ if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) {
+ return;
+ }
+
+ // Hack to account for usage of gSaveContext in Player_Draw
+ s32 originalAge = gSaveContext.linkAge;
+ gSaveContext.linkAge = client.linkAge;
+ u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0];
+ gSaveContext.equips.buttonItems[0] = client.buttonItem0;
+
+ Player_Draw((Actor*)player, play);
+ gSaveContext.linkAge = originalAge;
+ gSaveContext.equips.buttonItems[0] = originalButtonItem0;
+}
+
+void DummyPlayer_Destroy(Actor* actor, PlayState* play) {
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp
new file mode 100644
index 000000000..3ec04f043
--- /dev/null
+++ b/soh/soh/Network/Anchor/JsonConversions.hpp
@@ -0,0 +1,186 @@
+#ifdef ENABLE_REMOTE_CONTROL
+#ifndef NETWORK_ANCHOR_JSON_CONVERSIONS_H
+#define NETWORK_ANCHOR_JSON_CONVERSIONS_H
+#ifdef __cplusplus
+
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "Anchor.h"
+
+extern "C" {
+#include "z64.h"
+}
+
+using json = nlohmann::json;
+
+inline void from_json(const json& j, Color_RGB8& color) {
+ j.at("r").get_to(color.r);
+ j.at("g").get_to(color.g);
+ j.at("b").get_to(color.b);
+}
+
+inline void to_json(json& j, const Color_RGB8& color) {
+ j = json{
+ {"r", color.r},
+ {"g", color.g},
+ {"b", color.b}
+ };
+}
+
+inline void to_json(json& j, const Vec3f& vec) {
+ j = json{
+ {"x", vec.x},
+ {"y", vec.y},
+ {"z", vec.z}
+ };
+}
+
+inline void to_json(json& j, const Vec3s& vec) {
+ j = json{
+ {"x", vec.x},
+ {"y", vec.y},
+ {"z", vec.z}
+ };
+}
+
+inline void from_json(const json& j, Vec3f& vec) {
+ j.at("x").get_to(vec.x);
+ j.at("y").get_to(vec.y);
+ j.at("z").get_to(vec.z);
+}
+
+inline void from_json(const json& j, Vec3s& vec) {
+ j.at("x").get_to(vec.x);
+ j.at("y").get_to(vec.y);
+ j.at("z").get_to(vec.z);
+}
+
+inline void to_json(json& j, const PosRot& posRot) {
+ j = json{
+ {"pos", posRot.pos},
+ {"rot", posRot.rot}
+ };
+}
+
+inline void from_json(const json& j, PosRot& posRot) {
+ j.at("pos").get_to(posRot.pos);
+ j.at("rot").get_to(posRot.rot);
+}
+
+inline void from_json(const json& j, AnchorClient& client) {
+ j.contains("clientId") ? j.at("clientId").get_to(client.clientId) : client.clientId = 0;
+ j.contains("name") ? j.at("name").get_to(client.name) : client.name = "???";
+ j.contains("color") ? j.at("color").get_to(client.color) : client.color = { 255, 255, 255 };
+ j.contains("clientVersion") ? j.at("clientVersion").get_to(client.clientVersion) : client.clientVersion = "???";
+ j.contains("teamId") ? j.at("teamId").get_to(client.teamId) : client.teamId = "default";
+ j.contains("online") ? j.at("online").get_to(client.online) : client.online = false;
+ j.contains("seed") ? j.at("seed").get_to(client.seed) : client.seed = 0;
+ j.contains("isSaveLoaded") ? j.at("isSaveLoaded").get_to(client.isSaveLoaded) : client.isSaveLoaded = false;
+ j.contains("isGameComplete") ? j.at("isGameComplete").get_to(client.isGameComplete) : client.isGameComplete = false;
+ j.contains("sceneNum") ? j.at("sceneNum").get_to(client.sceneNum) : client.sceneNum = SCENE_ID_MAX;
+ j.contains("entranceIndex") ? j.at("entranceIndex").get_to(client.entranceIndex) : client.entranceIndex = 0;
+ j.contains("self") ? j.at("self").get_to(client.self) : client.self = false;
+}
+
+inline void to_json(json& j, const Inventory& inventory) {
+ j = json{
+ {"items", inventory.items},
+ {"ammo", inventory.ammo},
+ {"equipment", inventory.equipment},
+ {"upgrades", inventory.upgrades},
+ {"questItems", inventory.questItems},
+ {"dungeonItems", inventory.dungeonItems},
+ {"dungeonKeys", inventory.dungeonKeys},
+ {"defenseHearts", inventory.defenseHearts},
+ {"gsTokens", inventory.gsTokens}
+ };
+}
+
+inline void from_json(const json& j, Inventory& inventory) {
+ j.at("items").get_to(inventory.items);
+ j.at("ammo").get_to(inventory.ammo);
+ j.at("equipment").get_to(inventory.equipment);
+ j.at("upgrades").get_to(inventory.upgrades);
+ j.at("questItems").get_to(inventory.questItems);
+ j.at("dungeonItems").get_to(inventory.dungeonItems);
+ j.at("dungeonKeys").get_to(inventory.dungeonKeys);
+ j.at("defenseHearts").get_to(inventory.defenseHearts);
+ j.at("gsTokens").get_to(inventory.gsTokens);
+}
+
+inline void to_json(json& j, const SohStats& sohStats) {
+ j = json{
+ {"entrancesDiscovered", sohStats.entrancesDiscovered},
+ {"fileCreatedAt", sohStats.fileCreatedAt},
+ };
+}
+
+inline void from_json(const json& j, SohStats& sohStats) {
+ j.at("entrancesDiscovered").get_to(sohStats.entrancesDiscovered);
+ j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt);
+}
+
+inline void to_json(json& j, const SaveContext& saveContext) {
+ std::vector<u32> sceneFlagsArray;
+ for (const auto& sceneFlags : saveContext.sceneFlags) {
+ sceneFlagsArray.push_back(sceneFlags.chest);
+ sceneFlagsArray.push_back(sceneFlags.swch);
+ sceneFlagsArray.push_back(sceneFlags.clear);
+ sceneFlagsArray.push_back(sceneFlags.collect);
+ }
+
+ j = json{
+ {"healthCapacity", saveContext.healthCapacity},
+ {"magicLevel", saveContext.magicLevel},
+ {"magicCapacity", saveContext.magicCapacity},
+ {"isMagicAcquired", saveContext.isMagicAcquired},
+ {"isDoubleMagicAcquired", saveContext.isDoubleMagicAcquired},
+ {"isDoubleDefenseAcquired", saveContext.isDoubleDefenseAcquired},
+ {"bgsFlag", saveContext.bgsFlag},
+ {"swordHealth", saveContext.swordHealth},
+ {"sceneFlags", sceneFlagsArray},
+ {"eventChkInf", saveContext.eventChkInf},
+ {"itemGetInf", saveContext.itemGetInf},
+ {"infTable", saveContext.infTable},
+ {"randomizerInf", saveContext.randomizerInf},
+ {"gsFlags", saveContext.gsFlags},
+ {"inventory", saveContext.inventory},
+ {"sohStats", saveContext.sohStats},
+ {"adultTradeItems", saveContext.adultTradeItems},
+ {"triforcePiecesCollected", saveContext.triforcePiecesCollected},
+ {"questId", saveContext.questId},
+ };
+}
+
+inline void from_json(const json& j, SaveContext& saveContext) {
+ j.at("healthCapacity").get_to(saveContext.healthCapacity);
+ j.at("magicLevel").get_to(saveContext.magicLevel);
+ j.at("magicCapacity").get_to(saveContext.magicCapacity);
+ j.at("isMagicAcquired").get_to(saveContext.isMagicAcquired);
+ j.at("isDoubleMagicAcquired").get_to(saveContext.isDoubleMagicAcquired);
+ j.at("isDoubleDefenseAcquired").get_to(saveContext.isDoubleDefenseAcquired);
+ j.at("bgsFlag").get_to(saveContext.bgsFlag);
+ j.at("swordHealth").get_to(saveContext.swordHealth);
+ std::vector<u32> sceneFlagsArray;
+ j.at("sceneFlags").get_to(sceneFlagsArray);
+ for (int i = 0; i < 124; i++) {
+ saveContext.sceneFlags[i].chest = sceneFlagsArray[i * 4];
+ saveContext.sceneFlags[i].swch = sceneFlagsArray[i * 4 + 1];
+ saveContext.sceneFlags[i].clear = sceneFlagsArray[i * 4 + 2];
+ saveContext.sceneFlags[i].collect = sceneFlagsArray[i * 4 + 3];
+ }
+ j.at("eventChkInf").get_to(saveContext.eventChkInf);
+ j.at("itemGetInf").get_to(saveContext.itemGetInf);
+ j.at("infTable").get_to(saveContext.infTable);
+ j.at("randomizerInf").get_to(saveContext.randomizerInf);
+ j.at("gsFlags").get_to(saveContext.gsFlags);
+ j.at("inventory").get_to(saveContext.inventory);
+ j.at("sohStats").get_to(saveContext.sohStats);
+ j.at("adultTradeItems").get_to(saveContext.adultTradeItems);
+ j.at("triforcePiecesCollected").get_to(saveContext.triforcePiecesCollected);
+ j.at("questId").get_to(saveContext.questId);
+}
+
+#endif // __cplusplus
+#endif // NETWORK_ANCHOR_JSON_CONVERSIONS_H
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/AllClientState.cpp b/soh/soh/Network/Anchor/Packets/AllClientState.cpp
new file mode 100644
index 000000000..d9d455535
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/AllClientState.cpp
@@ -0,0 +1,74 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include "soh/Network/Anchor/JsonConversions.hpp"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/OTRGlobals.h"
+#include "soh/Notification/Notification.h"
+
+/**
+ * ALL_CLIENT_STATE
+ *
+ * Contains a list of all clients and their CLIENT_STATE currently connected to the server
+ *
+ * The server itself sends this packet to all clients when a client connects or disconnects
+ */
+
+void Anchor::HandlePacket_AllClientState(nlohmann::json payload) {
+ std::vector<AnchorClient> newClients = payload["state"].get<std::vector<AnchorClient>>();
+
+ // add new clients
+ for (auto& client : newClients) {
+ if (client.self) {
+ ownClientId = client.clientId;
+ CVarSetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), ownClientId);
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+ clients[client.clientId].self = true;
+ } else {
+ clients[client.clientId].self = false;
+ if (clients.contains(client.clientId)) {
+ if (clients[client.clientId].online != client.online) {
+ Notification::Emit({
+ .prefix = client.name,
+ .message = client.online ? "Connected" : "Disconnected",
+ });
+ }
+ } else if (client.online) {
+ Notification::Emit({
+ .prefix = client.name,
+ .message = "Connected",
+ });
+ }
+ }
+
+ clients[client.clientId].clientId = client.clientId;
+ clients[client.clientId].name = client.name;
+ clients[client.clientId].color = client.color;
+ clients[client.clientId].clientVersion = client.clientVersion;
+ clients[client.clientId].teamId = client.teamId;
+ clients[client.clientId].online = client.online;
+ clients[client.clientId].seed = client.seed;
+ clients[client.clientId].isSaveLoaded = client.isSaveLoaded;
+ clients[client.clientId].isGameComplete = client.isGameComplete;
+ clients[client.clientId].sceneNum = client.sceneNum;
+ clients[client.clientId].entranceIndex = client.entranceIndex;
+ }
+
+ // remove clients that are no longer in the list
+ std::vector<uint32_t> clientsToRemove;
+ for (auto& [clientId, client] : clients) {
+ if (std::find_if(newClients.begin(), newClients.end(),
+ [clientId](AnchorClient& c) { return c.clientId == clientId; }) == newClients.end()) {
+ clientsToRemove.push_back(clientId);
+ }
+ }
+ // (seperate loop to avoid iterator invalidation)
+ for (auto& clientId : clientsToRemove) {
+ clients.erase(clientId);
+ }
+
+ RefreshClientActors();
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/ConsumeAdultTradeItem.cpp b/soh/soh/Network/Anchor/Packets/ConsumeAdultTradeItem.cpp
new file mode 100644
index 000000000..645916f6d
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/ConsumeAdultTradeItem.cpp
@@ -0,0 +1,46 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/OTRGlobals.h"
+
+extern "C" {
+#include "functions.h"
+#include "soh/Enhancements/randomizer/adult_trade_shuffle.h"
+extern PlayState* gPlayState;
+}
+
+/**
+ * CONSUME_ADULT_TRADE_ITEM
+ *
+ * This is primarily to just get rid of used adult trade items to prevent confusion for other players.
+ * Whatever flags/items are given from adult trade checks are synced by other packets.
+ */
+
+void Anchor::SendPacket_ConsumeAdultTradeItem(u8 itemId) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ nlohmann::json payload;
+ payload["type"] = CONSUME_ADULT_TRADE_ITEM;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["addToQueue"] = true;
+ payload["itemId"] = itemId;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_ConsumeAdultTradeItem(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ uint8_t itemId = payload["itemId"].get<uint8_t>();
+ gSaveContext.adultTradeItems &= ~ADULT_TRADE_FLAG(itemId);
+ Inventory_ReplaceItem(gPlayState, itemId, Randomizer_GetNextAdultTradeItem());
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp b/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp
new file mode 100644
index 000000000..e2da27eef
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp
@@ -0,0 +1,62 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+
+extern "C" {
+#include "macros.h"
+#include "functions.h"
+extern PlayState* gPlayState;
+void func_80838280(Player* player);
+}
+
+/**
+ * DAMAGE_PLAYER
+ */
+
+void Anchor::SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ nlohmann::json payload;
+ payload["type"] = DAMAGE_PLAYER;
+ payload["targetClientId"] = clientId;
+ payload["damageEffect"] = damageEffect;
+ payload["damage"] = damage;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_DamagePlayer(nlohmann::json payload) {
+ uint32_t clientId = payload["clientId"].get<uint32_t>();
+ if (!clients.contains(clientId) || clients[clientId].player == nullptr) {
+ return;
+ }
+
+ AnchorClient& anchorClient = clients[clientId];
+ Player* otherPlayer = anchorClient.player;
+ Player* self = GET_PLAYER(gPlayState);
+
+ u8 damageEffect = payload["damageEffect"].get<u8>();
+ u8 damage = payload["damage"].get<u8>();
+
+ self->actor.colChkInfo.damage = damage * 8; // Arbitrary number currently, need to fine tune
+
+ if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_FIRE) {
+ for (int i = 0; i < ARRAY_COUNT(self->bodyFlameTimers); i++) {
+ self->bodyFlameTimers[i] = Rand_S16Offset(0, 200);
+ }
+ self->bodyIsBurning = true;
+ } else if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) {
+ self->actor.freezeTimer = 20;
+ Actor_SetColorFilter(&self->actor, 0, 0xFF, 0, 24);
+ return;
+ }
+
+ func_80837C0C(gPlayState, self, damageEffect, 4.0f, 5.0f, Actor_WorldYawTowardActor(&otherPlayer->actor, &self->actor), 20);
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp b/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp
new file mode 100644
index 000000000..6f01ddef6
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp
@@ -0,0 +1,18 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+
+/**
+ * DISABLE_ANCHOR
+ *
+ * No current use, potentially will be used for a future feature.
+ */
+
+void Anchor::HandlePacket_DisableAnchor(nlohmann::json payload) {
+ Disable();
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp b/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp
new file mode 100644
index 000000000..f5c27a82f
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp
@@ -0,0 +1,37 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/Enhancements/randomizer/randomizer_entrance.h"
+#include "soh/OTRGlobals.h"
+
+/**
+ * ENTRANCE_DISCOVERED
+ */
+
+void Anchor::SendPacket_EntranceDiscovered(u16 entranceIndex) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ nlohmann::json payload;
+ payload["type"] = ENTRANCE_DISCOVERED;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["entranceIndex"] = entranceIndex;
+ payload["quiet"] = true;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_EntranceDiscovered(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ u16 entranceIndex = payload["entranceIndex"].get<u16>();
+ Entrance_SetEntranceDiscovered(entranceIndex, 1);
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/GameComplete.cpp b/soh/soh/Network/Anchor/Packets/GameComplete.cpp
new file mode 100644
index 000000000..cd7d72d5b
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/GameComplete.cpp
@@ -0,0 +1,48 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/Notification/Notification.h"
+#include "soh/Enhancements/randomizer/3drando/random.hpp"
+
+const std::string gameCompleteMessages[] = {
+ "killed Ganon",
+ "saved Zelda",
+ "proved their Courage",
+ "collected the Triforce",
+ "is the Hero of Time",
+ "proved Mido wrong",
+};
+
+/**
+ * GAME_COMPLETE
+ */
+
+void Anchor::SendPacket_GameComplete() {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ nlohmann::json payload;
+ payload["type"] = GAME_COMPLETE;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_GameComplete(nlohmann::json payload) {
+ uint32_t clientId = payload["clientId"].get<uint32_t>();
+ if (!clients.contains(clientId)) {
+ return;
+ }
+
+ AnchorClient& anchorClient = clients[clientId];
+ anchorClient.isGameComplete = true;
+ Notification::Emit({
+ .prefix = anchorClient.name,
+ .message = RandomElement(gameCompleteMessages),
+ });
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/GiveItem.cpp b/soh/soh/Network/Anchor/Packets/GiveItem.cpp
new file mode 100644
index 000000000..f6a6e05d2
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/GiveItem.cpp
@@ -0,0 +1,104 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/Notification/Notification.h"
+#include "soh/Enhancements/randomizer/randomizer.h"
+#include "soh/ImGuiUtils.h"
+#include "soh/Enhancements/item-tables/ItemTableManager.h"
+#include "soh/OTRGlobals.h"
+
+extern "C" {
+#include "functions.h"
+extern PlayState* gPlayState;
+}
+
+/**
+ * GIVE_ITEM
+ */
+
+static bool gettingItem;
+static uint8_t incomingIceTraps;
+
+void Anchor::SendPacket_GiveItem(u16 modId, s16 getItemId) {
+ if (!IsSaveLoaded() || gettingItem) {
+ return;
+ }
+
+ if (modId == MOD_RANDOMIZER && getItemId == RG_ICE_TRAP && incomingIceTraps > 0) {
+ incomingIceTraps = MAX(incomingIceTraps - 1, 0);
+ return;
+ }
+
+ nlohmann::json payload;
+ payload["type"] = GIVE_ITEM;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["addToQueue"] = true;
+ payload["modId"] = modId;
+ payload["getItemId"] = getItemId;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_GiveItem(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ uint32_t clientId = payload["clientId"].get<uint32_t>();
+ AnchorClient& client = clients[clientId];
+
+ GetItemEntry getItemEntry;
+ if (payload["modId"].get<u16>() == MOD_NONE) {
+ getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, payload["getItemId"].get<s16>());
+ } else {
+ getItemEntry = Rando::StaticData::RetrieveItem(payload["getItemId"].get<RandomizerGet>()).GetGIEntry_Copy();
+ }
+
+ gettingItem = true;
+ if (getItemEntry.modIndex == MOD_NONE) {
+ if (getItemEntry.getItemId == GI_SWORD_BGS) {
+ gSaveContext.bgsFlag = true;
+ }
+ Item_Give(gPlayState, getItemEntry.itemId);
+ } else if (getItemEntry.modIndex == MOD_RANDOMIZER) {
+ if (getItemEntry.getItemId == RG_ICE_TRAP) {
+ gSaveContext.pendingIceTrapCount++;
+ incomingIceTraps++;
+ } else {
+ Randomizer_Item_Give(gPlayState, getItemEntry);
+ }
+ }
+
+ // Handle if the player gets a 4th heart piece (usually handled in z_message)
+ s32 heartPieces = (s32)(gSaveContext.inventory.questItems & 0xF0000000) >> (QUEST_HEART_PIECE + 4);
+ if (heartPieces >= 4) {
+ gSaveContext.inventory.questItems &= ~0xF0000000;
+ gSaveContext.inventory.questItems += (heartPieces % 4) << (QUEST_HEART_PIECE + 4);
+ gSaveContext.healthCapacity += 0x10 * (heartPieces / 4);
+ gSaveContext.health += 0x10 * (heartPieces / 4);
+ gSaveContext.healthAccumulator = 0x140;
+ }
+ gettingItem = false;
+
+ if (getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK) {
+ if (getItemEntry.modIndex == MOD_NONE) {
+ Notification::Emit({
+ .itemIcon = GetTextureForItemId(getItemEntry.itemId),
+ .prefix = client.name,
+ .message = "found",
+ .suffix = SohUtils::GetItemName(getItemEntry.itemId),
+ });
+ } else if (getItemEntry.modIndex == MOD_RANDOMIZER) {
+ Notification::Emit({
+ .prefix = client.name,
+ .message = "found",
+ .suffix = Rando::StaticData::RetrieveItem((RandomizerGet)getItemEntry.getItemId).GetName().english,
+ });
+ }
+ }
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/Handshake.cpp b/soh/soh/Network/Anchor/Packets/Handshake.cpp
new file mode 100644
index 000000000..592da4932
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/Handshake.cpp
@@ -0,0 +1,26 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/OTRGlobals.h"
+
+/**
+ * HANDSHAKE
+ *
+ * Sent by the client to the server when it first connects to the server, sends over both the local room settings
+ * in case the room needs to be created, along with the current client state
+ */
+
+void Anchor::SendPacket_Handshake() {
+ nlohmann::json payload;
+ payload["type"] = HANDSHAKE;
+ payload["roomId"] = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "");
+ payload["roomState"] = PrepRoomState();
+ payload["clientState"] = PrepClientState();
+
+ SendJsonToRemote(payload);
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp b/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp
new file mode 100644
index 000000000..dc2741d9a
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp
@@ -0,0 +1,51 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#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;
+}
+
+/**
+ * PLAYER_SFX
+ *
+ * Sound effects, only sent to other clients in the same scene as the player
+ */
+
+void Anchor::SendPacket_PlayerSfx(u16 sfxId) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ nlohmann::json payload;
+
+ payload["type"] = PLAYER_SFX;
+ payload["sfxId"] = sfxId;
+ payload["quiet"] = true;
+
+ for (auto& [clientId, client] : clients) {
+ if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded) {
+ payload["targetClientId"] = clientId;
+ SendJsonToRemote(payload);
+ }
+ }
+}
+
+void Anchor::HandlePacket_PlayerSfx(nlohmann::json payload) {
+ uint32_t clientId = payload["clientId"].get<uint32_t>();
+ u16 sfxId = payload["sfxId"].get<u16>();
+
+ if (!clients.contains(clientId) || !clients[clientId].player) {
+ return;
+ }
+
+ Player_PlaySfx((Actor*)clients[clientId].player, sfxId);
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp
new file mode 100644
index 000000000..7a604d802
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp
@@ -0,0 +1,120 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#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 "variables.h"
+extern PlayState* gPlayState;
+}
+
+/**
+ * PLAYER_UPDATE
+ *
+ * Contains real-time data necessary to update other clients in the same scene as the player
+ *
+ * Sent every frame to other clients within the same scene
+ *
+ * Note: This packet is sent _a lot_, so please do not include any unnecessary data in it
+ */
+
+void Anchor::SendPacket_PlayerUpdate() {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ uint32_t currentPlayerCount = 0;
+ for (auto& [clientId, client] : clients) {
+ if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded) {
+ currentPlayerCount++;
+ }
+ }
+ if (currentPlayerCount == 0) {
+ return;
+ }
+
+ Player* player = GET_PLAYER(gPlayState);
+ nlohmann::json payload;
+
+ payload["type"] = PLAYER_UPDATE;
+ payload["sceneNum"] = gPlayState->sceneNum;
+ payload["entranceIndex"] = gSaveContext.entranceIndex;
+ payload["linkAge"] = gSaveContext.linkAge;
+ payload["posRot"]["pos"] = player->actor.world.pos;
+ payload["posRot"]["rot"] = player->actor.shape.rot;
+ std::vector<int> jointArray;
+ for (const auto& joint : player->jointTable) {
+ jointArray.push_back(joint.x);
+ jointArray.push_back(joint.y);
+ jointArray.push_back(joint.z);
+ }
+ payload["jointTable"] = jointArray;
+ payload["upperLimbRot"] = player->upperLimbRot;
+ payload["currentBoots"] = player->currentBoots;
+ payload["currentShield"] = player->currentShield;
+ payload["currentTunic"] = player->currentTunic;
+ payload["stateFlags1"] = player->stateFlags1;
+ payload["stateFlags2"] = player->stateFlags2;
+ payload["buttonItem0"] = gSaveContext.equips.buttonItems[0];
+ payload["itemAction"] = player->itemAction;
+ payload["heldItemAction"] = player->heldItemAction;
+ payload["modelGroup"] = player->modelGroup;
+ payload["invincibilityTimer"] = player->invincibilityTimer;
+ payload["unk_862"] = player->unk_862;
+ payload["actionVar1"] = player->av1.actionVar1;
+ payload["quiet"] = true;
+
+ for (auto& [clientId, client] : clients) {
+ if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded) {
+ payload["targetClientId"] = clientId;
+ SendJsonToRemote(payload);
+ }
+ }
+}
+
+void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) {
+ uint32_t clientId = payload["clientId"].get<uint32_t>();
+
+ bool shouldRefreshActors = false;
+
+ if (clients.contains(clientId)) {
+ auto& client = clients[clientId];
+
+ if (client.linkAge != payload["linkAge"].get<s32>()) {
+ shouldRefreshActors = true;
+ }
+
+ client.sceneNum = payload["sceneNum"].get<s16>();
+ client.entranceIndex = payload["entranceIndex"].get<s32>();
+ client.linkAge = payload["linkAge"].get<s32>();
+ client.posRot = payload["posRot"].get<PosRot>();
+ std::vector<int> jointArray = payload["jointTable"];
+ for (int i = 0; i < 24; i++) {
+ client.jointTable[i].x = jointArray[i * 3];
+ client.jointTable[i].y = jointArray[i * 3 + 1];
+ client.jointTable[i].z = jointArray[i * 3 + 2];
+ }
+ client.upperLimbRot = payload["upperLimbRot"].get<Vec3s>();
+ client.currentBoots = payload["currentBoots"].get<s8>();
+ client.currentShield = payload["currentShield"].get<s8>();
+ client.currentTunic = payload["currentTunic"].get<s8>();
+ client.stateFlags1 = payload["stateFlags1"].get<u32>();
+ client.stateFlags2 = payload["stateFlags2"].get<u32>();
+ client.buttonItem0 = payload["buttonItem0"].get<u8>();
+ client.itemAction = payload["itemAction"].get<s8>();
+ client.heldItemAction = payload["heldItemAction"].get<s8>();
+ client.modelGroup = payload["modelGroup"].get<u8>();
+ client.invincibilityTimer = payload["invincibilityTimer"].get<s8>();
+ client.unk_862 = payload["unk_862"].get<s16>();
+ client.actionVar1 = payload["actionVar1"].get<s8>();
+ }
+
+ if (shouldRefreshActors) {
+ RefreshClientActors();
+ }
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp b/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp
new file mode 100644
index 000000000..f2a556dd1
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp
@@ -0,0 +1,37 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/OTRGlobals.h"
+
+/**
+ * REQUEST_TEAM_STATE
+ *
+ * Requests team state from the server, which will pass on the request to any connected teammates, or send the last known
+ * state if no teammates are connected.
+ *
+ * This fires when loading into a file while Anchor is connected, or when Anchor is connected while a file is already
+ * loaded
+ *
+ * Note: This can additionally be fired with a button in the menus to fix any desyncs that may have occurred in the save
+ * state
+ */
+
+void Anchor::SendPacket_RequestTeamState() {
+ nlohmann::json payload;
+ payload["type"] = REQUEST_TEAM_STATE;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_RequestTeamState(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ SendPacket_UpdateTeamState();
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp b/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp
new file mode 100644
index 000000000..fdd79e536
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp
@@ -0,0 +1,36 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+
+/**
+ * REQUEST_TELEPORT
+ *
+ * Because we don't have all the necessary information to directly teleport to a player, we emit a request,
+ * in which they will respond with a TELEPORT_TO packet, with the necessary information.
+ */
+
+void Anchor::SendPacket_RequestTeleport(uint32_t clientId) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ nlohmann::json payload;
+ payload["type"] = REQUEST_TELEPORT;
+ payload["targetClientId"] = clientId;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_RequestTeleport(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ uint32_t clientId = payload["clientId"].get<uint32_t>();
+ SendPacket_TeleportTo(clientId);
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/ServerMessage.cpp b/soh/soh/Network/Anchor/Packets/ServerMessage.cpp
new file mode 100644
index 000000000..3af07c8b8
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/ServerMessage.cpp
@@ -0,0 +1,21 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/Notification/Notification.h"
+
+/**
+ * SERVER_MESSAGE
+ */
+
+void Anchor::HandlePacket_ServerMessage(nlohmann::json payload) {
+ Notification::Emit({
+ .prefix = "Server:",
+ .prefixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f),
+ .message = payload["message"].get<std::string>(),
+ });
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp
new file mode 100644
index 000000000..276e52586
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp
@@ -0,0 +1,53 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/OTRGlobals.h"
+
+/**
+ * SET_CHECK_STATUS
+ *
+ * Fired when a check status is updated or skipped
+ */
+
+void Anchor::SendPacket_SetCheckStatus(RandomizerCheck rc) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ auto randoContext = Rando::Context::GetInstance();
+
+ nlohmann::json payload;
+ payload["type"] = SET_CHECK_STATUS;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["addToQueue"] = true;
+ payload["rc"] = rc;
+ payload["status"] = randoContext->GetItemLocation(rc)->GetCheckStatus();
+ payload["skipped"] = randoContext->GetItemLocation(rc)->GetIsSkipped();
+ payload["quiet"] = true;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ auto randoContext = Rando::Context::GetInstance();
+
+ RandomizerCheck rc = payload["rc"].get<RandomizerCheck>();
+ RandomizerCheckStatus status = payload["status"].get<RandomizerCheckStatus>();
+ bool skipped = payload["skipped"].get<bool>();
+
+ if (randoContext->GetItemLocation(rc)->GetCheckStatus() != status) {
+ randoContext->GetItemLocation(rc)->SetCheckStatus(status);
+ }
+ if (randoContext->GetItemLocation(rc)->GetIsSkipped() != skipped) {
+ randoContext->GetItemLocation(rc)->SetIsSkipped(skipped);
+ }
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/SetFlag.cpp b/soh/soh/Network/Anchor/Packets/SetFlag.cpp
new file mode 100644
index 000000000..b7438ab95
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/SetFlag.cpp
@@ -0,0 +1,50 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/OTRGlobals.h"
+
+/**
+ * SET_FLAG
+ *
+ * Fired when a flag is set in the save context
+ */
+
+void Anchor::SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag) {
+ nlohmann::json payload;
+ payload["type"] = SET_FLAG;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["addToQueue"] = true;
+ payload["sceneNum"] = sceneNum;
+ payload["flagType"] = flagType;
+ payload["flag"] = flag;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_SetFlag(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ s16 sceneNum = payload["sceneNum"].get<s16>();
+ s16 flagType = payload["flagType"].get<s16>();
+ s16 flag = payload["flag"].get<s16>();
+
+ if (sceneNum == SCENE_ID_MAX) {
+ auto effect = new GameInteractionEffect::SetFlag();
+ effect->parameters[0] = payload["flagType"].get<int16_t>();
+ effect->parameters[1] = payload["flag"].get<int16_t>();
+ effect->Apply();
+ } else {
+ auto effect = new GameInteractionEffect::SetSceneFlag();
+ effect->parameters[0] = payload["sceneNum"].get<int16_t>();
+ effect->parameters[1] = payload["flagType"].get<int16_t>();
+ effect->parameters[2] = payload["flag"].get<int16_t>();
+ effect->Apply();
+ }
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp
new file mode 100644
index 000000000..05e3cbb83
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp
@@ -0,0 +1,63 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/Network/Anchor/JsonConversions.hpp"
+
+extern "C" {
+#include "macros.h"
+extern PlayState* gPlayState;
+}
+
+/**
+ * TELEPORT_TO
+ *
+ * See REQUEST_TELEPORT for more information, this is the second part of the process.
+ */
+
+void Anchor::SendPacket_TeleportTo(uint32_t clientId) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ Player* player = GET_PLAYER(gPlayState);
+
+ nlohmann::json payload;
+ payload["type"] = TELEPORT_TO;
+ payload["targetClientId"] = clientId;
+ payload["entranceIndex"] = gSaveContext.entranceIndex;
+ payload["roomIndex"] = gPlayState->roomCtx.curRoom.num;
+ payload["posRot"] = player->actor.world;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_TeleportTo(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ s32 entranceIndex = payload["entranceIndex"].get<s32>();
+ s8 roomIndex = payload["roomIndex"].get<s8>();
+ PosRot posRot = payload["posRot"].get<PosRot>();
+
+ gPlayState->nextEntranceIndex = entranceIndex;
+ gPlayState->transitionTrigger = TRANS_TRIGGER_START;
+ gPlayState->transitionType = TRANS_TYPE_INSTANT;
+ gSaveContext.respawn[RESPAWN_MODE_DOWN].entranceIndex = entranceIndex;
+ gSaveContext.respawn[RESPAWN_MODE_DOWN].roomIndex = roomIndex;
+ gSaveContext.respawn[RESPAWN_MODE_DOWN].pos = posRot.pos;
+ gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = posRot.rot.y;
+ gSaveContext.respawn[RESPAWN_MODE_DOWN].playerParams = 0xDFF;
+ gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK_FAST;
+ gSaveContext.respawnFlag = 1;
+ static HOOK_ID hookId = 0;
+ hookId = REGISTER_VB_SHOULD(VB_INFLICT_VOID_DAMAGE, {
+ *should = false;
+ GameInteractor::Instance->UnregisterGameHookForID<GameInteractor::OnVanillaBehavior>(hookId);
+ });
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp
new file mode 100644
index 000000000..91da3f9b1
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp
@@ -0,0 +1,51 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/OTRGlobals.h"
+
+/**
+ * UNSET_FLAG
+ *
+ * Fired when a flag is unset in the save context
+ */
+
+void Anchor::SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag) {
+ nlohmann::json payload;
+ payload["type"] = UNSET_FLAG;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["addToQueue"] = true;
+ payload["sceneNum"] = sceneNum;
+ payload["flagType"] = flagType;
+ payload["flag"] = flag;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_UnsetFlag(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ s16 sceneNum = payload["sceneNum"].get<s16>();
+ s16 flagType = payload["flagType"].get<s16>();
+ s16 flag = payload["flag"].get<s16>();
+
+ if (sceneNum == SCENE_ID_MAX) {
+ auto effect = new GameInteractionEffect::UnsetFlag();
+ effect->parameters[0] = payload["flagType"].get<int16_t>();
+ effect->parameters[1] = payload["flag"].get<int16_t>();
+ effect->Apply();
+ } else {
+ auto effect = new GameInteractionEffect::UnsetSceneFlag();
+ effect->parameters[0] = payload["sceneNum"].get<int16_t>();
+ effect->parameters[1] = payload["flagType"].get<int16_t>();
+ effect->parameters[2] = payload["flag"].get<int16_t>();
+ effect->Apply();
+ }
+
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp b/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp
new file mode 100644
index 000000000..89e1d6443
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp
@@ -0,0 +1,43 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/OTRGlobals.h"
+
+extern "C" {
+#include "macros.h"
+}
+
+/**
+ * UPDATE_BEANS_COUNT
+ *
+ * Keeps the client's bean count in sync as they buy/use them
+ */
+
+void Anchor::SendPacket_UpdateBeansCount() {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ nlohmann::json payload;
+ payload["type"] = UPDATE_BEANS_COUNT;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["addToQueue"] = true;
+ payload["amount"] = AMMO(ITEM_BEAN);
+ payload["amountBought"] = BEANS_BOUGHT;
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_UpdateBeansCount(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ AMMO(ITEM_BEAN) = payload["amount"].get<s8>();
+ BEANS_BOUGHT = payload["amountBought"].get<s8>();
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp
new file mode 100644
index 000000000..237f1baa0
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp
@@ -0,0 +1,77 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include "soh/Network/Anchor/JsonConversions.hpp"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/OTRGlobals.h"
+
+extern "C" {
+#include "variables.h"
+extern PlayState* gPlayState;
+}
+
+/**
+ * UPDATE_CLIENT_STATE
+ *
+ * Contains a small subset of data that is cached on the server and important for the client to know for various reasons
+ *
+ * Sent on various events, such as changing scenes, soft resetting, finishing the game, opening file select, etc.
+ *
+ * Note: This packet should be cross version compatible, so if you add anything here don't assume all clients will be
+ * providing it, consider doing a `contains` check before accessing any version specific data
+ */
+
+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["clientVersion"] = clientVersion;
+ payload["teamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["online"] = true;
+
+ if (IsSaveLoaded()) {
+ payload["seed"] = IS_RANDO ? Rando::Context::GetInstance()->GetSettings()->GetSeed() : 0;
+ payload["isSaveLoaded"] = true;
+ payload["isGameComplete"] = gSaveContext.sohStats.gameComplete;
+ payload["sceneNum"] = gPlayState->sceneNum;
+ payload["entranceIndex"] = gSaveContext.entranceIndex;
+ } else {
+ payload["seed"] = 0;
+ payload["isSaveLoaded"] = false;
+ payload["isGameComplete"] = false;
+ payload["sceneNum"] = SCENE_ID_MAX;
+ payload["entranceIndex"] = 0x00;
+ }
+
+ return payload;
+}
+
+void Anchor::SendPacket_UpdateClientState() {
+ nlohmann::json payload;
+ payload["type"] = UPDATE_CLIENT_STATE;
+ payload["state"] = PrepClientState();
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_UpdateClientState(nlohmann::json payload) {
+ uint32_t clientId = payload["clientId"].get<uint32_t>();
+
+ if (clients.contains(clientId)) {
+ AnchorClient client = payload["state"].get<AnchorClient>();
+ clients[clientId].clientId = clientId;
+ clients[clientId].name = client.name;
+ clients[clientId].color = client.color;
+ clients[clientId].clientVersion = client.clientVersion;
+ clients[clientId].teamId = client.teamId;
+ clients[clientId].online = client.online;
+ clients[clientId].seed = client.seed;
+ clients[clientId].isSaveLoaded = client.isSaveLoaded;
+ clients[clientId].isGameComplete = client.isGameComplete;
+ clients[clientId].sceneNum = client.sceneNum;
+ clients[clientId].entranceIndex = client.entranceIndex;
+ }
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp
new file mode 100644
index 000000000..c7beaa6b8
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp
@@ -0,0 +1,42 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/OTRGlobals.h"
+
+/**
+ * UPDATE_DUNGEON_ITEMS
+ *
+ * This is for 2 things, first is updating the dungeon items in vanilla saves, and second is
+ * for ensuring the amount of keys used is synced as players are using them.
+ */
+
+void Anchor::SendPacket_UpdateDungeonItems() {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ nlohmann::json payload;
+ payload["type"] = UPDATE_DUNGEON_ITEMS;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+ payload["addToQueue"] = true;
+ payload["mapIndex"] = gSaveContext.mapIndex;
+ payload["dungeonItems"] = gSaveContext.inventory.dungeonItems[gSaveContext.mapIndex];
+ payload["dungeonKeys"] = gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex];
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ u16 mapIndex = payload["mapIndex"].get<u16>();
+ gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get<u8>();
+ gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get<s8>();
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp b/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp
new file mode 100644
index 000000000..ce08f3640
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp
@@ -0,0 +1,47 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include "soh/Network/Anchor/JsonConversions.hpp"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/OTRGlobals.h"
+
+extern "C" {
+#include "variables.h"
+extern PlayState* gPlayState;
+}
+
+/**
+ * UPDATE_ROOM_STATE
+ */
+
+nlohmann::json Anchor::PrepRoomState() {
+ nlohmann::json payload;
+ payload["ownerClientId"] = ownClientId;
+ payload["pvpMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), 1);
+ payload["showLocationsMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), 1);
+ payload["teleportMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), 1);
+
+ return payload;
+}
+
+void Anchor::SendPacket_UpdateRoomState() {
+ nlohmann::json payload;
+ payload["type"] = UPDATE_ROOM_STATE;
+ payload["state"] = PrepRoomState();
+
+ Network::SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_UpdateRoomState(nlohmann::json payload) {
+ if (!payload.contains("state")) {
+ return;
+ }
+
+ roomState.ownerClientId = payload["state"]["ownerClientId"].get<uint32_t>();
+ roomState.pvpMode = payload["state"]["pvpMode"].get<u8>();
+ roomState.showLocationsMode = payload["state"]["showLocationsMode"].get<u8>();
+ roomState.teleportMode = payload["state"]["teleportMode"].get<u8>();
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp
new file mode 100644
index 000000000..cbc9088eb
--- /dev/null
+++ b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp
@@ -0,0 +1,259 @@
+#ifdef ENABLE_REMOTE_CONTROL
+
+#include "soh/Network/Anchor/Anchor.h"
+#include "soh/Network/Anchor/JsonConversions.hpp"
+#include <nlohmann/json.hpp>
+#include <libultraship/libultraship.h>
+#include "soh/Enhancements/randomizer/entrance.h"
+#include "soh/Enhancements/randomizer/dungeon.h"
+#include "soh/OTRGlobals.h"
+#include "soh/Notification/Notification.h"
+
+extern "C" {
+#include "variables.h"
+extern PlayState* gPlayState;
+}
+
+/**
+ * UPDATE_TEAM_STATE
+ *
+ * Pushes the current save state to the server for other teammates to use.
+ *
+ * Fires when the server passes on a REQUEST_TEAM_STATE packet, or when this client saves the game
+ *
+ * When sending this packet we will assume that the team queue has been emptied for this client, so the queue
+ * stored in the server will be cleared.
+ *
+ * When receiving this packet, if there is items in the team queue, we will play them back in order.
+ */
+
+void Anchor::SendPacket_UpdateTeamState() {
+ if (!IsSaveLoaded()) {
+ return;
+ }
+
+ json payload;
+ payload["type"] = UPDATE_TEAM_STATE;
+ payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
+
+ // Assume the team queue has been emptied, so clear it
+ payload["queue"] = json::array();
+
+ payload["state"] = gSaveContext;
+ // manually update current scene flags
+ payload["state"]["sceneFlags"][gPlayState->sceneNum * 4] = gPlayState->actorCtx.flags.chest;
+ payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 1] = gPlayState->actorCtx.flags.swch;
+ payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 2] = gPlayState->actorCtx.flags.clear;
+ payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 3] = gPlayState->actorCtx.flags.collect;
+
+ // The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player doesn't have to generate the seed themselves
+ // Currently it doesn't work :)
+ if (IS_RANDO) {
+ auto randoContext = Rando::Context::GetInstance();
+
+ payload["state"]["rando"] = json::object();
+ payload["state"]["rando"]["itemLocations"] = json::array();
+ for (int i = 0; i < RC_MAX; i++) {
+ payload["state"]["rando"]["itemLocations"][i] = json::array();
+ // payload["state"]["rando"]["itemLocations"][i]["rgID"] = randoContext->GetItemLocation(i)->GetPlacedRandomizerGet();
+ payload["state"]["rando"]["itemLocations"][i][0] = randoContext->GetItemLocation(i)->GetCheckStatus();
+ payload["state"]["rando"]["itemLocations"][i][1] = (u8)randoContext->GetItemLocation(i)->GetIsSkipped();
+
+ // if (randoContext->GetItemLocation(i)->GetPlacedRandomizerGet() == RG_ICE_TRAP) {
+ // payload["state"]["rando"]["itemLocations"][i]["fakeRgID"] = randoContext->GetItemOverride(i).LooksLike();
+ // payload["state"]["rando"]["itemLocations"][i]["trickName"] = json::object();
+ // payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"] = randoContext->GetItemOverride(i).GetTrickName().GetEnglish();
+ // payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"] = randoContext->GetItemOverride(i).GetTrickName().GetFrench();
+ // }
+ // if (randoContext->GetItemLocation(i)->HasCustomPrice()) {
+ // payload["state"]["rando"]["itemLocations"][i]["price"] = randoContext->GetItemLocation(i)->GetPrice();
+ // }
+ }
+
+ // auto entranceCtx = randoContext->GetEntranceShuffler();
+ // for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) {
+ // payload["state"]["rando"]["entrances"][i] = json::object();
+ // payload["state"]["rando"]["entrances"][i]["type"] = entranceCtx->entranceOverrides[i].type;
+ // payload["state"]["rando"]["entrances"][i]["index"] = entranceCtx->entranceOverrides[i].index;
+ // payload["state"]["rando"]["entrances"][i]["destination"] = entranceCtx->entranceOverrides[i].destination;
+ // payload["state"]["rando"]["entrances"][i]["override"] = entranceCtx->entranceOverrides[i].override;
+ // payload["state"]["rando"]["entrances"][i]["overrideDestination"] = entranceCtx->entranceOverrides[i].overrideDestination;
+ // }
+
+ // payload["state"]["rando"]["seed"] = json::array();
+ // for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) {
+ // payload["state"]["rando"]["seed"][i] = randoContext->hashIconIndexes[i];
+ // }
+ // payload["state"]["rando"]["inputSeed"] = randoContext->GetSettings()->GetSeedString();
+ // payload["state"]["rando"]["finalSeed"] = randoContext->GetSettings()->GetSeed();
+
+ // payload["state"]["rando"]["randoSettings"] = json::array();
+ // for (int i = 0; i < RSK_MAX; i++) {
+ // payload["state"]["rando"]["randoSettings"][i] = randoContext->GetOption((RandomizerSettingKey(i))).GetSelectedOptionIndex();
+ // }
+
+ // payload["state"]["rando"]["masterQuestDungeonCount"] = randoContext->GetDungeons()->CountMQ();
+ // payload["state"]["rando"]["masterQuestDungeons"] = json::array();
+ // for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) {
+ // payload["state"]["rando"]["masterQuestDungeons"][i] = randoContext->GetDungeon(i)->IsMQ();
+ // }
+ // for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) {
+ // payload["state"]["rando"]["requiredTrials"][i] = randoContext->GetTrial(i)->IsRequired();
+ // }
+ }
+
+ SendJsonToRemote(payload);
+}
+
+void Anchor::HandlePacket_UpdateTeamState(nlohmann::json payload) {
+ isHandlingUpdateTeamState = true;
+ // This can happen in between file select and the game starting, so we cant use this check, but we need to ensure we
+ // be careful to wrap PlayState usage in this check
+ // if (!IsSaveLoaded()) {
+ // return;
+ // }
+
+ if (payload.contains("state")) {
+ SaveContext loadedData = payload["state"].get<SaveContext>();
+
+ gSaveContext.questId = loadedData.questId;
+ gSaveContext.healthCapacity = loadedData.healthCapacity;
+ gSaveContext.magicLevel = loadedData.magicLevel;
+ gSaveContext.magicCapacity = gSaveContext.magic = loadedData.magicCapacity;
+ gSaveContext.isMagicAcquired = loadedData.isMagicAcquired;
+ gSaveContext.isDoubleMagicAcquired = loadedData.isDoubleMagicAcquired;
+ gSaveContext.isDoubleDefenseAcquired = loadedData.isDoubleDefenseAcquired;
+ gSaveContext.bgsFlag = loadedData.bgsFlag;
+ gSaveContext.swordHealth = loadedData.swordHealth;
+ gSaveContext.adultTradeItems = loadedData.adultTradeItems;
+ gSaveContext.triforcePiecesCollected = loadedData.triforcePiecesCollected;
+
+ for (int i = 0; i < 124; i++) {
+ gSaveContext.sceneFlags[i] = loadedData.sceneFlags[i];
+ if (IsSaveLoaded() && gPlayState->sceneNum == i) {
+ gPlayState->actorCtx.flags.chest = loadedData.sceneFlags[i].chest;
+ gPlayState->actorCtx.flags.swch = loadedData.sceneFlags[i].swch;
+ gPlayState->actorCtx.flags.clear = loadedData.sceneFlags[i].clear;
+ gPlayState->actorCtx.flags.collect = loadedData.sceneFlags[i].collect;
+ }
+ }
+
+ for (int i = 0; i < 14; i++) {
+ gSaveContext.eventChkInf[i] = loadedData.eventChkInf[i];
+ }
+
+ for (int i = 0; i < 4; i++) {
+ gSaveContext.itemGetInf[i] = loadedData.itemGetInf[i];
+ }
+
+ // Skip last row of infTable, don't want to sync swordless flag
+ for (int i = 0; i < 29; i++) {
+ gSaveContext.infTable[i] = loadedData.infTable[i];
+ }
+
+ for (int i = 0; i < 17; i++) {
+ gSaveContext.randomizerInf[i] = loadedData.randomizerInf[i];
+ }
+
+ for (int i = 0; i < 6; i++) {
+ gSaveContext.gsFlags[i] = loadedData.gsFlags[i];
+ }
+
+ gSaveContext.sohStats.fileCreatedAt = loadedData.sohStats.fileCreatedAt;
+
+ // Restore master sword state
+ u8 hasMasterSword = CHECK_OWNED_EQUIP(EQUIP_TYPE_SWORD, 1);
+ if (hasMasterSword) {
+ loadedData.inventory.equipment |= 0x2;
+ } else {
+ loadedData.inventory.equipment &= ~0x2;
+ }
+
+ // Restore bottle contents (unless it's ruto's letter)
+ for (int i = 0; i < 4; i++) {
+ if (gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_NONE && gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_LETTER_RUTO) {
+ loadedData.inventory.items[SLOT_BOTTLE_1 + i] = gSaveContext.inventory.items[SLOT_BOTTLE_1 + i];
+ }
+ }
+
+ // Restore ammo if it's non-zero, unless it's beans
+ for (int i = 0; i < ARRAY_COUNT(gSaveContext.inventory.ammo); i++) {
+ if (gSaveContext.inventory.ammo[i] != 0 && i != SLOT(ITEM_BEAN) && i != SLOT(ITEM_BEAN + 1)) {
+ loadedData.inventory.ammo[i] = gSaveContext.inventory.ammo[i];
+ }
+ }
+
+ gSaveContext.inventory = loadedData.inventory;
+
+ // The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player doesn't have to generate the seed themselves
+ // Currently it doesn't work :)
+ if (IS_RANDO && payload["state"].contains("rando")) {
+ auto randoContext = Rando::Context::GetInstance();
+
+ for (int i = 0; i < RC_MAX; i++) {
+ // randoContext->GetItemLocation(i)->RefPlacedItem() = payload["state"]["rando"]["itemLocations"][i]["rgID"].get<RandomizerGet>();
+ OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetCheckStatus(payload["state"]["rando"]["itemLocations"][i][0].get<RandomizerCheckStatus>());
+ OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetIsSkipped(payload["state"]["rando"]["itemLocations"][i][0].get<u8>());
+
+ // if (payload["state"]["rando"]["itemLocations"][i].contains("fakeRgID")) {
+ // randoContext->overrides.emplace(static_cast<RandomizerCheck>(i), Rando::ItemOverride(static_cast<RandomizerCheck>(i), payload["state"]["rando"]["itemLocations"][i]["fakeRgID"].get<RandomizerGet>()));
+ // randoContext->GetItemOverride(i).GetTrickName().english = payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"].get<std::string>();
+ // randoContext->GetItemOverride(i).GetTrickName().french = payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"].get<std::string>();
+ // }
+ // if (payload["state"]["rando"]["itemLocations"][i].contains("price")) {
+ // u16 price = payload["state"]["rando"]["itemLocations"][i]["price"].get<u16>();
+ // if (price > 0) {
+ // randoContext->GetItemLocation(i)->SetCustomPrice(price);
+ // }
+ // }
+ }
+
+ // auto entranceCtx = randoContext->GetEntranceShuffler();
+ // for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) {
+ // entranceCtx->entranceOverrides[i].type = payload["state"]["rando"]["entrances"][i]["type"].get<u16>();
+ // entranceCtx->entranceOverrides[i].index = payload["state"]["rando"]["entrances"][i]["index"].get<s16>();
+ // entranceCtx->entranceOverrides[i].destination = payload["state"]["rando"]["entrances"][i]["destination"].get<s16>();
+ // entranceCtx->entranceOverrides[i].override = payload["state"]["rando"]["entrances"][i]["override"].get<s16>();
+ // entranceCtx->entranceOverrides[i].overrideDestination = payload["state"]["rando"]["entrances"][i]["overrideDestination"].get<s16>();
+ // }
+
+ // for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) {
+ // randoContext->hashIconIndexes[i] = payload["state"]["rando"]["seed"][i].get<u8>();
+ // }
+ // randoContext->GetSettings()->SetSeedString(payload["state"]["rando"]["inputSeed"].get<std::string>());
+ // randoContext->GetSettings()->SetSeed(payload["state"]["rando"]["finalSeed"].get<u32>());
+
+ // for (int i = 0; i < RSK_MAX; i++) {
+ // randoContext->GetOption(RandomizerSettingKey(i)).SetSelectedIndex(payload["state"]["rando"]["randoSettings"][i].get<u8>());
+ // }
+
+ // randoContext->GetDungeons()->ClearAllMQ();
+ // for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) {
+ // if (payload["state"]["rando"]["masterQuestDungeons"][i].get<bool>()) {
+ // randoContext->GetDungeon(i)->SetMQ();
+ // }
+ // }
+
+ // randoContext->GetTrials()->SkipAll();
+ // for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) {
+ // if (payload["state"]["rando"]["requiredTrials"][i].get<bool>()) {
+ // randoContext->GetTrial(i)->SetAsRequired();
+ // }
+ // }
+ }
+
+ Notification::Emit({
+ .message = "Save updated from team",
+ });
+ }
+
+ if (payload.contains("queue")) {
+ for (auto& item : payload["queue"]) {
+ nlohmann::json itemPayload = nlohmann::json::parse(item.get<std::string>());
+ Anchor::Instance->OnIncomingJson(itemPayload);
+ }
+ }
+ isHandlingUpdateTeamState = false;
+}
+
+#endif // ENABLE_REMOTE_CONTROL
diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp
index b5627db36..362aab447 100644
--- a/soh/soh/OTRGlobals.cpp
+++ b/soh/soh/OTRGlobals.cpp
@@ -77,8 +77,10 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/CrowdControl/CrowdControl.h"
#include "soh/Network/Sail/Sail.h"
+#include "soh/Network/Anchor/Anchor.h"
CrowdControl* CrowdControl::Instance;
Sail* Sail::Instance;
+Anchor* Anchor::Instance;
#endif
#include "Enhancements/mods.h"
@@ -1164,6 +1166,7 @@ extern "C" void InitOTR() {
#ifdef ENABLE_REMOTE_CONTROL
CrowdControl::Instance = new CrowdControl();
Sail::Instance = new Sail();
+ Anchor::Instance = new Anchor();
#endif
OTRMessage_Init();
@@ -1199,6 +1202,9 @@ extern "C" void InitOTR() {
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
Sail::Instance->Enable();
}
+ if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) {
+ Anchor::Instance->Enable();
+ }
#endif
}
@@ -1216,6 +1222,9 @@ extern "C" void DeinitOTR() {
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
Sail::Instance->Disable();
}
+ if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) {
+ Anchor::Instance->Disable();
+ }
SDLNet_Quit();
#endif
diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp
index 1edec3025..4f12f00c7 100644
--- a/soh/soh/SaveManager.cpp
+++ b/soh/soh/SaveManager.cpp
@@ -1223,7 +1223,7 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se
delete saveContext;
InitMeta(fileNum);
- GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum);
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum, sectionID);
SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum);
saveMtx.unlock();
}
diff --git a/soh/soh/SohGui.cpp b/soh/soh/SohGui.cpp
index 2dbf62a99..fdfe76d3f 100644
--- a/soh/soh/SohGui.cpp
+++ b/soh/soh/SohGui.cpp
@@ -39,6 +39,9 @@
#include "Enhancements/debugger/MessageViewer.h"
#include "soh/Notification/Notification.h"
#include "soh/Enhancements/TimeDisplay/TimeDisplay.h"
+#ifdef ENABLE_REMOTE_CONTROL
+#include "soh/Network/Anchor/Anchor.h"
+#endif
bool isBetaQuestEnabled = false;
@@ -138,6 +141,9 @@ namespace SohGui {
std::shared_ptr<SohModalWindow> mModalWindow;
std::shared_ptr<Notification::Window> mNotificationWindow;
std::shared_ptr<TimeDisplayWindow> mTimeDisplayWindow;
+#ifdef ENABLE_REMOTE_CONTROL
+ std::shared_ptr<AnchorRoomWindow> mAnchorRoomWindow;
+#endif
void SetupGuiElements() {
auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui();
@@ -225,6 +231,10 @@ namespace SohGui {
mNotificationWindow->Show();
mTimeDisplayWindow = std::make_shared<TimeDisplayWindow>(CVAR_WINDOW("TimeDisplayEnabled"), "Additional Timers");
gui->AddGuiWindow(mTimeDisplayWindow);
+#ifdef ENABLE_REMOTE_CONTROL
+ mAnchorRoomWindow = std::make_shared<AnchorRoomWindow>(CVAR_WINDOW("AnchorRoom"), "Anchor Room");
+ gui->AddGuiWindow(mAnchorRoomWindow);
+#endif
}
void Destroy() {
@@ -261,6 +271,9 @@ namespace SohGui {
mTimeSplitWindow = nullptr;
mPlandomizerWindow = nullptr;
mTimeDisplayWindow = nullptr;
+#ifdef ENABLE_REMOTE_CONTROL
+ mAnchorRoomWindow = nullptr;
+#endif
}
void RegisterPopup(std::string title, std::string message, std::string button1, std::string button2, std::function<void()> button1callback, std::function<void()> button2callback) {
diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp
index ba9e683bf..a76bf509e 100644
--- a/soh/soh/SohMenuBar.cpp
+++ b/soh/soh/SohMenuBar.cpp
@@ -22,6 +22,7 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/CrowdControl/CrowdControl.h"
#include "soh/Network/Sail/Sail.h"
+#include "soh/Network/Anchor/Anchor.h"
#endif
@@ -2102,6 +2103,7 @@ void DrawRemoteControlMenu() {
if (ImGui::BeginMenu("Network")) {
Sail::Instance->DrawMenu();
CrowdControl::Instance->DrawMenu();
+ Anchor::Instance->DrawMenu();
ImGui::EndMenu();
}
}
diff --git a/soh/soh/UIWidgets.cpp b/soh/soh/UIWidgets.cpp
index 23af9f613..958685eac 100644
--- a/soh/soh/UIWidgets.cpp
+++ b/soh/soh/UIWidgets.cpp
@@ -825,7 +825,7 @@ namespace UIWidgets {
return 0;
}
- bool InputString(const char* label, std::string* value) {
- return ImGui::InputText(label, (char*)value->c_str(), value->capacity() + 1, ImGuiInputTextFlags_CallbackResize, InputTextResizeCallback, value);
+ bool InputString(const char* label, std::string* value, ImGuiInputTextFlags flags) {
+ return ImGui::InputText(label, (char*)value->c_str(), value->capacity() + 1, ImGuiInputTextFlags_CallbackResize | flags, InputTextResizeCallback, value);
}
}
diff --git a/soh/soh/UIWidgets.hpp b/soh/soh/UIWidgets.hpp
index 5903220d2..0ad2a4703 100644
--- a/soh/soh/UIWidgets.hpp
+++ b/soh/soh/UIWidgets.hpp
@@ -102,7 +102,7 @@ namespace UIWidgets {
void DrawFlagArray16(const std::string& name, uint16_t& flags);
void DrawFlagArray8(const std::string& name, uint8_t& flags);
bool StateButton(const char* str_id, const char* label);
- bool InputString(const char* label, std::string* value);
+ bool InputString(const char* label, std::string* value, ImGuiInputTextFlags flags = 0);
}
#endif /* UIWidgets_hpp */
diff --git a/soh/soh/cvar_prefixes.h b/soh/soh/cvar_prefixes.h
index 6ddde97bd..cee81167e 100644
--- a/soh/soh/cvar_prefixes.h
+++ b/soh/soh/cvar_prefixes.h
@@ -14,4 +14,5 @@
#define CVAR_GENERAL(var) CVAR_PREFIX_GENERAL "." var
#define CVAR_REMOTE(var) CVAR_PREFIX_REMOTE "." var
#define CVAR_REMOTE_CROWD_CONTROL(var) CVAR_REMOTE(".CrowdControl." var)
-#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE(".Sail." var) \ No newline at end of file
+#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE(".Sail." var)
+#define CVAR_REMOTE_ANCHOR(var) CVAR_REMOTE(".Anchor." var)
diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp
index faa171231..4b43e2ba9 100644
--- a/soh/soh/util.cpp
+++ b/soh/soh/util.cpp
@@ -117,6 +117,7 @@ std::vector<std::string> sceneNames = {
"Castle Hedge Maze (Early)",
"Sasa Test",
"Treasure Chest Room",
+ "Unknown",
};
std::vector<std::string> itemNames = {
diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c
index 70c7a3233..9c241898f 100644
--- a/soh/src/code/z_actor.c
+++ b/soh/src/code/z_actor.c
@@ -1231,14 +1231,20 @@ void Actor_Init(Actor* actor, PlayState* play) {
ActorShape_Init(&actor->shape, 0.0f, NULL, 0.0f);
if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex)) {
Actor_SetObjectDependency(play, actor);
- actor->init(actor, play);
- actor->init = NULL;
- GameInteractor_ExecuteOnActorInit(actor);
+ if (GameInteractor_ShouldActorInit(actor)) {
+ actor->init(actor, play);
+ actor->init = NULL;
- // For enemy health bar we need to know the max health during init
- if (actor->category == ACTORCAT_ENEMY) {
- actor->maximumHealth = actor->colChkInfo.health;
+ GameInteractor_ExecuteOnActorInit(actor);
+
+ // For enemy health bar we need to know the max health during init
+ if (actor->category == ACTORCAT_ENEMY) {
+ actor->maximumHealth = actor->colChkInfo.health;
+ }
+ } else {
+ actor->init = NULL;
+ Actor_Kill(actor);
}
}
}
@@ -2210,6 +2216,10 @@ void Player_PlaySfx(Actor* actor, u16 sfxId) {
// Audio_PlaySoundGeneral(sfxId, &actor->projectedPos, 4, &D_801333E0 , &D_801333E0, &D_801333E8);
Audio_PlaySoundGeneral(sfxId, &actor->projectedPos, 4, &freqMultiplier, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
}
+
+ if (actor->id == ACTOR_PLAYER) {
+ GameInteractor_ExecuteOnPlayerSfx(sfxId);
+ }
}
void Audio_PlayActorSound2(Actor* actor, u16 sfxId) {
@@ -2589,14 +2599,20 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) {
if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex))
{
Actor_SetObjectDependency(play, actor);
- actor->init(actor, play);
- actor->init = NULL;
- GameInteractor_ExecuteOnActorInit(actor);
+ if (GameInteractor_ShouldActorInit(actor)) {
+ actor->init(actor, play);
+ actor->init = NULL;
+
+ GameInteractor_ExecuteOnActorInit(actor);
- // For enemy health bar we need to know the max health during init
- if (actor->category == ACTORCAT_ENEMY) {
- actor->maximumHealth = actor->colChkInfo.health;
+ // For enemy health bar we need to know the max health during init
+ if (actor->category == ACTORCAT_ENEMY) {
+ actor->maximumHealth = actor->colChkInfo.health;
+ }
+ } else {
+ actor->init = NULL;
+ Actor_Kill(actor);
}
}
actor = actor->next;
diff --git a/soh/src/code/z_en_item00.c b/soh/src/code/z_en_item00.c
index 1bd859718..b71a19feb 100644
--- a/soh/src/code/z_en_item00.c
+++ b/soh/src/code/z_en_item00.c
@@ -775,6 +775,13 @@ void EnItem00_Update(Actor* thisx, PlayState* play) {
EnItem00* this = (EnItem00*)thisx;
s32 pad;
+ // #region SOH [Co-op]
+ if (Flags_GetCollectible(play, this->collectibleFlag)) {
+ Actor_Kill(&this->actor);
+ return;
+ }
+ // #endregion
+
// Rotate some drops when 3D drops are on, otherwise reset rotation back to 0 for billboard effect
if (
(this->actor.params == ITEM00_HEART && this->unk_15A >= 0) ||
diff --git a/soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c b/soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c
index 160dc15f9..cf57bdc55 100644
--- a/soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c
+++ b/soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c
@@ -211,7 +211,9 @@ void func_8086ED50(BgBombwall* this, PlayState* play) {
}
void func_8086ED70(BgBombwall* this, PlayState* play) {
- if (this->collider.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
+ // #endregion
this->collider.base.acFlags &= ~AC_HIT;
func_8086EDFC(this, play);
Flags_SetSwitch(play, this->dyna.actor.params & 0x3F);
diff --git a/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c b/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c
index 9f89e7980..bee6b1155 100644
--- a/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c
+++ b/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c
@@ -277,7 +277,9 @@ void BgBreakwall_Wait(BgBreakwall* this, PlayState* play) {
}
}
- if (GameInteractor_Should(VB_BG_BREAKWALL_BREAK, this->collider.base.acFlags & 2 || blueFireArrowHit)) {
+ // #region SOH [Co-op]
+ if (GameInteractor_Should(VB_BG_BREAKWALL_BREAK, this->collider.base.acFlags & 2 || blueFireArrowHit) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
+ // #endregion
Vec3f effectPos;
s32 wallType = ((this->dyna.actor.params >> 13) & 3) & 0xFF;
diff --git a/soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c b/soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c
index f41964603..4d3ed967f 100644
--- a/soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c
+++ b/soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c
@@ -268,7 +268,9 @@ void func_80882E54(BgHakaZou* this, PlayState* play) {
}
void func_80883000(BgHakaZou* this, PlayState* play) {
- if (this->collider.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->switchFlag)) {
+ // #endregion
Flags_SetSwitch(play, this->switchFlag);
if (this->dyna.actor.params == STA_GIANT_BIRD_STATUE) {
diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c
index c1d402188..6ce78615c 100644
--- a/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c
+++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c
@@ -126,8 +126,10 @@ void BgHidanDalm_Destroy(Actor* thisx, PlayState* play) {
void BgHidanDalm_Wait(BgHidanDalm* this, PlayState* play) {
Player* player = GET_PLAYER(play);
- if ((this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) &&
- (player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23)) {
+ // #region SOH [Co-op]
+ if (((this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) &&
+ (player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23)) || Flags_GetSwitch(play, this->switchFlag)) {
+ // #endregion
this->collider.base.acFlags &= ~AC_HIT;
if ((this->collider.elements[0].info.bumperFlags & BUMP_HIT) ||
(this->collider.elements[1].info.bumperFlags & BUMP_HIT)) {
diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c
index 818533571..e49f33a9a 100644
--- a/soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c
+++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c
@@ -278,7 +278,9 @@ void func_80888734(BgHidanHamstep* this) {
}
void func_808887C4(BgHidanHamstep* this, PlayState* play) {
- if (this->collider.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params >> 8) & 0xFF)) {
+ // #endregion
OnePointCutscene_Init(play, 3310, 100, &this->dyna.actor, MAIN_CAM);
Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_HAMMER_SWITCH);
this->collider.base.acFlags = AC_NONE;
diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c
index ea49fbd4f..5079c200e 100644
--- a/soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c
+++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c
@@ -201,7 +201,9 @@ void func_8088960C(BgHidanHrock* this, PlayState* play) {
}
void func_808896B8(BgHidanHrock* this, PlayState* play) {
- if (this->collider.base.acFlags & 2) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & 2) || Flags_GetSwitch(play, this->unk_16A)) {
+ // #endregion
this->collider.base.acFlags &= ~2;
this->actionFunc = func_808894B0;
this->dyna.actor.flags |= ACTOR_FLAG_UPDATE_WHILE_CULLED;
diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c
index c9cbb039f..71f586f45 100644
--- a/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c
+++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c
@@ -303,7 +303,9 @@ void BgHidanKowarerukabe_Update(Actor* thisx, PlayState* play) {
BgHidanKowarerukabe* this = (BgHidanKowarerukabe*)thisx;
s32 pad;
- if (Actor_GetCollidedExplosive(play, &this->collider.base) != NULL) {
+ // #region SOH [Co-op]
+ if ((Actor_GetCollidedExplosive(play, &this->collider.base) != NULL) || Flags_GetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F)) {
+ // #endregion
BgHidanKowarerukabe_Break(this, play);
Flags_SetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F);
diff --git a/soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c b/soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c
index 9810f2749..d09980978 100644
--- a/soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c
+++ b/soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c
@@ -333,10 +333,12 @@ void func_8089107C(BgIceShelter* this, PlayState* play) {
MeltOnIceArrowHit(this, this->cylinder2, type, play);
}
// Default blue fire check
- if (this->cylinder1.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->cylinder1.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
this->cylinder1.base.acFlags &= ~AC_HIT;
- if ((this->cylinder1.base.ac != NULL) && (this->cylinder1.base.ac->id == ACTOR_EN_ICE_HONO)) {
+ if (((this->cylinder1.base.ac != NULL) && (this->cylinder1.base.ac->id == ACTOR_EN_ICE_HONO)) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
+ // #endregion
if (type == 4) {
if (this->dyna.actor.parent != NULL) {
this->dyna.actor.parent->freezeTimer = 50;
diff --git a/soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c b/soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c
index cb36b743d..bb7333c52 100644
--- a/soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c
+++ b/soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c
@@ -142,7 +142,9 @@ void BgJyaBombchuiwa_SetupWaitForExplosion(BgJyaBombchuiwa* this, PlayState* pla
}
void BgJyaBombchuiwa_WaitForExplosion(BgJyaBombchuiwa* this, PlayState* play) {
- if ((this->collider.base.acFlags & AC_HIT) || (this->timer > 0)) {
+ // #region SOH [Co-op]
+ if (((this->collider.base.acFlags & AC_HIT) || (this->timer > 0)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) {
+ // #endregion
if (this->timer == 0) {
OnePointCutscene_Init(play, 3410, -99, &this->actor, MAIN_CAM);
}
diff --git a/soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c b/soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c
index d0e4b9471..9edb9edc6 100644
--- a/soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c
+++ b/soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c
@@ -163,7 +163,9 @@ void BgJyaBombiwa_Break(BgJyaBombiwa* this, PlayState* play) {
void BgJyaBombiwa_Update(Actor* thisx, PlayState* play) {
BgJyaBombiwa* this = (BgJyaBombiwa*)thisx;
- if (this->collider.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
+ // #endregion
BgJyaBombiwa_Break(this, play);
Flags_SetSwitch(play, this->dyna.actor.params & 0x3F);
SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN);
diff --git a/soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c b/soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c
index 89db58c0e..8eaa8ff10 100644
--- a/soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c
+++ b/soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c
@@ -467,7 +467,9 @@ void BgMizuBwall_SpawnDebris(BgMizuBwall* this, PlayState* play) {
void BgMizuBwall_Idle(BgMizuBwall* this, PlayState* play) {
BgMizuBwall_SetAlpha(this, play);
- if (this->collider.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, ((u16)this->dyna.actor.params >> 8) & 0x3F)) {
+ // #endregion
this->collider.base.acFlags &= ~AC_HIT;
Flags_SetSwitch(play, ((u16)this->dyna.actor.params >> 8) & 0x3F);
this->breakTimer = 1;
diff --git a/soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c b/soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c
index e40302b66..5269510fe 100644
--- a/soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c
+++ b/soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c
@@ -183,7 +183,9 @@ void BgSpot08Bakudankabe_Destroy(Actor* thisx, PlayState* play) {
void BgSpot08Bakudankabe_Update(Actor* thisx, PlayState* play) {
BgSpot08Bakudankabe* this = (BgSpot08Bakudankabe*)thisx;
- if (this->collider.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) {
+ // #endregion
func_808B0324(this, play);
Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F));
SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN);
diff --git a/soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c b/soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c
index 3b7f7c7e4..81fe0a06b 100644
--- a/soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c
+++ b/soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c
@@ -135,7 +135,9 @@ void BgSpot11Bakudankabe_Destroy(Actor* thisx, PlayState* play) {
void BgSpot11Bakudankabe_Update(Actor* thisx, PlayState* play) {
BgSpot11Bakudankabe* this = (BgSpot11Bakudankabe*)thisx;
- if (this->collider.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) {
+ // #endregion
func_808B2218(this, play);
Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F));
SoundSource_PlaySfxAtFixedWorldPos(play, &D_808B2738, 40, NA_SE_EV_WALL_BROKEN);
diff --git a/soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c b/soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c
index 6537a1690..fe934bd2d 100644
--- a/soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c
+++ b/soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c
@@ -114,7 +114,9 @@ void BgSpot17Bakudankabe_Destroy(Actor* thisx, PlayState* play) {
void BgSpot17Bakudankabe_Update(Actor* thisx, PlayState* play) {
BgSpot17Bakudankabe* this = (BgSpot17Bakudankabe*)thisx;
- if (this->dyna.actor.xzDistToPlayer < 650.0f && func_80033684(play, &this->dyna.actor) != NULL) {
+ // #region SOH [Co-op]
+ if ((this->dyna.actor.xzDistToPlayer < 650.0f && func_80033684(play, &this->dyna.actor) != NULL) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) {
+ // #endregion
func_808B6BC0(this, play);
Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F));
SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN);
diff --git a/soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c b/soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c
index 23d7fac96..f2556541b 100644
--- a/soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c
+++ b/soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c
@@ -146,7 +146,9 @@ void func_808BEFF4(BgYdanMaruta* this, PlayState* play) {
}
void func_808BF078(BgYdanMaruta* this, PlayState* play) {
- if (this->collider.base.acFlags & AC_HIT) {
+ // #region SOH [Co-op]
+ if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->switchFlag)) {
+ // #endregion
this->unk_16A = 20;
Flags_SetSwitch(play, this->switchFlag);
Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME);
diff --git a/soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c b/soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c
index 59402d658..f448d55a0 100644
--- a/soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c
+++ b/soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c
@@ -282,6 +282,12 @@ void BgYdanSp_FloorWebIdle(BgYdanSp* this, PlayState* play) {
webPos.x = this->dyna.actor.world.pos.x;
webPos.y = this->dyna.actor.world.pos.y - 50.0f;
webPos.z = this->dyna.actor.world.pos.z;
+ // #region SOH [Co-op]
+ if (Flags_GetSwitch(play, this->isDestroyedSwitchFlag)) {
+ BgYdanSp_BurnWeb(this, play);
+ return;
+ }
+ // #endregion
if (Player_IsBurningStickInRange(play, &webPos, 70.0f, 50.0f) != 0) {
this->dyna.actor.home.pos.x = player->meleeWeaponInfo[0].tip.x;
this->dyna.actor.home.pos.z = player->meleeWeaponInfo[0].tip.z;
@@ -411,6 +417,11 @@ void BgYdanSp_WallWebIdle(BgYdanSp* this, PlayState* play) {
BgYdanSp_BurnWeb(this, play);
}
}
+ // #region SOH [Co-op]
+ if (Flags_GetSwitch(play, this->isDestroyedSwitchFlag)) {
+ BgYdanSp_BurnWeb(this, play);
+ }
+ // #endregion
CollisionCheck_SetAC(play, &play->colChkCtx, &this->trisCollider.base);
}
diff --git a/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c b/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c
index 99dd5e43f..3db8803a6 100644
--- a/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c
+++ b/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c
@@ -6,6 +6,7 @@
#include "z_door_gerudo.h"
#include "objects/object_door_gerudo/object_door_gerudo.h"
+#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
#define FLAGS 0
@@ -103,6 +104,7 @@ void func_8099485C(DoorGerudo* this, PlayState* play) {
gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex] -= 1;
Flags_SetSwitch(play, this->dyna.actor.params & 0x3F);
Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK);
+ GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex);
} else {
s32 direction = func_80994750(this, play);
diff --git a/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c b/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c
index c842571e7..14cff8380 100644
--- a/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c
+++ b/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c
@@ -379,6 +379,7 @@ void func_80996B0C(DoorShutter* this, PlayState* play) {
if (this->doorType != SHUTTER_BOSS) {
gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]--;
Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK);
+ GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex);
} else {
Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK_B);
}
@@ -645,6 +646,11 @@ void DoorShutter_Update(Actor* thisx, PlayState* play) {
if (!(player->stateFlags1 & (PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_GETTING_ITEM | PLAYER_STATE1_IN_ITEM_CS)) || (this->actionFunc == DoorShutter_SetupType)) {
this->actionFunc(this, play);
}
+ // #region SOH [Co-op]
+ if (Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
+ DECR(this->unk_16E);
+ }
+ // #endregion
}
Gfx* func_80997838(PlayState* play, DoorShutter* this, Gfx* p) {
diff --git a/soh/src/overlays/actors/ovl_En_Door/z_en_door.c b/soh/src/overlays/actors/ovl_En_Door/z_en_door.c
index 92670921d..c8edec4b2 100644
--- a/soh/src/overlays/actors/ovl_En_Door/z_en_door.c
+++ b/soh/src/overlays/actors/ovl_En_Door/z_en_door.c
@@ -11,6 +11,7 @@
#include "objects/object_mizu_objects/object_mizu_objects.h"
#include "objects/object_haka_door/object_haka_door.h"
#include "soh/ResourceManagerHelpers.h"
+#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
#define FLAGS ACTOR_FLAG_UPDATE_WHILE_CULLED
@@ -204,6 +205,7 @@ void EnDoor_Idle(EnDoor* this, PlayState* play) {
gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]--;
Flags_SetSwitch(play, this->actor.params & 0x3F);
Audio_PlayActorSound2(&this->actor, NA_SE_EV_CHAIN_KEY_UNLOCK);
+ GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex);
}
} else if (!Player_InCsMode(play)) {
if (fabsf(playerPosRelToDoor.y) < 20.0f && fabsf(playerPosRelToDoor.x) < 20.0f &&
@@ -231,6 +233,11 @@ void EnDoor_Idle(EnDoor* this, PlayState* play) {
this->actionFunc = EnDoor_AjarOpen;
}
}
+ // #region SOH [Co-op]
+ if (Flags_GetSwitch(play, this->actor.params & 0x3F)) {
+ DECR(this->lockTimer);
+ }
+ // #endregion
}
void EnDoor_WaitForCheck(EnDoor* this, PlayState* play) {
diff --git a/soh/src/overlays/actors/ovl_En_Si/z_en_si.c b/soh/src/overlays/actors/ovl_En_Si/z_en_si.c
index 338dfaf12..8f64213cb 100644
--- a/soh/src/overlays/actors/ovl_En_Si/z_en_si.c
+++ b/soh/src/overlays/actors/ovl_En_Si/z_en_si.c
@@ -149,6 +149,13 @@ void func_80AFB950(EnSi* this, PlayState* play) {
void EnSi_Update(Actor* thisx, PlayState* play) {
EnSi* this = (EnSi*)thisx;
+ // #region SOH [Co-op]
+ if (GET_GS_FLAGS((thisx->params & 0x1F00) >> 8) & (thisx->params & 0xFF)) {
+ Actor_Kill(&this->actor);
+ return;
+ }
+ // #endregion
+
Actor_MoveXZGravity(&this->actor);
Actor_UpdateBgCheckInfo(play, &this->actor, 0.0f, 0.0f, 0.0f, 4);
this->actionFunc(this, play);
diff --git a/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c b/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c
index 93dd9c9d3..98da453f3 100644
--- a/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c
+++ b/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c
@@ -916,6 +916,13 @@ void func_80B0E9BC(EnSw* this, PlayState* play) {
void EnSw_Update(Actor* thisx, PlayState* play) {
EnSw* this = (EnSw*)thisx;
+ // #region SOH [Co-op]
+ if (GET_GS_FLAGS((thisx->params & 0x1F00) >> 8) & (thisx->params & 0xFF)) {
+ Actor_Kill(&this->actor);
+ return;
+ }
+ // #endregion
+
SkelAnime_Update(&this->skelAnime);
func_80B0C9F0(this, play);
this->actionFunc(this, play);
diff --git a/soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c b/soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c
index 9d3c9c240..a47b63cf3 100644
--- a/soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c
+++ b/soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c
@@ -55,6 +55,13 @@ void ItemBHeart_Destroy(Actor* thisx, PlayState* play) {
void ItemBHeart_Update(Actor* thisx, PlayState* play) {
ItemBHeart* this = (ItemBHeart*)thisx;
+ // #region SOH [Co-op]
+ if (Flags_GetCollectible(play, 0x1F)) {
+ Actor_Kill(&this->actor);
+ return;
+ }
+ // #endregion
+
func_80B85264(this, play);
Actor_UpdateBgCheckInfo(play, &this->actor, 0.0f, 0.0f, 0.0f, 4);
if (Actor_HasParent(&this->actor, play)) {
diff --git a/soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c b/soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c
index 602236816..9ec4f10bf 100644
--- a/soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c
+++ b/soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c
@@ -125,8 +125,10 @@ void ObjBombiwa_Update(Actor* thisx, PlayState* play) {
ObjBombiwa* this = (ObjBombiwa*)thisx;
s32 pad;
+ // #region SOH [Co-op]
if ((func_80033684(play, &this->actor) != NULL) ||
- ((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040))) {
+ ((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) {
+ // #endregion
ObjBombiwa_Break(this, play);
Flags_SetSwitch(play, this->actor.params & 0x3F);
SoundSource_PlaySfxAtFixedWorldPos(play, &this->actor.world.pos, 80, NA_SE_EV_WALL_BROKEN);
diff --git a/soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c b/soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c
index e17cf0d80..2611f25ce 100644
--- a/soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c
+++ b/soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c
@@ -171,10 +171,12 @@ void ObjHamishi_Update(Actor* thisx, PlayState* play) {
ObjHamishi_Shake(this);
- if ((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) {
+ // #region SOH [Co-op]
+ if (((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) {
this->collider.base.acFlags &= ~AC_HIT;
this->hitCount++;
- if (this->hitCount < 2) {
+ if (this->hitCount < 2 && !Flags_GetSwitch(play, this->actor.params & 0x3F)) {
+ // #endregion
this->shakeFrames = 15;
this->shakePosSize = 2.0f;
this->shakeRotSize = 400.0f;