diff --git a/soh/soh/Network/Anchor/Anchor.cpp b/soh/soh/Network/Anchor/Anchor.cpp new file mode 100644 index 000000000..bc5614f47 --- /dev/null +++ b/soh/soh/Network/Anchor/Anchor.cpp @@ -0,0 +1,198 @@ +#include "Anchor.h" +#include +#include +#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.hm64.org"), + 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_DEBUG("[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_DEBUG("[Anchor] Received payload:\n{}", payload.dump()); + } + + std::string packetType = payload["type"].get(); + + // 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(); + if (clients.contains(clientId) && clients[clientId].clientVersion != clientVersion) { + return; + } + } + } + + // Handle PLAYER_UPDATE packets immediately, no need to queue + if (packetType == PLAYER_UPDATE) { + HandlePacket_PlayerUpdate(payload); + return; + } + + // Queue all packets to be processed on the game thread + std::lock_guard lock(incomingPacketQueueMutex); + incomingPacketQueue.push(payload); +} + +void Anchor::ProcessIncomingPacketQueue() { + std::lock_guard lock(incomingPacketQueueMutex); + + while (!incomingPacketQueue.empty()) { + nlohmann::json payload = incomingPacketQueue.front(); + incomingPacketQueue.pop(); + + std::string packetType = payload["type"].get(); + + isProcessingIncomingPacket = true; + + // packetType here is a string so we can't use a switch statement + if (packetType == ALL_CLIENT_STATE) + HandlePacket_AllClientState(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 == 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); + + isProcessingIncomingPacket = false; + } +} + +// 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; +} diff --git a/soh/soh/Network/Anchor/Anchor.h b/soh/soh/Network/Anchor/Anchor.h new file mode 100644 index 000000000..a5e831153 --- /dev/null +++ b/soh/soh/Network/Anchor/Anchor.h @@ -0,0 +1,181 @@ +#ifndef NETWORK_ANCHOR_H +#define NETWORK_ANCHOR_H +#ifdef __cplusplus + +#include "soh/Network/Network.h" +#include +#include +#include + +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 + u8 syncItemsAndFlags; // 0 = off, 1 = on +} RoomState; + +class Anchor : public Network { + private: + bool refreshingActors = false; + bool justLoadedSave = false; + bool isHandlingUpdateTeamState = false; + bool isProcessingIncomingPacket = false; + std::queue incomingPacketQueue; + std::mutex incomingPacketQueueMutex; + + 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: + uint32_t ownClientId; + 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 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 clients; + std::vector actorIndexToClientId; + RoomState roomState; + + void Enable(); + void Disable(); + void OnIncomingJson(nlohmann::json payload); + void OnConnected(); + void OnDisconnected(); + void DrawMenu(); + void ProcessIncomingPacketQueue(); + void SendJsonToRemote(nlohmann::json packet); + bool IsSaveLoaded(); + bool CanTeleportTo(uint32_t clientId); + + void SendPacket_ClearTeamState(std::string teamId); + 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 diff --git a/soh/soh/Network/Anchor/AnchorRoomWindow.cpp b/soh/soh/Network/Anchor/AnchorRoomWindow.cpp new file mode 100644 index 000000000..d0d273e2c --- /dev/null +++ b/soh/soh/Network/Anchor/AnchorRoomWindow.cpp @@ -0,0 +1,129 @@ +#include "Anchor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +extern PlayState* gPlayState; +} + +void AnchorRoomWindow::Draw() { + if (!IsVisible() || !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); + + DrawElement(); + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); +} + +void AnchorRoomWindow::DrawElement() { + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + if (isGlobalRoom) { + u32 activeClients = 0; + for (auto& [clientId, client] : Anchor::Instance->clients) { + if (client.online) { + activeClients++; + } + } + ImGui::Text("Players Online: %d", activeClients); + return; + } + + // First build a list of teams + std::set 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; + } + + ImGui::PushID(clientId); + + 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()); + ImGui::PopID(); + 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->CanTeleportTo(client.clientId)) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Button(ICON_FA_LOCATION_ARROW, ImVec2(20.0f, 20.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()->GetSeed() : 0; + if (client.isSaveLoaded && Anchor::Instance->IsSaveLoaded() && client.seed != seed && client.online && + !client.self) { + 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::PopID(); + } + } +} diff --git a/soh/soh/Network/Anchor/DummyPlayer.cpp b/soh/soh/Network/Anchor/DummyPlayer.cpp new file mode 100644 index 000000000..d0eb58d93 --- /dev/null +++ b/soh/soh/Network/Anchor/DummyPlayer.cpp @@ -0,0 +1,221 @@ +#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_HOOKSHOT_PULLS_PLAYER; + player->cylinder.dim.radius = 30; + player->actor.colChkInfo.damageTable = &DummyPlayerDamageTable; + + gSaveContext.linkAge = originalAge; + + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + if (!isGlobalRoom) { + NameTag_RegisterForActorWithOptions(actor, client.name.c_str(), {}); + } +} + +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"))) { + actor->flags |= ACTOR_FLAG_LOCK_ON_DISABLED; + return; + } + + actor->flags &= ~ACTOR_FLAG_LOCK_ON_DISABLED; + + 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) { +} diff --git a/soh/soh/Network/Anchor/HookHandlers.cpp b/soh/soh/Network/Anchor/HookHandlers.cpp new file mode 100644 index 000000000..15886c527 --- /dev/null +++ b/soh/soh/Network/Anchor/HookHandlers.cpp @@ -0,0 +1,361 @@ +#include "Anchor.h" +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +#include "src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.h" +#include "src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.h" +#include "src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.h" +#include "src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.h" +#include "src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.h" +#include "src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.h" +#include "src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.h" +#include "src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.h" +#include "src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.h" +#include "src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.h" +#include "src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.h" +#include "src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.h" +#include "src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.h" +#include "src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.h" +#include "src/overlays/actors/ovl_Door_Shutter/z_door_shutter.h" +#include "src/overlays/actors/ovl_En_Door/z_en_door.h" +#include "src/overlays/actors/ovl_En_Si/z_en_si.h" +#include "src/overlays/actors/ovl_En_Sw/z_en_sw.h" +#include "src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.h" +#include "src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.h" +#include "src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.h" +#include "src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.h" +#include "src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.h" + +extern PlayState* gPlayState; + +void func_8086ED70(BgBombwall* bgBombwall, PlayState* play); +void BgBreakwall_Wait(BgBreakwall* bgBreakwall, PlayState* play); +void func_80883000(BgHakaZou* bgHakaZou, PlayState* play); +void func_808887C4(BgHidanHamstep* bgHidanHamstep, PlayState* play); +void func_808896B8(BgHidanHrock* bgHidanHrock, PlayState* play); +void func_8089107C(BgIceShelter* bgIceShelter, PlayState* play); +void func_808911BC(BgIceShelter* bgIceShelter); +void ObjBombiwa_Break(ObjBombiwa* objBombiwa, PlayState* play); +void ObjHamishi_Break(ObjHamishi* objHamishi, PlayState* play); +void BgJyaBombchuiwa_WaitForExplosion(BgJyaBombchuiwa* bgJyaBombchuiwa, PlayState* play); +void BgMizuBwall_Idle(BgMizuBwall* bgMizuBwall, PlayState* play); +void func_808B6BC0(BgSpot17Bakudankabe* bgSpot17Bakudankabe, PlayState* play); +void func_808BF078(BgYdanMaruta* bgYdanMaruta, PlayState* play); +void BgYdanSp_FloorWebIdle(BgYdanSp* bgYdanSp, PlayState* play); +void BgYdanSp_WallWebIdle(BgYdanSp* bgYdanSp, PlayState* play); +void BgYdanSp_BurnWeb(BgYdanSp* bgYdanSp, PlayState* play); +void EnDoor_Idle(EnDoor* enDoor, PlayState* play); +} + +void Anchor::RegisterHooks() { + + // #region Hooks that are required for basic Anchor functionality + + COND_HOOK(OnSceneSpawnActors, isConnected, [&]() { + SendPacket_UpdateClientState(); + + if (IsSaveLoaded()) { + RefreshClientActors(); + } + }); + + COND_HOOK(OnPresentFileSelect, isConnected, [&]() { SendPacket_UpdateClientState(); }); + + COND_ID_HOOK(ShouldActorInit, ACTOR_PLAYER, isConnected, [&](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; + } + }); + + COND_HOOK(OnPlayerUpdate, isConnected, [&]() { + if (justLoadedSave) { + justLoadedSave = false; + SendPacket_RequestTeamState(); + } + SendPacket_PlayerUpdate(); + }); + + COND_HOOK(OnGameFrameUpdate, isConnected, [&]() { ProcessIncomingPacketQueue(); }); + + COND_HOOK(OnPlayerSfx, isConnected, [&](u16 sfxId) { SendPacket_PlayerSfx(sfxId); }); + + COND_HOOK(OnLoadGame, isConnected, [&](s16 fileNum) { justLoadedSave = true; }); + + COND_HOOK(OnSaveFile, isConnected, [&](s16 fileNum, int sectionID) { + if (sectionID == 0) { + SendPacket_UpdateTeamState(); + } + }); + + COND_HOOK(OnFlagSet, isConnected, + [&](s16 flagType, s16 flag) { SendPacket_SetFlag(SCENE_ID_MAX, flagType, flag); }); + + COND_HOOK(OnFlagUnset, isConnected, + [&](s16 flagType, s16 flag) { SendPacket_UnsetFlag(SCENE_ID_MAX, flagType, flag); }); + + COND_HOOK(OnSceneFlagSet, isConnected, + [&](s16 sceneNum, s16 flagType, s16 flag) { SendPacket_SetFlag(sceneNum, flagType, flag); }); + + COND_HOOK(OnSceneFlagUnset, isConnected, + [&](s16 sceneNum, s16 flagType, s16 flag) { SendPacket_UnsetFlag(sceneNum, flagType, flag); }); + + COND_HOOK(OnRandoSetCheckStatus, isConnected, [&](RandomizerCheck rc, RandomizerCheckStatus status) { + if (!isHandlingUpdateTeamState) { + SendPacket_SetCheckStatus(rc); + } + }); + + COND_HOOK(OnRandoSetIsSkipped, isConnected, [&](RandomizerCheck rc, bool isSkipped) { + if (!isHandlingUpdateTeamState) { + SendPacket_SetCheckStatus(rc); + } + }); + + COND_HOOK(OnRandoEntranceDiscovered, isConnected, + [&](u16 entranceIndex, u8 isReversedEntrance) { SendPacket_EntranceDiscovered(entranceIndex); }); + + COND_ID_HOOK(OnBossDefeat, ACTOR_BOSS_GANON2, isConnected, [&](void* refActor) { SendPacket_GameComplete(); }); + + COND_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); + }); + + COND_HOOK(OnDungeonKeyUsed, isConnected, [&](uint16_t mapIndex) { + // Handle vanilla dungeon items a bit differently + SendPacket_UpdateDungeonItems(); + }); + + // #endregion + + // #region Hooks that are purely to sync actor states across the clients, not super essential + + COND_ID_HOOK(OnActorUpdate, ACTOR_EN_ITEM00, isConnected, [&](void* refActor) { + EnItem00* actor = static_cast(refActor); + + if (Flags_GetCollectible(gPlayState, actor->collectibleFlag)) { + Actor_Kill(&actor->actor); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_BOMBWALL, isConnected, [&](void* refActor, bool* should) { + BgBombwall* actor = static_cast(refActor); + + if (actor->actionFunc == func_8086ED70 && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_BREAKWALL, isConnected, [&](void* refActor, bool* should) { + BgBreakwall* actor = static_cast(refActor); + + if (actor->actionFunc == BgBreakwall_Wait && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HAKA_ZOU, isConnected, [&](void* refActor, bool* should) { + BgHakaZou* actor = static_cast(refActor); + + if (actor->actionFunc == func_80883000 && Flags_GetSwitch(gPlayState, actor->switchFlag)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HIDAN_HAMSTEP, isConnected, [&](void* refActor, bool* should) { + BgHidanHamstep* actor = static_cast(refActor); + + if (actor->actionFunc == func_808887C4 && Flags_GetSwitch(gPlayState, (actor->dyna.actor.params >> 8) & 0xFF)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HIDAN_HROCK, isConnected, [&](void* refActor, bool* should) { + BgHidanHrock* actor = static_cast(refActor); + + if (actor->actionFunc == func_808896B8 && Flags_GetSwitch(gPlayState, actor->unk_16A)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_ICE_SHELTER, isConnected, [&](void* refActor, bool* should) { + BgIceShelter* actor = static_cast(refActor); + + if (actor->actionFunc == func_8089107C && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + func_808911BC(actor); + Audio_PlayActorSound2(&actor->dyna.actor, NA_SE_EV_ICE_MELT); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_JYA_BOMBCHUIWA, isConnected, [&](void* refActor, bool* should) { + BgJyaBombchuiwa* actor = static_cast(refActor); + + if (actor->actionFunc == BgJyaBombchuiwa_WaitForExplosion && + Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_JYA_BOMBIWA, isConnected, [&](void* refActor, bool* should) { + BgJyaBombiwa* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_MIZU_BWALL, isConnected, [&](void* refActor, bool* should) { + BgMizuBwall* actor = static_cast(refActor); + + if (actor->actionFunc == BgMizuBwall_Idle && + Flags_GetSwitch(gPlayState, ((u16)actor->dyna.actor.params >> 8) & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT08_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) { + BgSpot08Bakudankabe* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT11_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) { + BgSpot11Bakudankabe* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT17_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) { + BgSpot17Bakudankabe* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) { + func_808B6BC0(actor, gPlayState); + SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN); + Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME); + Actor_Kill(&actor->dyna.actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_YDAN_MARUTA, isConnected, [&](void* refActor, bool* should) { + BgYdanMaruta* actor = static_cast(refActor); + + if (actor->actionFunc == func_808BF078 && Flags_GetSwitch(gPlayState, actor->switchFlag)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_YDAN_SP, isConnected, [&](void* refActor, bool* should) { + BgYdanSp* actor = static_cast(refActor); + + if ((actor->actionFunc == BgYdanSp_FloorWebIdle || actor->actionFunc == BgYdanSp_WallWebIdle) && + Flags_GetSwitch(gPlayState, actor->isDestroyedSwitchFlag)) { + BgYdanSp_BurnWeb(actor, gPlayState); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_DOOR_SHUTTER, isConnected, [&](void* refActor, bool* should) { + DoorShutter* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + DECR(actor->unk_16E); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_DOOR, isConnected, [&](void* refActor, bool* should) { + EnDoor* actor = static_cast(refActor); + + if (actor->actionFunc == EnDoor_Idle && Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) { + DECR(actor->lockTimer); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_SI, isConnected, [&](void* refActor, bool* should) { + EnSi* actor = static_cast(refActor); + + if (GET_GS_FLAGS((actor->actor.params & 0x1F00) >> 8) & (actor->actor.params & 0xFF)) { + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_SW, isConnected, [&](void* refActor, bool* should) { + EnSw* actor = static_cast(refActor); + + if (GET_GS_FLAGS((actor->actor.params & 0x1F00) >> 8) & (actor->actor.params & 0xFF)) { + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_ITEM_B_HEART, isConnected, [&](void* refActor, bool* should) { + ItemBHeart* actor = static_cast(refActor); + + if (Flags_GetCollectible(gPlayState, 0x1F)) { + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_BOMBIWA, isConnected, [&](void* refActor, bool* should) { + ObjBombiwa* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) { + ObjBombiwa_Break(actor, gPlayState); + SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->actor.world.pos, 80, NA_SE_EV_WALL_BROKEN); + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_HAMISHI, isConnected, [&](void* refActor, bool* should) { + ObjHamishi* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) { + ObjHamishi_Break(actor, gPlayState); + SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->actor.world.pos, 40, NA_SE_EV_WALL_BROKEN); + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_VB_SHOULD(VB_HAMMER_TOTEM_BREAK, isConnected, { + BgHidanDalm* actor = va_arg(args, BgHidanDalm*); + + if (Flags_GetSwitch(gPlayState, actor->switchFlag)) { + *should = true; + } + }); + + COND_VB_SHOULD(VB_FIRE_TEMPLE_BOMBABLE_WALL_BREAK, isConnected, { + BgHidanKowarerukabe* actor = va_arg(args, BgHidanKowarerukabe*); + + if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params >> 8) & 0x3F)) { + *should = true; + } + }); + + // #endregion +} diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp new file mode 100644 index 000000000..b99b78496 --- /dev/null +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -0,0 +1,205 @@ +#ifndef NETWORK_ANCHOR_JSON_CONVERSIONS_H +#define NETWORK_ANCHOR_JSON_CONVERSIONS_H +#ifdef __cplusplus + +#include +#include +#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 ShipRandomizerSaveContextData& shipRandomizerSaveContextData) { + j = json{ + { "triforcePiecesCollected", shipRandomizerSaveContextData.triforcePiecesCollected }, + }; +} + +inline void from_json(const json& j, ShipRandomizerSaveContextData& shipRandomizerSaveContextData) { + j.at("triforcePiecesCollected").get_to(shipRandomizerSaveContextData.triforcePiecesCollected); +} + +inline void to_json(json& j, const ShipQuestSpecificSaveContextData& shipQuestSpecificSaveContextData) { + j = json{ + { "randomizer", shipQuestSpecificSaveContextData.randomizer }, + }; +} + +inline void from_json(const json& j, ShipQuestSpecificSaveContextData& shipQuestSpecificSaveContextData) { + j.at("randomizer").get_to(shipQuestSpecificSaveContextData.randomizer); +} + +inline void to_json(json& j, const ShipQuestSaveContextData& shipQuestSaveContextData) { + j = json{ + { "id", shipQuestSaveContextData.id }, + { "data", shipQuestSaveContextData.data }, + }; +} + +inline void from_json(const json& j, ShipQuestSaveContextData& shipQuestSaveContextData) { + j.at("id").get_to(shipQuestSaveContextData.id); + j.at("data").get_to(shipQuestSaveContextData.data); +} + +inline void to_json(json& j, const ShipSaveContextData& shipSaveContextData) { + j = json{ + { "stats", shipSaveContextData.stats }, + { "quest", shipSaveContextData.quest }, + { "randomizerInf", shipSaveContextData.randomizerInf }, + }; +} + +inline void from_json(const json& j, ShipSaveContextData& shipSaveContextData) { + j.at("stats").get_to(shipSaveContextData.stats); + j.at("quest").get_to(shipSaveContextData.quest); + j.at("randomizerInf").get_to(shipSaveContextData.randomizerInf); +} + +inline void to_json(json& j, const SaveContext& saveContext) { + std::vector 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 }, + { "gsFlags", saveContext.gsFlags }, + { "inventory", saveContext.inventory }, + { "ship", saveContext.ship }, + }; +} + +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 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("gsFlags").get_to(saveContext.gsFlags); + j.at("inventory").get_to(saveContext.inventory); + j.at("ship").get_to(saveContext.ship); +} + +#endif // __cplusplus +#endif // NETWORK_ANCHOR_JSON_CONVERSIONS_H diff --git a/soh/soh/Network/Anchor/Menu.cpp b/soh/soh/Network/Anchor/Menu.cpp new file mode 100644 index 000000000..5a1d5fc43 --- /dev/null +++ b/soh/soh/Network/Anchor/Menu.cpp @@ -0,0 +1,239 @@ +#include "Anchor.h" +#include +#include "soh/SohGui/SohGui.hpp" +#include "soh/SohGui/SohMenu.h" +#include "soh/util.h" + +namespace SohGui { +extern std::shared_ptr mSohMenu; +extern std::shared_ptr mAnchorRoomWindow; +} // namespace SohGui + +static const char* pvpModes[3] = { "Off", "On", "On + Friendly Fire" }; +static std::vector teleportModes = { "None", "Team Only", "All" }; +static std::vector showLocationsModes = { "None", "Team Only", "All" }; + +void AnchorMainMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + + std::string host = CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org"); + 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("Connection Settings"); + + ImGui::BeginDisabled(anchor->isEnabled); + ImGui::Text("Host & Port"); + if (UIWidgets::InputString("##Host", &host, + UIWidgets::InputOptions() + .Size(ImGui::GetContentRegionAvail() - + ImVec2((ImGui::GetFontSize() * 5 + ImGui::GetStyle().ItemSpacing.x), 0)) + .Color(THEME_COLOR))) { + CVarSetString(CVAR_REMOTE_ANCHOR("Host"), host.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::SameLine(); + UIWidgets::PushStyleInput(THEME_COLOR); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5); + if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) { + CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), port); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + UIWidgets::PopStyleInput(); + + ImGui::Text("Name"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##Name", &anchorName, UIWidgets::InputOptions().Color(THEME_COLOR))) { + 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, + UIWidgets::InputOptions().IsSecret(anchor->isEnabled).Color(THEME_COLOR))) { + 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, UIWidgets::InputOptions().Color(THEME_COLOR))) { + CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), anchorTeamId.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Spacing(); + + if (UIWidgets::Button("Restore Defaults", UIWidgets::ButtonOptions() + .Size(ImVec2(ImGui::GetContentRegionAvail().x / 2, 0)) + .Color(UIWidgets::Colors::Red))) { + CVarSetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org"); + CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383); + CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), ""); + CVarSetString(CVAR_REMOTE_ANCHOR("Name"), ""); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::SameLine(); + + if (UIWidgets::Button("Global Room", UIWidgets::ButtonOptions() + .Color(UIWidgets::Colors::Blue) + .Tooltip("Always-online public room so you don't have to experience " + "Hyrule alone. PVP and syncing are disabled."))) { + CVarSetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org"); + CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383); + CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), "soh-global"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::EndDisabled(); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!isFormValid); + const char* buttonLabel = anchor->isEnabled ? "Disable" : "Enable"; + UIWidgets::PushStyleButton(anchor->isEnabled ? UIWidgets::ColorValues.at(UIWidgets::Colors::Red) + : UIWidgets::ColorValues.at(UIWidgets::Colors::Green)); + if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) { + if (anchor->isEnabled) { + CVarClear(CVAR_REMOTE_ANCHOR("Enabled")); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + anchor->Disable(); + } else { + CVarSetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + anchor->Enable(); + } + } + UIWidgets::PopStyleButton(); + ImGui::EndDisabled(); + ImGui::Spacing(); + + if (!anchor->isEnabled) { + return; + } + + if (!anchor->isConnected) { + ImGui::Text("Connecting..."); + return; + } + + ImGui::SeparatorText("Current Room"); + ImGui::Text("%s Connected", ICON_FA_CHECK); + + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button("Request Team State")) { + anchor->SendPacket_RequestTeamState(); + } + UIWidgets::Tooltip("Try this if you are missing items or flags that your team members have collected"); + UIWidgets::PopStyleButton(); + + ImGui::SameLine(); + + UIWidgets::WindowButton("Toggle Anchor Room Window", CVAR_WINDOW("AnchorRoom"), SohGui::mAnchorRoomWindow); + if (!SohGui::mAnchorRoomWindow->IsVisible()) { + SohGui::mAnchorRoomWindow->DrawElement(); + } +} + +void AnchorAdminMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + if (!anchor->isEnabled || !anchor->isConnected || anchor->roomState.ownerClientId != anchor->ownClientId || + isGlobalRoom) { + return; + } + + ImGui::SeparatorText("Room Settings (Admin Only)"); + + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button("Clear All Team State")) { + std::set teams; + for (auto& [clientId, client] : Anchor::Instance->clients) { + teams.insert(client.teamId); + } + for (auto& team : teams) { + anchor->SendPacket_ClearTeamState(team); + } + } + UIWidgets::PopStyleButton(); + + if (UIWidgets::CVarCombobox("PvP Mode:", CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), pvpModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPositions::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCombobox("Show Locations For:", CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), + showLocationsModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPositions::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCombobox("Allow Teleporting To:", CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), teleportModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPositions::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCheckbox("Sync Items & Flags", CVAR_REMOTE_ANCHOR("RoomSettings.SyncItemsAndFlags"), + UIWidgets::CheckboxOptions().DefaultValue(true).Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } +} + +void AnchorInstructionsMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + + ImGui::SeparatorText("Usage Instructions"); + + ImGui::TextWrapped("1. All players involved should start at the file select screen"); + + ImGui::TextWrapped("2. Come up with a unique Room ID (this is basically your password) and enter it, along with " + "your desired player name and team ID and click Enable"); + + ImGui::TextWrapped("3. The host should configure the randomizer settings and generate a seed, then share the newly " + "generated JSON spoiler file with other players."); + + ImGui::TextWrapped("4. All players should load the same JSON spoiler file (drag it into SoH window), make sure " + "seed icons match, then create a new file."); + + ImGui::TextWrapped("5. All players should now load into their game. IMPORTANT! If using an existing save/seed " + "ensure the player with the most progress loads the file first."); + + ImGui::TextWrapped("6. After everyone has loaded in, verify on the network tab that it doesn't warn about anyone " + "being on a wrong version or seed."); + + ImGui::Spacing(); + + ImGui::TextWrapped( + "Note: Team ID is used to group players together in the same team, sharing items and flags. Make sure all " + "players who want to share progress use the same Team ID. All players with the same Team ID should be using " + "the same randomizer seed, while players on different teams can use different seeds."); +} + +void RegisterAnchorMenu() { + WidgetPath path = { "Network", "Anchor", SECTION_COLUMN_1 }; + SohGui::mSohMenu->AddWidget(path, "AnchorMainMenu", WIDGET_CUSTOM) + .CustomFunction(AnchorMainMenu) + .HideInSearch(true); + path.column = SECTION_COLUMN_2; + SohGui::mSohMenu->AddWidget(path, "AnchorAdminMenu", WIDGET_CUSTOM) + .CustomFunction(AnchorAdminMenu) + .HideInSearch(true); + SohGui::mSohMenu->AddWidget(path, "AnchorInstructionsMenu", WIDGET_CUSTOM) + .CustomFunction(AnchorInstructionsMenu) + .HideInSearch(true); +} + +static RegisterMenuInitFunc menuInitFunc(RegisterAnchorMenu); diff --git a/soh/soh/Network/Anchor/Packets/AllClientState.cpp b/soh/soh/Network/Anchor/Packets/AllClientState.cpp new file mode 100644 index 000000000..08d8d1a55 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/AllClientState.cpp @@ -0,0 +1,71 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#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 newClients = payload["state"].get>(); + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + // 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 && !isGlobalRoom) { + Notification::Emit({ + .prefix = client.name, + .message = client.online ? "Connected" : "Disconnected", + }); + } + } else if (client.online && !isGlobalRoom) { + 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 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(); +} diff --git a/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp b/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp new file mode 100644 index 000000000..e97abea4f --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp @@ -0,0 +1,65 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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(); + if (!clients.contains(clientId) || clients[clientId].player == nullptr) { + return; + } + + AnchorClient& anchorClient = clients[clientId]; + Player* otherPlayer = anchorClient.player; + Player* self = GET_PLAYER(gPlayState); + + // Prevent incoming damage during cutscenes or item get sequences + if (Player_InBlockingCsMode(gPlayState, self) || self->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || + self->stateFlags1 & PLAYER_STATE1_GETTING_ITEM) { + return; + } + + u8 damageEffect = payload["damageEffect"].get(); + u8 damage = payload["damage"].get(); + + 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); +} diff --git a/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp b/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp new file mode 100644 index 000000000..914601bf7 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp @@ -0,0 +1,14 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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(); +} diff --git a/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp b/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp new file mode 100644 index 000000000..6669e28a0 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp @@ -0,0 +1,33 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) { + 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() || !roomState.syncItemsAndFlags) { + return; + } + + u16 entranceIndex = payload["entranceIndex"].get(); + Entrance_SetEntranceDiscovered(entranceIndex, 1); +} diff --git a/soh/soh/Network/Anchor/Packets/GameComplete.cpp b/soh/soh/Network/Anchor/Packets/GameComplete.cpp new file mode 100644 index 000000000..aa05a6e64 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/GameComplete.cpp @@ -0,0 +1,42 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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(); + if (!clients.contains(clientId)) { + return; + } + + AnchorClient& anchorClient = clients[clientId]; + anchorClient.isGameComplete = true; + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + Notification::Emit({ + .prefix = isGlobalRoom ? "Someone" : anchorClient.name, + .message = RandomElement(gameCompleteMessages), + }); +} diff --git a/soh/soh/Network/Anchor/Packets/GiveItem.cpp b/soh/soh/Network/Anchor/Packets/GiveItem.cpp new file mode 100644 index 000000000..fbf048d7d --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/GiveItem.cpp @@ -0,0 +1,108 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Notification/Notification.h" +#include "soh/Enhancements/randomizer/randomizer.h" +#include "soh/SohGui/ImGuiUtils.h" +#include "soh/Enhancements/item-tables/ItemTableManager.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" +extern PlayState* gPlayState; +} + +/** + * GIVE_ITEM + */ + +uint8_t incomingIceTrapsFromAnchor = 0; + +void Anchor::SendPacket_GiveItem(u16 modId, s16 getItemId) { + if (!IsSaveLoaded() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) { + return; + } + + if (modId == MOD_RANDOMIZER && getItemId == RG_ICE_TRAP && incomingIceTrapsFromAnchor > 0) { + incomingIceTrapsFromAnchor = MAX(incomingIceTrapsFromAnchor - 1, 0); + return; + } + + // Ignore sending master sword in final Ganon fight + if (modId == MOD_RANDOMIZER && getItemId == RG_MASTER_SWORD && gPlayState->sceneNum == SCENE_GANON_BOSS) { + 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() || !roomState.syncItemsAndFlags) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + AnchorClient& client = clients[clientId]; + u16 modId = payload["modId"].get(); + u16 getItemId = payload["getItemId"].get(); + + GetItemEntry getItemEntry; + if (modId == MOD_NONE) { + getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, getItemId); + } else { + getItemEntry = Rando::StaticData::RetrieveItem(static_cast(getItemId)).GetGIEntry_Copy(); + } + + 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.ship.pendingIceTrapCount++; + incomingIceTrapsFromAnchor++; + } else { + Randomizer_Item_Give(gPlayState, getItemEntry); + } + } + + // Full heal if getting a heart container or piece + if (getItemEntry.gid == GID_HEART_CONTAINER || getItemEntry.gid == GID_HEART_PIECE) { + gSaveContext.healthAccumulator = 0x140; + } + + // 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); + } + + 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, + }); + } + } +} diff --git a/soh/soh/Network/Anchor/Packets/Handshake.cpp b/soh/soh/Network/Anchor/Packets/Handshake.cpp new file mode 100644 index 000000000..b93e63278 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/Handshake.cpp @@ -0,0 +1,22 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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); +} diff --git a/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp b/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp new file mode 100644 index 000000000..ce4755adf --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp @@ -0,0 +1,47 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include + +extern "C" { +#include "macros.h" +#include "functions.h" +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * 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 && !client.self) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_PlayerSfx(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + u16 sfxId = payload["sfxId"].get(); + + if (!clients.contains(clientId) || !clients[clientId].player) { + return; + } + + Player_PlaySfx((Actor*)clients[clientId].player, sfxId); +} diff --git a/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp new file mode 100644 index 000000000..34bacaf11 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp @@ -0,0 +1,117 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include + +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 && !client.self) { + 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 jointArray; + for (size_t i = 0; i < 24; i++) { + Vec3s joint = player->skelAnime.jointTable[i]; + 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 && !client.self) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + + bool shouldRefreshActors = false; + + if (clients.contains(clientId)) { + auto& client = clients[clientId]; + + if (client.linkAge != payload["linkAge"].get()) { + shouldRefreshActors = true; + } + + client.sceneNum = payload["sceneNum"].get(); + client.entranceIndex = payload["entranceIndex"].get(); + client.linkAge = payload["linkAge"].get(); + client.posRot = payload["posRot"].get(); + std::vector 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(); + client.currentBoots = payload["currentBoots"].get(); + client.currentShield = payload["currentShield"].get(); + client.currentTunic = payload["currentTunic"].get(); + client.stateFlags1 = payload["stateFlags1"].get(); + client.stateFlags2 = payload["stateFlags2"].get(); + client.buttonItem0 = payload["buttonItem0"].get(); + client.itemAction = payload["itemAction"].get(); + client.heldItemAction = payload["heldItemAction"].get(); + client.modelGroup = payload["modelGroup"].get(); + client.invincibilityTimer = payload["invincibilityTimer"].get(); + client.unk_862 = payload["unk_862"].get(); + client.actionVar1 = payload["actionVar1"].get(); + } + + if (shouldRefreshActors) { + RefreshClientActors(); + } +} diff --git a/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp b/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp new file mode 100644 index 000000000..dda852473 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp @@ -0,0 +1,37 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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() { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + 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() || !roomState.syncItemsAndFlags) { + return; + } + + SendPacket_UpdateTeamState(); +} diff --git a/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp b/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp new file mode 100644 index 000000000..8bcb1a306 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp @@ -0,0 +1,91 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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 (!CanTeleportTo(clientId)) { + 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(); + SendPacket_TeleportTo(clientId); +} + +// Reusable function to check if teleporting to a client is allowed +bool Anchor::CanTeleportTo(uint32_t clientId) { + // Teleporting is disabled + if (roomState.teleportMode == 0) { + return false; + } + + // You're not loaded into a save + if (!IsSaveLoaded()) { + return false; + } + + // The client doesn't exist + if (clients.find(clientId) == clients.end()) { + return false; + } + + AnchorClient& client = clients[clientId]; + + // The client is yourself + if (client.self) { + return false; + } + + // The client isn't online or loaded into a save + if (!client.online || !client.isSaveLoaded) { + return false; + } + + // Teleporting to team only, but the client is not on your team + std::string ownTeamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + if (roomState.teleportMode == 1 && client.teamId != ownTeamId) { + return false; + } + + // Problematic scenes for teleporting + if (client.sceneNum == SCENE_ID_MAX || client.sceneNum == SCENE_GROTTOS || client.sceneNum == SCENE_MARKET_DAY || + client.sceneNum == SCENE_MARKET_NIGHT || client.sceneNum == SCENE_MARKET_RUINS || + client.sceneNum == SCENE_MARKET_ENTRANCE_DAY || client.sceneNum == SCENE_MARKET_ENTRANCE_NIGHT || + client.sceneNum == SCENE_MARKET_ENTRANCE_RUINS || client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_DAY || + client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_NIGHT || + client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_RUINS || client.sceneNum == SCENE_BACK_ALLEY_DAY || + client.sceneNum == SCENE_BACK_ALLEY_NIGHT) { + return false; + } + + // Child can't teleport to Ganon's Castle exterior + if (client.sceneNum == SCENE_OUTSIDE_GANONS_CASTLE && gSaveContext.linkAge == LINK_AGE_CHILD) { + return false; + } + + // Adult can't teleport to Hyrule Castle exterior + if (client.sceneNum == SCENE_HYRULE_CASTLE && gSaveContext.linkAge == LINK_AGE_ADULT) { + return false; + } + + return true; +} diff --git a/soh/soh/Network/Anchor/Packets/ServerMessage.cpp b/soh/soh/Network/Anchor/Packets/ServerMessage.cpp new file mode 100644 index 000000000..9c1fdc132 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/ServerMessage.cpp @@ -0,0 +1,17 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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(), + }); +} diff --git a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp new file mode 100644 index 000000000..74bc44699 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp @@ -0,0 +1,51 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) { + 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() || !roomState.syncItemsAndFlags) { + return; + } + + auto randoContext = Rando::Context::GetInstance(); + + RandomizerCheck rc = payload["rc"].get(); + RandomizerCheckStatus status = payload["status"].get(); + bool skipped = payload["skipped"].get(); + + if (randoContext->GetItemLocation(rc)->GetCheckStatus() != status) { + randoContext->GetItemLocation(rc)->SetCheckStatus(status); + } + if (randoContext->GetItemLocation(rc)->GetIsSkipped() != skipped) { + randoContext->GetItemLocation(rc)->SetIsSkipped(skipped); + } + CheckTracker::RecalculateAllAreaTotals(); + CheckTracker::RecalculateAvailableChecks(); +} diff --git a/soh/soh/Network/Anchor/Packets/SetFlag.cpp b/soh/soh/Network/Anchor/Packets/SetFlag.cpp new file mode 100644 index 000000000..3468bab01 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/SetFlag.cpp @@ -0,0 +1,73 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" + +extern PlayState* gPlayState; +} + +/** + * SET_FLAG + * + * Fired when a flag is set in the save context + */ + +void Anchor::SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + 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() || !roomState.syncItemsAndFlags) { + return; + } + + s16 sceneNum = payload["sceneNum"].get(); + s16 flagType = payload["flagType"].get(); + s16 flag = payload["flag"].get(); + + if (sceneNum == SCENE_ID_MAX) { + auto effect = new GameInteractionEffect::SetFlag(); + effect->parameters[0] = flagType; + effect->parameters[1] = flag; + effect->Apply(); + + // Special case: If King Zora moved, and the player has Ruto's Letter, convert it to an empty bottle + if (flagType == FLAG_EVENT_CHECK_INF && flag == EVENTCHKINF_KING_ZORA_MOVED && + Inventory_HasSpecificBottle(ITEM_LETTER_RUTO)) { + Inventory_ReplaceItem(gPlayState, ITEM_LETTER_RUTO, ITEM_BOTTLE); + } + } else { + // Special case: Ignore water temple water level flags, stored at 0x1C, 0x1D, 0x1E. + if (sceneNum == SCENE_WATER_TEMPLE && flagType == FLAG_SCENE_SWITCH && + (flag == 0x1C || flag == 0x1D || flag == 0x1E)) { + return; + } + + // Special case: Ignore forest temple elevator flag, stored at 0x1B. + if (sceneNum == SCENE_FOREST_TEMPLE && flagType == FLAG_SCENE_SWITCH && flag == 0x1B) { + return; + } + + auto effect = new GameInteractionEffect::SetSceneFlag(); + effect->parameters[0] = sceneNum; + effect->parameters[1] = flagType; + effect->parameters[2] = flag; + effect->Apply(); + } +} diff --git a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp new file mode 100644 index 000000000..1d50be449 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp @@ -0,0 +1,59 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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(); + s8 roomIndex = payload["roomIndex"].get(); + PosRot posRot = payload["posRot"].get(); + + 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(hookId); + }); +} diff --git a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp new file mode 100644 index 000000000..cf2fbf5d4 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp @@ -0,0 +1,109 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" +#include "soh/Enhancements/randomizer/ShuffleTradeItems.h" +extern PlayState* gPlayState; +} + +/** + * UNSET_FLAG + * + * Fired when a flag is unset in the save context + */ + +void Anchor::SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + 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() || !roomState.syncItemsAndFlags) { + return; + } + + s16 sceneNum = payload["sceneNum"].get(); + s16 flagType = payload["flagType"].get(); + s16 flag = payload["flag"].get(); + + if (sceneNum == SCENE_ID_MAX) { + auto effect = new GameInteractionEffect::UnsetFlag(); + effect->parameters[0] = flagType; + effect->parameters[1] = flag; + effect->Apply(); + + // Special case: If an adult trade item flag is unset, replace the item if the player has it equipped + if (flagType == FLAG_RANDOMIZER_INF && + (flag >= RAND_INF_ADULT_TRADES_HAS_POCKET_EGG && flag <= RAND_INF_ADULT_TRADES_HAS_CLAIM_CHECK)) { + u16 itemToReplace = ITEM_POCKET_EGG; + switch (flag) { + case RAND_INF_ADULT_TRADES_HAS_POCKET_EGG: + itemToReplace = ITEM_POCKET_EGG; + break; + case RAND_INF_ADULT_TRADES_HAS_POCKET_CUCCO: + itemToReplace = ITEM_POCKET_CUCCO; + break; + case RAND_INF_ADULT_TRADES_HAS_COJIRO: + itemToReplace = ITEM_COJIRO; + break; + case RAND_INF_ADULT_TRADES_HAS_ODD_MUSHROOM: + itemToReplace = ITEM_ODD_MUSHROOM; + break; + case RAND_INF_ADULT_TRADES_HAS_ODD_POTION: + itemToReplace = ITEM_ODD_POTION; + break; + case RAND_INF_ADULT_TRADES_HAS_SAW: + itemToReplace = ITEM_SAW; + break; + case RAND_INF_ADULT_TRADES_HAS_SWORD_BROKEN: + itemToReplace = ITEM_SWORD_BROKEN; + break; + case RAND_INF_ADULT_TRADES_HAS_PRESCRIPTION: + itemToReplace = ITEM_PRESCRIPTION; + break; + case RAND_INF_ADULT_TRADES_HAS_FROG: + itemToReplace = ITEM_FROG; + break; + case RAND_INF_ADULT_TRADES_HAS_EYEDROPS: + itemToReplace = ITEM_EYEDROPS; + break; + case RAND_INF_ADULT_TRADES_HAS_CLAIM_CHECK: + itemToReplace = ITEM_CLAIM_CHECK; + break; + } + Inventory_ReplaceItem(gPlayState, itemToReplace, Randomizer_GetNextAdultTradeItem()); + } + } else { + // Special case: Ignore water temple water level flags, stored at 0x1C, 0x1D, 0x1E. + if (sceneNum == SCENE_WATER_TEMPLE && flagType == FLAG_SCENE_SWITCH && + (flag == 0x1C || flag == 0x1D || flag == 0x1E)) { + return; + } + + // Special case: Ignore forest temple elevator flag, stored at 0x1B. + if (sceneNum == SCENE_FOREST_TEMPLE && flagType == FLAG_SCENE_SWITCH && flag == 0x1B) { + return; + } + + auto effect = new GameInteractionEffect::UnsetSceneFlag(); + effect->parameters[0] = sceneNum; + effect->parameters[1] = flagType; + effect->parameters[2] = flag; + effect->Apply(); + } +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp b/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp new file mode 100644 index 000000000..976c7cc24 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp @@ -0,0 +1,39 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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() || !roomState.syncItemsAndFlags) { + 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() || !roomState.syncItemsAndFlags) { + return; + } + + AMMO(ITEM_BEAN) = payload["amount"].get(); + BEANS_BOUGHT = payload["amountBought"].get(); +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp new file mode 100644 index 000000000..7b3c180bb --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp @@ -0,0 +1,73 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#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()->GetSeed() : 0; + payload["isSaveLoaded"] = true; + payload["isGameComplete"] = gSaveContext.ship.stats.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(); + + if (clients.contains(clientId)) { + AnchorClient client = payload["state"].get(); + 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; + } +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp new file mode 100644 index 000000000..9e0a43200 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp @@ -0,0 +1,38 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#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() || !roomState.syncItemsAndFlags) { + 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() || !roomState.syncItemsAndFlags) { + return; + } + + u16 mapIndex = payload["mapIndex"].get(); + gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get(); + gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get(); +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp b/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp new file mode 100644 index 000000000..327385715 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp @@ -0,0 +1,55 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#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; + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + if (isGlobalRoom) { + // Global room uses hardcoded settings + payload["pvpMode"] = 0; + payload["showLocationsMode"] = 0; + payload["teleportMode"] = 0; + payload["syncItemsAndFlags"] = 0; + } else { + 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); + payload["syncItemsAndFlags"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.SyncItemsAndFlags"), 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(); + roomState.pvpMode = payload["state"]["pvpMode"].get(); + roomState.showLocationsMode = payload["state"]["showLocationsMode"].get(); + roomState.teleportMode = payload["state"]["teleportMode"].get(); + roomState.syncItemsAndFlags = payload["state"]["syncItemsAndFlags"].get(); +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp new file mode 100644 index 000000000..9c7c3309a --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp @@ -0,0 +1,300 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#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() || !roomState.syncItemsAndFlags) { + 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->GetSeedString(); + // payload["state"]["rando"]["finalSeed"] = randoContext->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::SendPacket_ClearTeamState(std::string teamId) { + json payload; + payload["type"] = UPDATE_TEAM_STATE; + payload["targetTeamId"] = teamId; + payload["queue"] = json::array(); + payload["state"] = json::object(); + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateTeamState(nlohmann::json payload) { + if (!roomState.syncItemsAndFlags) { + return; + } + + 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(); + + 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.ship.quest = loadedData.ship.quest; + + for (int i = 0; i < 124; i++) { + if (i == SCENE_WATER_TEMPLE) { + // Keep water temple water level flags + u32 mask = (1 << 0x1C) | (1 << 0x1D) | (1 << 0x1E); + loadedData.sceneFlags[i].swch = + (loadedData.sceneFlags[i].swch & ~mask) | (gSaveContext.sceneFlags[i].swch & mask); + } + + if (i == SCENE_FOREST_TEMPLE) { + // Keep forest temple elevator flag + u32 mask = (1 << 0x1B); + loadedData.sceneFlags[i].swch = + (loadedData.sceneFlags[i].swch & ~mask) | (gSaveContext.sceneFlags[i].swch & mask); + } + + 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 < ceil((RAND_INF_MAX + 15) / 16); i++) { + gSaveContext.ship.randomizerInf[i] = loadedData.ship.randomizerInf[i]; + } + + for (int i = 0; i < 6; i++) { + gSaveContext.gsFlags[i] = loadedData.gsFlags[i]; + } + + gSaveContext.ship.stats.fileCreatedAt = loadedData.ship.stats.fileCreatedAt; + + // Restore master sword state + // Disabling this for now, not really sure I understand why I did this in the past + // 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(); + OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetCheckStatus( + payload["state"]["rando"]["itemLocations"][i][0].get()); + OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetIsSkipped( + payload["state"]["rando"]["itemLocations"][i][0].get()); + + // if (payload["state"]["rando"]["itemLocations"][i].contains("fakeRgID")) { + // randoContext->overrides.emplace(static_cast(i), + // Rando::ItemOverride(static_cast(i), + // payload["state"]["rando"]["itemLocations"][i]["fakeRgID"].get())); + // randoContext->GetItemOverride(i).GetTrickName().english = + // payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"].get(); + // randoContext->GetItemOverride(i).GetTrickName().french = + // payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"].get(); + // } + // if (payload["state"]["rando"]["itemLocations"][i].contains("price")) { + // u16 price = payload["state"]["rando"]["itemLocations"][i]["price"].get(); + // 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(); entranceCtx->entranceOverrides[i].index + // = payload["state"]["rando"]["entrances"][i]["index"].get(); + // entranceCtx->entranceOverrides[i].destination = + // payload["state"]["rando"]["entrances"][i]["destination"].get(); + // entranceCtx->entranceOverrides[i].override = + // payload["state"]["rando"]["entrances"][i]["override"].get(); + // entranceCtx->entranceOverrides[i].overrideDestination = + // payload["state"]["rando"]["entrances"][i]["overrideDestination"].get(); + // } + + // for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) { + // randoContext->hashIconIndexes[i] = payload["state"]["rando"]["seed"][i].get(); + // } + // randoContext->GetSettings()->SetSeedString(payload["state"]["rando"]["inputSeed"].get()); + // randoContext->GetSettings()->SetSeed(payload["state"]["rando"]["finalSeed"].get()); + + // for (int i = 0; i < RSK_MAX; i++) { + // randoContext->GetOption(RandomizerSettingKey(i)).SetSelectedIndex(payload["state"]["rando"]["randoSettings"][i].get()); + // } + + // randoContext->GetDungeons()->ClearAllMQ(); + // for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) { + // if (payload["state"]["rando"]["masterQuestDungeons"][i].get()) { + // randoContext->GetDungeon(i)->SetMQ(); + // } + // } + + // randoContext->GetTrials()->SkipAll(); + // for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) { + // if (payload["state"]["rando"]["requiredTrials"][i].get()) { + // 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()); + incomingPacketQueue.push(itemPayload); + } + } + isHandlingUpdateTeamState = false; +} diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 4a96bae2d..ffc1aaa24 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -77,6 +77,7 @@ #include "SaveManager.h" #include "soh/Network/CrowdControl/CrowdControl.h" #include "soh/Network/Sail/Sail.h" +#include "soh/Network/Anchor/Anchor.h" #include "Enhancements/mods.h" #include "Enhancements/game-interactor/GameInteractor.h" #include "Enhancements/randomizer/draw.h" @@ -142,6 +143,7 @@ AudioCollection* AudioCollection::Instance; SpeechSynthesizer* SpeechSynthesizer::Instance; CrowdControl* CrowdControl::Instance; Sail* Sail::Instance; +Anchor* Anchor::Instance; extern "C" char** cameraStrings; std::vector> cameraStdStrings; @@ -1289,6 +1291,7 @@ extern "C" void InitOTR(int argc, char* argv[]) { CrowdControl::Instance = new CrowdControl(); Sail::Instance = new Sail(); + Anchor::Instance = new Anchor(); OTRMessage_Init(); OTRAudio_Init(); @@ -1325,6 +1328,9 @@ extern "C" void InitOTR(int argc, char* argv[]) { if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) { Sail::Instance->Enable(); } + if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) { + Anchor::Instance->Enable(); + } } extern "C" void SaveManager_ThreadPoolWait() { @@ -1340,6 +1346,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(); + } #ifdef ENABLE_REMOTE_CONTROL SDLNet_Quit(); #endif diff --git a/soh/soh/SohGui/SohGui.cpp b/soh/soh/SohGui/SohGui.cpp index 23befb2f3..172f923e4 100644 --- a/soh/soh/SohGui/SohGui.cpp +++ b/soh/soh/SohGui/SohGui.cpp @@ -34,6 +34,7 @@ #include "soh/Notification/Notification.h" #include "soh/Enhancements/TimeDisplay/TimeDisplay.h" #include "soh/Enhancements/mod_menu.h" +#include "soh/Network/Anchor/Anchor.h" namespace SohGui { @@ -98,6 +99,7 @@ std::shared_ptr mRandomizerSettingsWindow; std::shared_ptr mModalWindow; std::shared_ptr mNotificationWindow; std::shared_ptr mTimeDisplayWindow; +std::shared_ptr mAnchorRoomWindow; UIWidgets::Colors GetMenuThemeColor() { return mSohMenu->GetMenuThemeColor(); @@ -205,6 +207,8 @@ void SetupGuiElements() { mNotificationWindow->Show(); mTimeDisplayWindow = std::make_shared(CVAR_WINDOW("TimeDisplayEnabled"), "Additional Timers"); gui->AddGuiWindow(mTimeDisplayWindow); + mAnchorRoomWindow = std::make_shared(CVAR_WINDOW("AnchorRoom"), "Anchor Room"); + gui->AddGuiWindow(mAnchorRoomWindow); } void Destroy() { @@ -240,6 +244,7 @@ void Destroy() { mTimeSplitWindow = nullptr; mPlandomizerWindow = nullptr; mTimeDisplayWindow = nullptr; + mAnchorRoomWindow = nullptr; } void RegisterPopup(std::string title, std::string message, std::string button1, std::string button2, diff --git a/soh/soh/SohGui/SohMenuNetwork.cpp b/soh/soh/SohGui/SohMenuNetwork.cpp index c7296496d..716e4652c 100644 --- a/soh/soh/SohGui/SohMenuNetwork.cpp +++ b/soh/soh/SohGui/SohMenuNetwork.cpp @@ -180,6 +180,8 @@ void SohMenu::AddMenuNetwork() { .RaceDisable(true) .Options(CheckboxOptions().Tooltip("Enemies spawned by CrowdControl won't be considered for \"clear enemy " "rooms\", so they don't need to be killed to complete these rooms.")); + path.sidebarName = "Anchor"; + AddSidebarEntry("Network", path.sidebarName, 2); } } // namespace SohGui diff --git a/soh/soh/SohGui/UIWidgets.cpp b/soh/soh/SohGui/UIWidgets.cpp index 774407583..ca5b4be9b 100644 --- a/soh/soh/SohGui/UIWidgets.cpp +++ b/soh/soh/SohGui/UIWidgets.cpp @@ -767,15 +767,18 @@ bool InputString(const char* label, std::string* value, const InputOptions& opti ImGui::PushStyleColor(ImGuiCol_Border, ColorValues.at(Colors::Red)); } float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; - if (options.alignment == ComponentAlignments::Left) { - if (options.labelPosition == LabelPositions::Above) { - ImGui::Text(label, *value->c_str()); - } - } else if (options.alignment == ComponentAlignments::Right) { - if (options.labelPosition == LabelPositions::Above) { - ImGui::NewLine(); - ImGui::SameLine(width - ImGui::CalcTextSize(label).x); - ImGui::Text(label, *value->c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label, NULL, true); + if (labelSize.x != 0) { + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text(label, *value->c_str()); + } + } else if (options.alignment == ComponentAlignments::Right) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::NewLine(); + ImGui::SameLine(width - ImGui::CalcTextSize(label).x); + ImGui::Text(label, *value->c_str()); + } } } ImGui::SetNextItemWidth(width); diff --git a/soh/soh/cvar_prefixes.h b/soh/soh/cvar_prefixes.h index 375833cb1..1ac1ea7b9 100644 --- a/soh/soh/cvar_prefixes.h +++ b/soh/soh/cvar_prefixes.h @@ -15,5 +15,6 @@ #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) +#define CVAR_REMOTE_ANCHOR(var) CVAR_REMOTE("Anchor." var) #define CVAR_GAMEPLAY_STATS(var) CVAR_PREFIX_GAMEPLAY_STATS "." var #define CVAR_TIME_DISPLAY(var) CVAR_PREFIX_TIME_DISPLAY "." var \ No newline at end of file diff --git a/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.h b/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.h index 0c7445397..0523823c9 100644 --- a/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.h +++ b/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.h @@ -6,7 +6,7 @@ struct EnSw; -typedef void (*EnSwActionFunc)(struct EnSw* this, PlayState* play); +typedef void (*EnSwActionFunc)(struct EnSw* thisx, PlayState* play); typedef struct EnSw { /* 0x0000 */ Actor actor;