Anchor Beta

This commit is contained in:
Garrett Cox 2024-10-20 01:31:29 -05:00
parent 7df9641297
commit 468929e9bd
68 changed files with 2753 additions and 44 deletions

View File

@ -21,8 +21,11 @@ DEFINE_HOOK(OnFlagSet, (int16_t flagType, int16_t flag));
DEFINE_HOOK(OnFlagUnset, (int16_t flagType, int16_t flag));
DEFINE_HOOK(OnSceneSpawnActors, ());
DEFINE_HOOK(OnPlayerUpdate, ());
DEFINE_HOOK(OnPlayerSfx, (u16 sfxId));
DEFINE_HOOK(OnOcarinaSongAction, ());
DEFINE_HOOK(OnShopSlotChange, (uint8_t cursorIndex, int16_t price));
DEFINE_HOOK(OnDungeonKeyUsed, (uint16_t mapIndex));
DEFINE_HOOK(ShouldActorInit, (void* actor, bool* result));
DEFINE_HOOK(OnActorInit, (void* actor));
DEFINE_HOOK(OnActorUpdate, (void* actor));
DEFINE_HOOK(OnActorKill, (void* actor));
@ -35,7 +38,7 @@ DEFINE_HOOK(OnPlayerBottleUpdate, (int16_t contents));
DEFINE_HOOK(OnPlayDestroy, ());
DEFINE_HOOK(OnPlayDrawEnd, ());
DEFINE_HOOK(OnVanillaBehavior, (GIVanillaBehavior flag, bool* result, va_list originalArgs));
DEFINE_HOOK(OnSaveFile, (int32_t fileNum));
DEFINE_HOOK(OnSaveFile, (int32_t fileNum, int32_t sectionID));
DEFINE_HOOK(OnLoadFile, (int32_t fileNum));
DEFINE_HOOK(OnDeleteFile, (int32_t fileNum));
@ -62,3 +65,7 @@ DEFINE_HOOK(OnSetGameLanguage, ());
DEFINE_HOOK(OnFileDropped, (std::string filePath));
DEFINE_HOOK(OnAssetAltChange, ());
DEFINE_HOOK(OnKaleidoUpdate, ());
DEFINE_HOOK(OnRandoSetCheckStatus, (RandomizerCheck rc, RandomizerCheckStatus status));
DEFINE_HOOK(OnRandoSetIsSkipped, (RandomizerCheck rc, bool isSkipped));
DEFINE_HOOK(OnRandoEntranceDiscovered, (u16 entranceIndex, u8 isReversedEntrance));

View File

@ -76,6 +76,10 @@ void GameInteractor_ExecuteOnPlayerUpdate() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerUpdate>();
}
void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerSfx>(sfxId);
}
void GameInteractor_ExecuteOnOcarinaSongAction() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOcarinaSongAction>();
}
@ -84,6 +88,19 @@ void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t pr
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnShopSlotChange>(cursorIndex, price);
}
void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnDungeonKeyUsed>(mapIndex);
}
bool GameInteractor_ShouldActorInit(void* actor) {
bool result = true;
GameInteractor::Instance->ExecuteHooks<GameInteractor::ShouldActorInit>(actor, &result);
GameInteractor::Instance->ExecuteHooksForID<GameInteractor::ShouldActorInit>(((Actor*)actor)->id, actor, &result);
GameInteractor::Instance->ExecuteHooksForPtr<GameInteractor::ShouldActorInit>((uintptr_t)actor, actor, &result);
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::ShouldActorInit>(actor, &result);
return result;
}
void GameInteractor_ExecuteOnActorInit(void* actor) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnActorInit>(actor);
GameInteractor::Instance->ExecuteHooksForID<GameInteractor::OnActorInit>(((Actor*)actor)->id, actor);
@ -166,8 +183,8 @@ bool GameInteractor_Should(GIVanillaBehavior flag, u32 result, ...) {
// MARK: - Save Files
void GameInteractor_ExecuteOnSaveFile(int32_t fileNum) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum);
void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum, sectionID);
}
void GameInteractor_ExecuteOnLoadFile(int32_t fileNum) {
@ -267,3 +284,8 @@ void GameInteractor_RegisterOnAssetAltChange(void (*fn)(void)) {
void GameInteractor_ExecuteOnKaleidoUpdate() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnKaleidoUpdate>();
}
// MARK: - Rando
void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnRandoEntranceDiscovered>(entranceIndex, isReversedEntrance);
}

View File

@ -23,7 +23,9 @@ void GameInteractor_ExecuteOnFlagSet(int16_t flagType, int16_t flag);
void GameInteractor_ExecuteOnFlagUnset(int16_t flagType, int16_t flag);
void GameInteractor_ExecuteOnSceneSpawnActors();
void GameInteractor_ExecuteOnPlayerUpdate();
void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId);
void GameInteractor_ExecuteOnOcarinaSongAction();
bool GameInteractor_ShouldActorInit(void* actor);
void GameInteractor_ExecuteOnActorInit(void* actor);
void GameInteractor_ExecuteOnActorUpdate(void* actor);
void GameInteractor_ExecuteOnActorKill(void* actor);
@ -35,12 +37,13 @@ void GameInteractor_ExecuteOnPlayerHealthChange(int16_t amount);
void GameInteractor_ExecuteOnPlayerBottleUpdate(int16_t contents);
void GameInteractor_ExecuteOnOcarinaSongAction();
void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t price);
void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex);
void GameInteractor_ExecuteOnPlayDestroy();
void GameInteractor_ExecuteOnPlayDrawEnd();
bool GameInteractor_Should(GIVanillaBehavior flag, uint32_t result, ...);
// MARK: - Save Files
void GameInteractor_ExecuteOnSaveFile(int32_t fileNum);
void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID);
void GameInteractor_ExecuteOnLoadFile(int32_t fileNum);
void GameInteractor_ExecuteOnDeleteFile(int32_t fileNum);
@ -74,6 +77,9 @@ void GameInteractor_RegisterOnAssetAltChange(void (*fn)(void));
//Mark: - Pause Menu
void GameInteractor_ExecuteOnKaleidoUpdate();
// MARK: - Rando
void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance);
#ifdef __cplusplus
}
#endif

View File

@ -1023,12 +1023,12 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l
if (item00->itemEntry.modIndex == MOD_NONE) {
Notification::Emit({
.itemIcon = GetTextureForItemId(item00->itemEntry.itemId),
.message = "You found ",
.message = "You found",
.suffix = SohUtils::GetItemName(item00->itemEntry.itemId),
});
} else if (item00->itemEntry.modIndex == MOD_RANDOMIZER) {
Notification::Emit({
.message = "You found ",
.message = "You found",
.suffix = Rando::StaticData::RetrieveItem((RandomizerGet)item00->itemEntry.getItemId).GetName().english,
});
}

View File

@ -131,6 +131,7 @@ bool ItemLocation::HasObtained() const {
void ItemLocation::SetCheckStatus(RandomizerCheckStatus status_) {
status = status_;
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnRandoSetCheckStatus>(rc, status);
}
RandomizerCheckStatus ItemLocation::GetCheckStatus() {
@ -139,6 +140,7 @@ RandomizerCheckStatus ItemLocation::GetCheckStatus() {
void ItemLocation::SetIsSkipped(bool isSkipped_) {
isSkipped = isSkipped_;
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnRandoSetIsSkipped>(rc, isSkipped);
}
bool ItemLocation::GetIsSkipped() {

View File

@ -15,6 +15,7 @@
#include "global.h"
#include "entrance.h"
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
extern PlayState* gPlayState;
@ -778,6 +779,8 @@ void Entrance_SetEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) {
return;
}
GameInteractor_ExecuteOnRandoEntranceDiscovered(entranceIndex, isReversedEntrance);
u16 bitsPerIndex = sizeof(u32) * 8;
u32 idx = entranceIndex / bitsPerIndex;
if (idx < SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT) {

View File

@ -0,0 +1,405 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/OTRGlobals.h"
#include "soh/Enhancements/nametag.h"
extern "C" {
#include "variables.h"
#include "functions.h"
extern PlayState* gPlayState;
}
// MARK: - Overrides
void Anchor::Enable() {
Network::Enable(CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.proxysaw.dev"), CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383));
ownClientId = CVarGetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), 0);
roomState.ownerClientId = 0;
}
void Anchor::Disable() {
Network::Disable();
clients.clear();
RefreshClientActors();
}
void Anchor::OnConnected() {
SendPacket_Handshake();
RegisterHooks();
if (IsSaveLoaded()) {
SendPacket_RequestTeamState();
}
}
void Anchor::OnDisconnected() {
RegisterHooks();
}
void Anchor::SendJsonToRemote(nlohmann::json payload) {
if (!isConnected) {
return;
}
payload["clientId"] = ownClientId;
if (!payload.contains("quiet")) {
SPDLOG_INFO("[Anchor] Sending payload:\n{}", payload.dump());
}
Network::SendJsonToRemote(payload);
}
void Anchor::OnIncomingJson(nlohmann::json payload) {
// If it doesn't contain a type, it's not a valid payload
if (!payload.contains("type")) {
return;
}
// If it's not a quiet payload, log it
if (!payload.contains("quiet")) {
SPDLOG_INFO("[Anchor] Received payload:\n{}", payload.dump());
}
std::string packetType = payload["type"].get<std::string>();
// Ignore packets from mismatched clients, except for ALL_CLIENT_STATE or UPDATE_CLIENT_STATE
if (packetType != ALL_CLIENT_STATE && packetType != UPDATE_CLIENT_STATE) {
if (payload.contains("clientId")) {
uint32_t clientId = payload["clientId"].get<uint32_t>();
if (clients.contains(clientId) && clients[clientId].clientVersion != clientVersion) {
return;
}
}
}
// packetType here is a string so we can't use a switch statement
if (packetType == ALL_CLIENT_STATE) HandlePacket_AllClientState(payload);
else if (packetType == CONSUME_ADULT_TRADE_ITEM) HandlePacket_ConsumeAdultTradeItem(payload);
else if (packetType == DAMAGE_PLAYER) HandlePacket_DamagePlayer(payload);
else if (packetType == DISABLE_ANCHOR) HandlePacket_DisableAnchor(payload);
else if (packetType == ENTRANCE_DISCOVERED) HandlePacket_EntranceDiscovered(payload);
else if (packetType == GAME_COMPLETE) HandlePacket_GameComplete(payload);
else if (packetType == GIVE_ITEM) HandlePacket_GiveItem(payload);
else if (packetType == PLAYER_SFX) HandlePacket_PlayerSfx(payload);
else if (packetType == PLAYER_UPDATE) HandlePacket_PlayerUpdate(payload);
else if (packetType == UPDATE_TEAM_STATE) HandlePacket_UpdateTeamState(payload);
else if (packetType == REQUEST_TEAM_STATE) HandlePacket_RequestTeamState(payload);
else if (packetType == REQUEST_TELEPORT) HandlePacket_RequestTeleport(payload);
else if (packetType == SERVER_MESSAGE) HandlePacket_ServerMessage(payload);
else if (packetType == SET_CHECK_STATUS) HandlePacket_SetCheckStatus(payload);
else if (packetType == SET_FLAG) HandlePacket_SetFlag(payload);
else if (packetType == TELEPORT_TO) HandlePacket_TeleportTo(payload);
else if (packetType == UNSET_FLAG) HandlePacket_UnsetFlag(payload);
else if (packetType == UPDATE_BEANS_COUNT) HandlePacket_UpdateBeansCount(payload);
else if (packetType == UPDATE_CLIENT_STATE) HandlePacket_UpdateClientState(payload);
else if (packetType == UPDATE_ROOM_STATE) HandlePacket_UpdateRoomState(payload);
else if (packetType == UPDATE_DUNGEON_ITEMS) HandlePacket_UpdateDungeonItems(payload);
}
// Macros to let us easily register and unregister functions when the anchor is enabled/disabled
#define HOOK(hook, condition, body) \
static HOOK_ID hook = 0; \
GameInteractor::Instance->UnregisterGameHook<GameInteractor::hook>(hook); \
hook = 0; \
if (condition) { \
hook = GameInteractor::Instance->RegisterGameHook<GameInteractor::hook>(body); \
}
#define HOOK_FOR_ID(hook, condition, id, body) \
static HOOK_ID hook = 0; \
GameInteractor::Instance->UnregisterGameHookForID<GameInteractor::hook>(hook); \
hook = 0; \
if (condition) { \
hook = GameInteractor::Instance->RegisterGameHookForID<GameInteractor::hook>(id, body); \
}
void Anchor::RegisterHooks() {
HOOK(OnSceneSpawnActors, isConnected, [&]() {
SendPacket_UpdateClientState();
if (IsSaveLoaded()) {
RefreshClientActors();
}
});
HOOK(OnPresentFileSelect, isConnected, [&]() {
SendPacket_UpdateClientState();
});
HOOK_FOR_ID(ShouldActorInit, isConnected, ACTOR_PLAYER, [&](void* actorRef, bool* should) {
Actor* actor = (Actor*)actorRef;
if (refreshingActors) {
// By the time we get here, the actor was already added to the ACTORCAT_PLAYER list, so we need to move it
Actor_ChangeCategory(gPlayState, &gPlayState->actorCtx, actor, ACTORCAT_NPC);
actor->id = ACTOR_EN_OE2;
actor->category = ACTORCAT_NPC;
actor->init = DummyPlayer_Init;
actor->update = DummyPlayer_Update;
actor->draw = DummyPlayer_Draw;
actor->destroy = DummyPlayer_Destroy;
}
});
HOOK(OnPlayerUpdate, isConnected, [&]() {
if (justLoadedSave) {
justLoadedSave = false;
SendPacket_RequestTeamState();
}
SendPacket_PlayerUpdate();
});
HOOK(OnPlayerSfx, isConnected, [&](u16 sfxId) {
SendPacket_PlayerSfx(sfxId);
});
HOOK(OnLoadGame, isConnected, [&](s16 fileNum) {
justLoadedSave = true;
});
HOOK(OnSaveFile, isConnected, [&](s16 fileNum, int sectionID) {
if (sectionID == 0) {
SendPacket_UpdateTeamState();
}
});
HOOK(OnFlagSet, isConnected, [&](s16 flagType, s16 flag) {
SendPacket_SetFlag(SCENE_ID_MAX, flagType, flag);
});
HOOK(OnFlagUnset, isConnected, [&](s16 flagType, s16 flag) {
SendPacket_UnsetFlag(SCENE_ID_MAX, flagType, flag);
});
HOOK(OnSceneFlagSet, isConnected, [&](s16 sceneNum, s16 flagType, s16 flag) {
SendPacket_SetFlag(sceneNum, flagType, flag);
});
HOOK(OnSceneFlagUnset, isConnected, [&](s16 sceneNum, s16 flagType, s16 flag) {
SendPacket_UnsetFlag(sceneNum, flagType, flag);
});
HOOK(OnRandoSetCheckStatus, isConnected, [&](RandomizerCheck rc, RandomizerCheckStatus status) {
if (!isHandlingUpdateTeamState) {
SendPacket_SetCheckStatus(rc);
}
});
HOOK(OnRandoSetIsSkipped, isConnected, [&](RandomizerCheck rc, bool isSkipped) {
if (!isHandlingUpdateTeamState) {
SendPacket_SetCheckStatus(rc);
}
});
HOOK(OnRandoEntranceDiscovered, isConnected, [&](u16 entranceIndex, u8 isReversedEntrance) {
SendPacket_EntranceDiscovered(entranceIndex);
});
HOOK_FOR_ID(OnBossDefeat, isConnected, ACTOR_BOSS_GANON2, [&](void* refActor) {
SendPacket_GameComplete();
});
HOOK(OnItemReceive, isConnected, [&](GetItemEntry itemEntry) {
// Handle vanilla dungeon items a bit differently
if (itemEntry.modIndex == MOD_NONE && (itemEntry.itemId >= ITEM_KEY_BOSS && itemEntry.itemId <= ITEM_KEY_SMALL)) {
SendPacket_UpdateDungeonItems();
return;
}
SendPacket_GiveItem(itemEntry.tableId, itemEntry.getItemId);
});
HOOK(OnDungeonKeyUsed, isConnected, [&](uint16_t mapIndex) {
// Handle vanilla dungeon items a bit differently
SendPacket_UpdateDungeonItems();
});
}
// MARK: - Misc/Helpers
// Kills all existing anchor actors and respawns them with the new client data
void Anchor::RefreshClientActors() {
if (!IsSaveLoaded()) {
return;
}
Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_NPC].head;
while (actor != NULL) {
if (actor->id == ACTOR_EN_OE2 && actor->update == DummyPlayer_Update) {
NameTag_RemoveAllForActor(actor);
Actor_Kill(actor);
}
actor = actor->next;
}
actorIndexToClientId.clear();
refreshingActors = true;
for (auto& [clientId, client] : clients) {
if (!client.online || client.self) {
continue;
}
actorIndexToClientId.push_back(clientId);
// We are using a hook `ShouldActorInit` to override the init/update/draw/destroy functions of the Player we spawn
// We quickly store a mapping of "index" to clientId, then within the init function we use this to get the clientId
// and store it on player->zTargetActiveTimer (unused s32 for the dummy) for convenience
auto dummy = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_PLAYER, client.posRot.pos.x,
client.posRot.pos.y, client.posRot.pos.z, client.posRot.rot.x, client.posRot.rot.y,
client.posRot.rot.z, actorIndexToClientId.size() - 1, false);
client.player = (Player*)dummy;
}
refreshingActors = false;
}
bool Anchor::IsSaveLoaded() {
if (gPlayState == nullptr) {
return false;
}
if (GET_PLAYER(gPlayState) == nullptr) {
return false;
}
if (gSaveContext.fileNum < 0 || gSaveContext.fileNum > 2) {
return false;
}
if (gSaveContext.gameMode != GAMEMODE_NORMAL) {
return false;
}
return true;
}
// MARK: - UI
void Anchor::DrawMenu() {
ImGui::PushID("Anchor");
std::string host = CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.proxysaw.dev");
uint16_t port = CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383);
std::string anchorTeamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
std::string anchorRoomId = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "");
std::string anchorName = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), "");
bool isFormValid = !SohUtils::IsStringEmpty(host) && port > 1024 && port < 65535 &&
!SohUtils::IsStringEmpty(anchorRoomId) && !SohUtils::IsStringEmpty(anchorName);
ImGui::SeparatorText("Anchor");
// UIWidgets::Tooltip("Anchor Stuff");
if (ImGui::IsItemClicked()) {
// ImGui::SetClipboardText("https://github.com/garrettjoecox/anchor");
}
ImGui::BeginDisabled(isEnabled);
ImGui::Text("Host & Port");
if (UIWidgets::InputString("##Host", &host)) {
CVarSetString(CVAR_REMOTE_ANCHOR("Host"), host.c_str());
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5);
if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) {
CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), port);
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
}
ImGui::Text("Tunic Color & Name");
static Color_RGBA8 color = CVarGetColor(CVAR_REMOTE_ANCHOR("Color"), { 100, 255, 100, 255 });
static ImVec4 colorVec = ImVec4(color.r / 255.0, color.g / 255.0, color.b / 255.0, 1);
if (ImGui::ColorEdit3("##Color", (float*)&colorVec,
ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) {
color.r = colorVec.x * 255.0;
color.g = colorVec.y * 255.0;
color.b = colorVec.z * 255.0;
CVarSetColor(CVAR_REMOTE_ANCHOR("Color"), color);
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (UIWidgets::InputString("##Name", &anchorName)) {
CVarSetString(CVAR_REMOTE_ANCHOR("Name"), anchorName.c_str());
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
}
ImGui::Text("Room ID");
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (UIWidgets::InputString("##RoomId", &anchorRoomId, isEnabled ? ImGuiInputTextFlags_Password : 0)) {
CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), anchorRoomId.c_str());
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
}
ImGui::Text("Team ID (Items & Flags Shared)");
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (UIWidgets::InputString("##TeamId", &anchorTeamId)) {
CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), anchorTeamId.c_str());
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
}
ImGui::EndDisabled();
ImGui::Spacing();
ImGui::BeginDisabled(!isFormValid);
const char* buttonLabel = isEnabled ? "Disable" : "Enable";
if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) {
if (isEnabled) {
CVarClear(CVAR_REMOTE_ANCHOR("Enabled"));
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
Disable();
} else {
CVarSetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 1);
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
Enable();
}
}
ImGui::EndDisabled();
if (isEnabled) {
ImGui::Spacing();
if (isConnected) {
ImGui::Text("Connected");
if (roomState.ownerClientId == ownClientId) {
if (ImGui::BeginMenu("Room Settings")) {
ImGui::Text("PvP Mode:");
static const char* pvpModes[3] = { "Off", "On", "On + Friendly Fire" };
if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), pvpModes, 1)) {
SendPacket_UpdateRoomState();
}
ImGui::Text("Show Locations For:");
static const char* showLocationsModes[3] = { "None", "Team Only", "All" };
if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), showLocationsModes, 1)) {
SendPacket_UpdateRoomState();
}
ImGui::Text("Allow Teleporting To:");
static const char* teleportModes[3] = { "None", "Team Only", "All" };
if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), teleportModes, 1)) {
SendPacket_UpdateRoomState();
}
ImGui::EndMenu();
}
}
if (ImGui::Button("Request Team State", ImVec2(ImGui::GetContentRegionAvail().x - 25.0f, 0.0f))) {
SendPacket_RequestTeamState();
}
if (roomState.ownerClientId == ownClientId) {
ImGui::SameLine();
if (ImGui::Button(ICON_FA_TRASH)) {
SendPacket_ClearTeamState();
}
UIWidgets::Tooltip("Clear Team State");
}
} else {
ImGui::Text("Connecting...");
}
}
ImGui::PopID();
}
#endif

View File

@ -0,0 +1,177 @@
#ifdef ENABLE_REMOTE_CONTROL
#ifndef NETWORK_ANCHOR_H
#define NETWORK_ANCHOR_H
#ifdef __cplusplus
#include "soh/Network/Network.h"
#include <libultraship/libultraship.h>
extern "C" {
#include "variables.h"
#include "z64.h"
}
void DummyPlayer_Init(Actor* actor, PlayState* play);
void DummyPlayer_Update(Actor* actor, PlayState* play);
void DummyPlayer_Draw(Actor* actor, PlayState* play);
void DummyPlayer_Destroy(Actor* actor, PlayState* play);
typedef struct {
uint32_t clientId;
std::string name;
Color_RGB8 color;
std::string clientVersion;
std::string teamId;
bool online;
bool self;
uint32_t seed;
bool isSaveLoaded;
bool isGameComplete;
s16 sceneNum;
s32 entranceIndex;
// Only available in PLAYER_UPDATE packets
s32 linkAge;
PosRot posRot;
Vec3s jointTable[24];
Vec3s upperLimbRot;
s8 currentBoots;
s8 currentShield;
s8 currentTunic;
u32 stateFlags1;
u32 stateFlags2;
u8 buttonItem0;
s8 itemAction;
s8 heldItemAction;
u8 modelGroup;
s8 invincibilityTimer;
s16 unk_862;
s8 actionVar1;
// Ptr to the dummy player
Player* player;
} AnchorClient;
typedef struct {
uint32_t ownerClientId;
u8 pvpMode; // 0 = off, 1 = on, 2 = on with friendly fire
u8 showLocationsMode; // 0 = none, 1 = team, 2 = all
u8 teleportMode; // 0 = off, 1 = team, 2 = all
} RoomState;
class Anchor : public Network {
private:
bool refreshingActors = false;
bool justLoadedSave = false;
bool isHandlingUpdateTeamState = false;
uint32_t ownClientId;
nlohmann::json PrepClientState();
nlohmann::json PrepRoomState();
void RegisterHooks();
void RefreshClientActors();
void HandlePacket_AllClientState(nlohmann::json payload);
void HandlePacket_ConsumeAdultTradeItem(nlohmann::json payload);
void HandlePacket_DamagePlayer(nlohmann::json payload);
void HandlePacket_DisableAnchor(nlohmann::json payload);
void HandlePacket_EntranceDiscovered(nlohmann::json payload);
void HandlePacket_GameComplete(nlohmann::json payload);
void HandlePacket_GiveItem(nlohmann::json payload);
void HandlePacket_PlayerSfx(nlohmann::json payload);
void HandlePacket_PlayerUpdate(nlohmann::json payload);
void HandlePacket_RequestTeamState(nlohmann::json payload);
void HandlePacket_RequestTeleport(nlohmann::json payload);
void HandlePacket_ServerMessage(nlohmann::json payload);
void HandlePacket_SetCheckStatus(nlohmann::json payload);
void HandlePacket_SetFlag(nlohmann::json payload);
void HandlePacket_TeleportTo(nlohmann::json payload);
void HandlePacket_UnsetFlag(nlohmann::json payload);
void HandlePacket_UpdateBeansCount(nlohmann::json payload);
void HandlePacket_UpdateClientState(nlohmann::json payload);
void HandlePacket_UpdateDungeonItems(nlohmann::json payload);
void HandlePacket_UpdateRoomState(nlohmann::json payload);
void HandlePacket_UpdateTeamState(nlohmann::json payload);
public:
inline static const std::string clientVersion = (char*)gBuildVersion;
// Packet types //
inline static const std::string ALL_CLIENT_STATE = "ALL_CLIENT_STATE";
inline static const std::string CONSUME_ADULT_TRADE_ITEM = "CONSUME_ADULT_TRADE_ITEM";
inline static const std::string DAMAGE_PLAYER = "DAMAGE_PLAYER";
inline static const std::string DISABLE_ANCHOR = "DISABLE_ANCHOR";
inline static const std::string ENTRANCE_DISCOVERED = "ENTRANCE_DISCOVERED";
inline static const std::string GAME_COMPLETE = "GAME_COMPLETE";
inline static const std::string GIVE_ITEM = "GIVE_ITEM";
inline static const std::string HANDSHAKE = "HANDSHAKE";
inline static const std::string PLAYER_SFX = "PLAYER_SFX";
inline static const std::string PLAYER_UPDATE = "PLAYER_UPDATE";
inline static const std::string REQUEST_TEAM_STATE = "REQUEST_TEAM_STATE";
inline static const std::string REQUEST_TELEPORT = "REQUEST_TELEPORT";
inline static const std::string SERVER_MESSAGE = "SERVER_MESSAGE";
inline static const std::string SET_CHECK_STATUS = "SET_CHECK_STATUS";
inline static const std::string SET_FLAG = "SET_FLAG";
inline static const std::string TELEPORT_TO = "TELEPORT_TO";
inline static const std::string UNSET_FLAG = "UNSET_FLAG";
inline static const std::string UPDATE_BEANS_COUNT = "UPDATE_BEANS_COUNT";
inline static const std::string UPDATE_CLIENT_STATE = "UPDATE_CLIENT_STATE";
inline static const std::string UPDATE_DUNGEON_ITEMS = "UPDATE_DUNGEON_ITEMS";
inline static const std::string UPDATE_ROOM_STATE = "UPDATE_ROOM_STATE";
inline static const std::string UPDATE_TEAM_STATE = "UPDATE_TEAM_STATE";
static Anchor* Instance;
std::map<uint32_t, AnchorClient> clients;
std::vector<uint32_t> actorIndexToClientId;
RoomState roomState;
void Enable();
void Disable();
void OnIncomingJson(nlohmann::json payload);
void OnConnected();
void OnDisconnected();
void DrawMenu();
void SendJsonToRemote(nlohmann::json packet);
bool IsSaveLoaded();
void SendPacket_ClearTeamState();
void SendPacket_ConsumeAdultTradeItem(u8 itemId);
void SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage);
void SendPacket_EntranceDiscovered(u16 entranceIndex);
void SendPacket_GameComplete();
void SendPacket_GiveItem(u16 modId, s16 getItemId);
void SendPacket_Handshake();
void SendPacket_PlayerSfx(u16 sfxId);
void SendPacket_PlayerUpdate();
void SendPacket_RequestTeamState();
void SendPacket_RequestTeleport(u32 clientId);
void SendPacket_SetCheckStatus(RandomizerCheck rc);
void SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag);
void SendPacket_TeleportTo(u32 clientId);
void SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag);
void SendPacket_UpdateBeansCount();
void SendPacket_UpdateClientState();
void SendPacket_UpdateDungeonItems();
void SendPacket_UpdateRoomState();
void SendPacket_UpdateTeamState();
};
typedef enum {
// Starting at 5 to continue from the last value in the PlayerDamageResponseType enum
DUMMY_PLAYER_HIT_RESPONSE_STUN = 5,
DUMMY_PLAYER_HIT_RESPONSE_FIRE,
DUMMY_PLAYER_HIT_RESPONSE_NORMAL,
} DummyPlayerDamageResponseType;
class AnchorRoomWindow : public Ship::GuiWindow {
public:
using GuiWindow::GuiWindow;
void InitElement() override {};
void DrawElement() override {};
void Draw() override;
void UpdateElement() override {};
};
#endif // __cplusplus
#endif // NETWORK_ANCHOR_H
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,122 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "Anchor.h"
#include "soh/OTRGlobals.h"
extern "C" {
#include "variables.h"
#include "functions.h"
extern PlayState* gPlayState;
}
void AnchorRoomWindow::Draw() {
if (!Anchor::Instance->isConnected) {
return;
}
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, CVarGetFloat(CVAR_SETTING("Notifications.BgOpacity"), 0.5f)));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
auto vp = ImGui::GetMainViewport();
ImGui::SetNextWindowViewport(vp->ID);
ImGui::Begin("Anchor Room", nullptr,
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollWithMouse |
ImGuiWindowFlags_NoScrollbar
);
// First build a list of teams
std::set<std::string> teams;
for (auto& [clientId, client] : Anchor::Instance->clients) {
teams.insert(client.teamId);
}
for (auto& team : teams) {
if (teams.size() > 1) {
ImGui::SeparatorText(team.c_str());
}
bool isOwnTeam = team == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
for (auto& [clientId, client] : Anchor::Instance->clients) {
if (client.teamId != team) {
continue;
}
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->IsSaveLoaded() && !client.self && client.isSaveLoaded &&
(Anchor::Instance->roomState.teleportMode == 2 || (Anchor::Instance->roomState.teleportMode == 1 && isOwnTeam))
) {
ImGui::SameLine();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
if (ImGui::Button(ICON_FA_LOCATION_ARROW, ImVec2(15.0f, 15.0f))) {
Anchor::Instance->SendPacket_RequestTeleport(client.clientId);
}
ImGui::PopStyleVar();
}
if (client.clientVersion != Anchor::clientVersion) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Incompatible version! Will not work together!");
ImGui::Text("Yours: %s", Anchor::clientVersion.c_str());
ImGui::Text("Theirs: %s", client.clientVersion.c_str());
ImGui::EndTooltip();
}
}
uint32_t seed = IS_RANDO ? Rando::Context::GetInstance()->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();
}
}
ImGui::End();
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,216 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "Anchor.h"
#include "soh/Enhancements/nametag.h"
#include "soh/frame_interpolation.h"
extern "C" {
#include "macros.h"
#include "variables.h"
#include "functions.h"
extern PlayState* gPlayState;
void Player_UseItem(PlayState* play, Player* player, s32 item);
void Player_Draw(Actor* actor, PlayState* play);
}
// Hijacking player->zTargetActiveTimer (unused s32 for the dummy) to store the clientId for convenience
#define DUMMY_CLIENT_ID player->zTargetActiveTimer
static DamageTable DummyPlayerDamageTable = {
/* Deku nut */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
/* Deku stick */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Slingshot */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Explosive */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Boomerang */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
/* Normal arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Hammer swing */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE),
/* Hookshot */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
/* Kokiri sword */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Master sword */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Giant's Knife */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Fire arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_FIRE),
/* Ice arrow */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_ICE_TRAP),
/* Light arrow */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK),
/* Unk arrow 1 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
/* Unk arrow 2 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
/* Unk arrow 3 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
/* Fire magic */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_FIRE),
/* Ice magic */ DMG_ENTRY(3, PLAYER_HIT_RESPONSE_ICE_TRAP),
/* Light magic */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK),
/* Shield */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
/* Mirror Ray */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
/* Kokiri spin */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Giant spin */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Master spin */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Kokiri jump */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Giant jump */ DMG_ENTRY(8, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Master jump */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
/* Unknown 1 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
/* Unblockable */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
/* Hammer jump */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE),
/* Unknown 2 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
};
void DummyPlayer_Init(Actor* actor, PlayState* play) {
Player* player = (Player*)actor;
uint32_t clientId = Anchor::Instance->actorIndexToClientId[actor->params];
DUMMY_CLIENT_ID = clientId;
if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
Actor_Kill(actor);
return;
}
AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
// Hack to account for usage of gSaveContext in Player_Init
s32 originalAge = gSaveContext.linkAge;
gSaveContext.linkAge = client.linkAge;
// #region modeled after EnTorch2_Init and Player_Init
actor->room = -1;
player->itemAction = player->heldItemAction = -1;
player->heldItemId = ITEM_NONE;
Player_UseItem(play, player, ITEM_NONE);
Player_SetModelGroup(player, Player_ActionToModelGroup(player, player->heldItemAction));
play->playerInit(player, play, gPlayerSkelHeaders[client.linkAge]);
play->func_11D54(player, play);
// #endregion
player->cylinder.base.acFlags = AC_ON | AC_TYPE_PLAYER;
player->cylinder.base.ocFlags2 = OC2_TYPE_1;
player->cylinder.info.bumperFlags = BUMP_ON | BUMP_HOOKABLE | BUMP_NO_HITMARK;
player->actor.flags |= ACTOR_FLAG_HOOKSHOT_PULLS_PLAYER;
player->cylinder.dim.radius = 30;
player->actor.colChkInfo.damageTable = &DummyPlayerDamageTable;
gSaveContext.linkAge = originalAge;
NameTag_RegisterForActorWithOptions(actor, client.name.c_str(), { .yOffset = 30 });
}
void Math_Vec3s_Copy(Vec3s* dest, Vec3s* src) {
dest->x = src->x;
dest->y = src->y;
dest->z = src->z;
}
// Update the actor with new data from the client
void DummyPlayer_Update(Actor* actor, PlayState* play) {
Player* player = (Player*)actor;
if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
Actor_Kill(actor);
return;
}
AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) {
actor->world.pos.x = -9999.0f;
actor->world.pos.y = -9999.0f;
actor->world.pos.z = -9999.0f;
actor->shape.shadowAlpha = 0;
return;
}
actor->shape.shadowAlpha = 255;
Math_Vec3s_Copy(&player->upperLimbRot, &client.upperLimbRot);
Math_Vec3s_Copy(&actor->shape.rot, &client.posRot.rot);
Math_Vec3f_Copy(&actor->world.pos, &client.posRot.pos);
player->skelAnime.jointTable = client.jointTable;
player->currentBoots = client.currentBoots;
player->currentShield = client.currentShield;
player->currentTunic = client.currentTunic;
player->stateFlags1 = client.stateFlags1;
player->stateFlags2 = client.stateFlags2;
player->itemAction = client.itemAction;
player->heldItemAction = client.heldItemAction;
player->invincibilityTimer = client.invincibilityTimer;
player->unk_862 = client.unk_862;
player->av1.actionVar1 = client.actionVar1;
if (player->modelGroup != client.modelGroup) {
// Hack to account for usage of gSaveContext
s32 originalAge = gSaveContext.linkAge;
gSaveContext.linkAge = client.linkAge;
u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0];
gSaveContext.equips.buttonItems[0] = client.buttonItem0;
Player_SetModelGroup(player, client.modelGroup);
gSaveContext.linkAge = originalAge;
gSaveContext.equips.buttonItems[0] = originalButtonItem0;
}
if (
Anchor::Instance->roomState.pvpMode == 0 ||
(Anchor::Instance->roomState.pvpMode == 1 && client.teamId == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"))
) {
return;
}
if (player->cylinder.base.acFlags & AC_HIT && player->invincibilityTimer == 0) {
Anchor::Instance->SendPacket_DamagePlayer(client.clientId, player->actor.colChkInfo.damageEffect, player->actor.colChkInfo.damage);
if (player->actor.colChkInfo.damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) {
Actor_SetColorFilter(&player->actor, 0, 0xFF, 0, 24);
} else {
player->invincibilityTimer = 20;
}
}
Collider_UpdateCylinder(&player->actor, &player->cylinder);
if (!(player->stateFlags2 & PLAYER_STATE2_FROZEN)) {
if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_HANGING_OFF_LEDGE | PLAYER_STATE1_CLIMBING_LEDGE | PLAYER_STATE1_ON_HORSE))) {
CollisionCheck_SetOC(play, &play->colChkCtx, &player->cylinder.base);
}
if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_DAMAGED)) && (player->invincibilityTimer <= 0)) {
CollisionCheck_SetAC(play, &play->colChkCtx, &player->cylinder.base);
if (player->invincibilityTimer < 0) {
CollisionCheck_SetAT(play, &play->colChkCtx, &player->cylinder.base);
}
}
}
if (player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_IN_ITEM_CS | PLAYER_STATE1_IN_CUTSCENE)) {
player->actor.colChkInfo.mass = MASS_IMMOVABLE;
} else {
player->actor.colChkInfo.mass = 50;
}
Collider_ResetCylinderAC(play, &player->cylinder.base);
}
void DummyPlayer_Draw(Actor* actor, PlayState* play) {
Player* player = (Player*)actor;
if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
Actor_Kill(actor);
return;
}
AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) {
return;
}
// Hack to account for usage of gSaveContext in Player_Draw
s32 originalAge = gSaveContext.linkAge;
gSaveContext.linkAge = client.linkAge;
u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0];
gSaveContext.equips.buttonItems[0] = client.buttonItem0;
Player_Draw((Actor*)player, play);
gSaveContext.linkAge = originalAge;
gSaveContext.equips.buttonItems[0] = originalButtonItem0;
}
void DummyPlayer_Destroy(Actor* actor, PlayState* play) {
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,226 @@
#ifdef ENABLE_REMOTE_CONTROL
#ifndef NETWORK_ANCHOR_JSON_CONVERSIONS_H
#define NETWORK_ANCHOR_JSON_CONVERSIONS_H
#ifdef __cplusplus
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "Anchor.h"
extern "C" {
#include "z64.h"
}
using json = nlohmann::json;
inline void from_json(const json& j, Color_RGB8& color) {
j.at("r").get_to(color.r);
j.at("g").get_to(color.g);
j.at("b").get_to(color.b);
}
inline void to_json(json& j, const Color_RGB8& color) {
j = json{
{"r", color.r},
{"g", color.g},
{"b", color.b}
};
}
inline void to_json(json& j, const Vec3f& vec) {
j = json{
{"x", vec.x},
{"y", vec.y},
{"z", vec.z}
};
}
inline void to_json(json& j, const Vec3s& vec) {
j = json{
{"x", vec.x},
{"y", vec.y},
{"z", vec.z}
};
}
inline void from_json(const json& j, Vec3f& vec) {
j.at("x").get_to(vec.x);
j.at("y").get_to(vec.y);
j.at("z").get_to(vec.z);
}
inline void from_json(const json& j, Vec3s& vec) {
j.at("x").get_to(vec.x);
j.at("y").get_to(vec.y);
j.at("z").get_to(vec.z);
}
inline void to_json(json& j, const PosRot& posRot) {
j = json{
{"pos", posRot.pos},
{"rot", posRot.rot}
};
}
inline void from_json(const json& j, PosRot& posRot) {
j.at("pos").get_to(posRot.pos);
j.at("rot").get_to(posRot.rot);
}
inline void from_json(const json& j, AnchorClient& client) {
j.contains("clientId") ? j.at("clientId").get_to(client.clientId) : client.clientId = 0;
j.contains("name") ? j.at("name").get_to(client.name) : client.name = "???";
j.contains("color") ? j.at("color").get_to(client.color) : client.color = { 255, 255, 255 };
j.contains("clientVersion") ? j.at("clientVersion").get_to(client.clientVersion) : client.clientVersion = "???";
j.contains("teamId") ? j.at("teamId").get_to(client.teamId) : client.teamId = "default";
j.contains("online") ? j.at("online").get_to(client.online) : client.online = false;
j.contains("seed") ? j.at("seed").get_to(client.seed) : client.seed = 0;
j.contains("isSaveLoaded") ? j.at("isSaveLoaded").get_to(client.isSaveLoaded) : client.isSaveLoaded = false;
j.contains("isGameComplete") ? j.at("isGameComplete").get_to(client.isGameComplete) : client.isGameComplete = false;
j.contains("sceneNum") ? j.at("sceneNum").get_to(client.sceneNum) : client.sceneNum = SCENE_ID_MAX;
j.contains("entranceIndex") ? j.at("entranceIndex").get_to(client.entranceIndex) : client.entranceIndex = 0;
j.contains("self") ? j.at("self").get_to(client.self) : client.self = false;
}
inline void to_json(json& j, const Inventory& inventory) {
j = json{
{"items", inventory.items},
{"ammo", inventory.ammo},
{"equipment", inventory.equipment},
{"upgrades", inventory.upgrades},
{"questItems", inventory.questItems},
{"dungeonItems", inventory.dungeonItems},
{"dungeonKeys", inventory.dungeonKeys},
{"defenseHearts", inventory.defenseHearts},
{"gsTokens", inventory.gsTokens}
};
}
inline void from_json(const json& j, Inventory& inventory) {
j.at("items").get_to(inventory.items);
j.at("ammo").get_to(inventory.ammo);
j.at("equipment").get_to(inventory.equipment);
j.at("upgrades").get_to(inventory.upgrades);
j.at("questItems").get_to(inventory.questItems);
j.at("dungeonItems").get_to(inventory.dungeonItems);
j.at("dungeonKeys").get_to(inventory.dungeonKeys);
j.at("defenseHearts").get_to(inventory.defenseHearts);
j.at("gsTokens").get_to(inventory.gsTokens);
}
inline void to_json(json& j, const SohStats& sohStats) {
j = json{
{"entrancesDiscovered", sohStats.entrancesDiscovered},
{"fileCreatedAt", sohStats.fileCreatedAt},
};
}
inline void from_json(const json& j, SohStats& sohStats) {
j.at("entrancesDiscovered").get_to(sohStats.entrancesDiscovered);
j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt);
}
inline void to_json(json& j, const ShipRandomizerSaveContextData& shipRandomizerSaveContextData) {
j = json{
{"adultTradeItems", shipRandomizerSaveContextData.adultTradeItems},
{"triforcePiecesCollected", shipRandomizerSaveContextData.triforcePiecesCollected},
};
}
inline void from_json(const json& j, ShipRandomizerSaveContextData& shipRandomizerSaveContextData) {
j.at("adultTradeItems").get_to(shipRandomizerSaveContextData.adultTradeItems);
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<u32> sceneFlagsArray;
for (const auto& sceneFlags : saveContext.sceneFlags) {
sceneFlagsArray.push_back(sceneFlags.chest);
sceneFlagsArray.push_back(sceneFlags.swch);
sceneFlagsArray.push_back(sceneFlags.clear);
sceneFlagsArray.push_back(sceneFlags.collect);
}
j = json{
{"healthCapacity", saveContext.healthCapacity},
{"magicLevel", saveContext.magicLevel},
{"magicCapacity", saveContext.magicCapacity},
{"isMagicAcquired", saveContext.isMagicAcquired},
{"isDoubleMagicAcquired", saveContext.isDoubleMagicAcquired},
{"isDoubleDefenseAcquired", saveContext.isDoubleDefenseAcquired},
{"bgsFlag", saveContext.bgsFlag},
{"swordHealth", saveContext.swordHealth},
{"sceneFlags", sceneFlagsArray},
{"eventChkInf", saveContext.eventChkInf},
{"itemGetInf", saveContext.itemGetInf},
{"infTable", saveContext.infTable},
{"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<u32> sceneFlagsArray;
j.at("sceneFlags").get_to(sceneFlagsArray);
for (int i = 0; i < 124; i++) {
saveContext.sceneFlags[i].chest = sceneFlagsArray[i * 4];
saveContext.sceneFlags[i].swch = sceneFlagsArray[i * 4 + 1];
saveContext.sceneFlags[i].clear = sceneFlagsArray[i * 4 + 2];
saveContext.sceneFlags[i].collect = sceneFlagsArray[i * 4 + 3];
}
j.at("eventChkInf").get_to(saveContext.eventChkInf);
j.at("itemGetInf").get_to(saveContext.itemGetInf);
j.at("infTable").get_to(saveContext.infTable);
j.at("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
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,74 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/OTRGlobals.h"
#include "soh/Notification/Notification.h"
/**
* ALL_CLIENT_STATE
*
* Contains a list of all clients and their CLIENT_STATE currently connected to the server
*
* The server itself sends this packet to all clients when a client connects or disconnects
*/
void Anchor::HandlePacket_AllClientState(nlohmann::json payload) {
std::vector<AnchorClient> newClients = payload["state"].get<std::vector<AnchorClient>>();
// add new clients
for (auto& client : newClients) {
if (client.self) {
ownClientId = client.clientId;
CVarSetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), ownClientId);
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
clients[client.clientId].self = true;
} else {
clients[client.clientId].self = false;
if (clients.contains(client.clientId)) {
if (clients[client.clientId].online != client.online) {
Notification::Emit({
.prefix = client.name,
.message = client.online ? "Connected" : "Disconnected",
});
}
} else if (client.online) {
Notification::Emit({
.prefix = client.name,
.message = "Connected",
});
}
}
clients[client.clientId].clientId = client.clientId;
clients[client.clientId].name = client.name;
clients[client.clientId].color = client.color;
clients[client.clientId].clientVersion = client.clientVersion;
clients[client.clientId].teamId = client.teamId;
clients[client.clientId].online = client.online;
clients[client.clientId].seed = client.seed;
clients[client.clientId].isSaveLoaded = client.isSaveLoaded;
clients[client.clientId].isGameComplete = client.isGameComplete;
clients[client.clientId].sceneNum = client.sceneNum;
clients[client.clientId].entranceIndex = client.entranceIndex;
}
// remove clients that are no longer in the list
std::vector<uint32_t> clientsToRemove;
for (auto& [clientId, client] : clients) {
if (std::find_if(newClients.begin(), newClients.end(),
[clientId](AnchorClient& c) { return c.clientId == clientId; }) == newClients.end()) {
clientsToRemove.push_back(clientId);
}
}
// (seperate loop to avoid iterator invalidation)
for (auto& clientId : clientsToRemove) {
clients.erase(clientId);
}
RefreshClientActors();
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,46 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/OTRGlobals.h"
extern "C" {
#include "functions.h"
#include "soh/Enhancements/randomizer/adult_trade_shuffle.h"
extern PlayState* gPlayState;
}
/**
* CONSUME_ADULT_TRADE_ITEM
*
* This is primarily to just get rid of used adult trade items to prevent confusion for other players.
* Whatever flags/items are given from adult trade checks are synced by other packets.
*/
void Anchor::SendPacket_ConsumeAdultTradeItem(u8 itemId) {
if (!IsSaveLoaded()) {
return;
}
nlohmann::json payload;
payload["type"] = CONSUME_ADULT_TRADE_ITEM;
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
payload["addToQueue"] = true;
payload["itemId"] = itemId;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_ConsumeAdultTradeItem(nlohmann::json payload) {
if (!IsSaveLoaded()) {
return;
}
uint8_t itemId = payload["itemId"].get<uint8_t>();
gSaveContext.ship.quest.data.randomizer.adultTradeItems &= ~ADULT_TRADE_FLAG(itemId);
Inventory_ReplaceItem(gPlayState, itemId, Randomizer_GetNextAdultTradeItem());
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,62 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
extern "C" {
#include "macros.h"
#include "functions.h"
extern PlayState* gPlayState;
void func_80838280(Player* player);
}
/**
* DAMAGE_PLAYER
*/
void Anchor::SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage) {
if (!IsSaveLoaded()) {
return;
}
nlohmann::json payload;
payload["type"] = DAMAGE_PLAYER;
payload["targetClientId"] = clientId;
payload["damageEffect"] = damageEffect;
payload["damage"] = damage;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_DamagePlayer(nlohmann::json payload) {
uint32_t clientId = payload["clientId"].get<uint32_t>();
if (!clients.contains(clientId) || clients[clientId].player == nullptr) {
return;
}
AnchorClient& anchorClient = clients[clientId];
Player* otherPlayer = anchorClient.player;
Player* self = GET_PLAYER(gPlayState);
u8 damageEffect = payload["damageEffect"].get<u8>();
u8 damage = payload["damage"].get<u8>();
self->actor.colChkInfo.damage = damage * 8; // Arbitrary number currently, need to fine tune
if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_FIRE) {
for (int i = 0; i < ARRAY_COUNT(self->bodyFlameTimers); i++) {
self->bodyFlameTimers[i] = Rand_S16Offset(0, 200);
}
self->bodyIsBurning = true;
} else if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) {
self->actor.freezeTimer = 20;
Actor_SetColorFilter(&self->actor, 0, 0xFF, 0, 24);
return;
}
func_80837C0C(gPlayState, self, damageEffect, 4.0f, 5.0f, Actor_WorldYawTowardActor(&otherPlayer->actor, &self->actor), 20);
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,18 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
/**
* DISABLE_ANCHOR
*
* No current use, potentially will be used for a future feature.
*/
void Anchor::HandlePacket_DisableAnchor(nlohmann::json payload) {
Disable();
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,41 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Enhancements/randomizer/randomizer_entrance.h"
#include "soh/OTRGlobals.h"
static bool isResultOfHandling = false;
/**
* ENTRANCE_DISCOVERED
*/
void Anchor::SendPacket_EntranceDiscovered(u16 entranceIndex) {
if (!IsSaveLoaded() || isResultOfHandling) {
return;
}
nlohmann::json payload;
payload["type"] = ENTRANCE_DISCOVERED;
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
payload["entranceIndex"] = entranceIndex;
payload["quiet"] = true;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_EntranceDiscovered(nlohmann::json payload) {
if (!IsSaveLoaded()) {
return;
}
isResultOfHandling = true;
u16 entranceIndex = payload["entranceIndex"].get<u16>();
Entrance_SetEntranceDiscovered(entranceIndex, 1);
isResultOfHandling = false;
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,48 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Notification/Notification.h"
#include "soh/Enhancements/randomizer/3drando/random.hpp"
const std::string gameCompleteMessages[] = {
"killed Ganon",
"saved Zelda",
"proved their Courage",
"collected the Triforce",
"is the Hero of Time",
"proved Mido wrong",
};
/**
* GAME_COMPLETE
*/
void Anchor::SendPacket_GameComplete() {
if (!IsSaveLoaded()) {
return;
}
nlohmann::json payload;
payload["type"] = GAME_COMPLETE;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_GameComplete(nlohmann::json payload) {
uint32_t clientId = payload["clientId"].get<uint32_t>();
if (!clients.contains(clientId)) {
return;
}
AnchorClient& anchorClient = clients[clientId];
anchorClient.isGameComplete = true;
Notification::Emit({
.prefix = anchorClient.name,
.message = RandomElement(gameCompleteMessages),
});
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,104 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Notification/Notification.h"
#include "soh/Enhancements/randomizer/randomizer.h"
#include "soh/SohGui/ImGuiUtils.h"
#include "soh/Enhancements/item-tables/ItemTableManager.h"
#include "soh/OTRGlobals.h"
extern "C" {
#include "functions.h"
extern PlayState* gPlayState;
}
/**
* GIVE_ITEM
*/
static bool gettingItem;
static uint8_t incomingIceTraps;
void Anchor::SendPacket_GiveItem(u16 modId, s16 getItemId) {
if (!IsSaveLoaded() || gettingItem) {
return;
}
if (modId == MOD_RANDOMIZER && getItemId == RG_ICE_TRAP && incomingIceTraps > 0) {
incomingIceTraps = MAX(incomingIceTraps - 1, 0);
return;
}
nlohmann::json payload;
payload["type"] = GIVE_ITEM;
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
payload["addToQueue"] = true;
payload["modId"] = modId;
payload["getItemId"] = getItemId;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_GiveItem(nlohmann::json payload) {
if (!IsSaveLoaded()) {
return;
}
uint32_t clientId = payload["clientId"].get<uint32_t>();
AnchorClient& client = clients[clientId];
GetItemEntry getItemEntry;
if (payload["modId"].get<u16>() == MOD_NONE) {
getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, payload["getItemId"].get<s16>());
} else {
getItemEntry = Rando::StaticData::RetrieveItem(payload["getItemId"].get<RandomizerGet>()).GetGIEntry_Copy();
}
gettingItem = true;
if (getItemEntry.modIndex == MOD_NONE) {
if (getItemEntry.getItemId == GI_SWORD_BGS) {
gSaveContext.bgsFlag = true;
}
Item_Give(gPlayState, getItemEntry.itemId);
} else if (getItemEntry.modIndex == MOD_RANDOMIZER) {
if (getItemEntry.getItemId == RG_ICE_TRAP) {
gSaveContext.ship.pendingIceTrapCount++;
incomingIceTraps++;
} else {
Randomizer_Item_Give(gPlayState, getItemEntry);
}
}
// Handle if the player gets a 4th heart piece (usually handled in z_message)
s32 heartPieces = (s32)(gSaveContext.inventory.questItems & 0xF0000000) >> (QUEST_HEART_PIECE + 4);
if (heartPieces >= 4) {
gSaveContext.inventory.questItems &= ~0xF0000000;
gSaveContext.inventory.questItems += (heartPieces % 4) << (QUEST_HEART_PIECE + 4);
gSaveContext.healthCapacity += 0x10 * (heartPieces / 4);
gSaveContext.health += 0x10 * (heartPieces / 4);
gSaveContext.healthAccumulator = 0x140;
}
gettingItem = false;
if (getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK) {
if (getItemEntry.modIndex == MOD_NONE) {
Notification::Emit({
.itemIcon = GetTextureForItemId(getItemEntry.itemId),
.prefix = client.name,
.message = "found",
.suffix = SohUtils::GetItemName(getItemEntry.itemId),
});
} else if (getItemEntry.modIndex == MOD_RANDOMIZER) {
Notification::Emit({
.prefix = client.name,
.message = "found",
.suffix = Rando::StaticData::RetrieveItem((RandomizerGet)getItemEntry.getItemId).GetName().english,
});
}
}
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,26 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/OTRGlobals.h"
/**
* HANDSHAKE
*
* Sent by the client to the server when it first connects to the server, sends over both the local room settings
* in case the room needs to be created, along with the current client state
*/
void Anchor::SendPacket_Handshake() {
nlohmann::json payload;
payload["type"] = HANDSHAKE;
payload["roomId"] = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "");
payload["roomState"] = PrepRoomState();
payload["clientState"] = PrepClientState();
SendJsonToRemote(payload);
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,51 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
extern "C" {
#include "macros.h"
#include "functions.h"
#include "variables.h"
extern PlayState* gPlayState;
}
/**
* PLAYER_SFX
*
* Sound effects, only sent to other clients in the same scene as the player
*/
void Anchor::SendPacket_PlayerSfx(u16 sfxId) {
if (!IsSaveLoaded()) {
return;
}
nlohmann::json payload;
payload["type"] = PLAYER_SFX;
payload["sfxId"] = sfxId;
payload["quiet"] = true;
for (auto& [clientId, client] : clients) {
if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded && !client.self) {
payload["targetClientId"] = clientId;
SendJsonToRemote(payload);
}
}
}
void Anchor::HandlePacket_PlayerSfx(nlohmann::json payload) {
uint32_t clientId = payload["clientId"].get<uint32_t>();
u16 sfxId = payload["sfxId"].get<u16>();
if (!clients.contains(clientId) || !clients[clientId].player) {
return;
}
Player_PlaySfx((Actor*)clients[clientId].player, sfxId);
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,120 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
extern "C" {
#include "macros.h"
#include "variables.h"
extern PlayState* gPlayState;
}
/**
* PLAYER_UPDATE
*
* Contains real-time data necessary to update other clients in the same scene as the player
*
* Sent every frame to other clients within the same scene
*
* Note: This packet is sent _a lot_, so please do not include any unnecessary data in it
*/
void Anchor::SendPacket_PlayerUpdate() {
if (!IsSaveLoaded()) {
return;
}
uint32_t currentPlayerCount = 0;
for (auto& [clientId, client] : clients) {
if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded && !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<int> jointArray;
for (const auto& joint : player->jointTable) {
jointArray.push_back(joint.x);
jointArray.push_back(joint.y);
jointArray.push_back(joint.z);
}
payload["jointTable"] = jointArray;
payload["upperLimbRot"] = player->upperLimbRot;
payload["currentBoots"] = player->currentBoots;
payload["currentShield"] = player->currentShield;
payload["currentTunic"] = player->currentTunic;
payload["stateFlags1"] = player->stateFlags1;
payload["stateFlags2"] = player->stateFlags2;
payload["buttonItem0"] = gSaveContext.equips.buttonItems[0];
payload["itemAction"] = player->itemAction;
payload["heldItemAction"] = player->heldItemAction;
payload["modelGroup"] = player->modelGroup;
payload["invincibilityTimer"] = player->invincibilityTimer;
payload["unk_862"] = player->unk_862;
payload["actionVar1"] = player->av1.actionVar1;
payload["quiet"] = true;
for (auto& [clientId, client] : clients) {
if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded && !client.self) {
payload["targetClientId"] = clientId;
SendJsonToRemote(payload);
}
}
}
void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) {
uint32_t clientId = payload["clientId"].get<uint32_t>();
bool shouldRefreshActors = false;
if (clients.contains(clientId)) {
auto& client = clients[clientId];
if (client.linkAge != payload["linkAge"].get<s32>()) {
shouldRefreshActors = true;
}
client.sceneNum = payload["sceneNum"].get<s16>();
client.entranceIndex = payload["entranceIndex"].get<s32>();
client.linkAge = payload["linkAge"].get<s32>();
client.posRot = payload["posRot"].get<PosRot>();
std::vector<int> jointArray = payload["jointTable"];
for (int i = 0; i < 24; i++) {
client.jointTable[i].x = jointArray[i * 3];
client.jointTable[i].y = jointArray[i * 3 + 1];
client.jointTable[i].z = jointArray[i * 3 + 2];
}
client.upperLimbRot = payload["upperLimbRot"].get<Vec3s>();
client.currentBoots = payload["currentBoots"].get<s8>();
client.currentShield = payload["currentShield"].get<s8>();
client.currentTunic = payload["currentTunic"].get<s8>();
client.stateFlags1 = payload["stateFlags1"].get<u32>();
client.stateFlags2 = payload["stateFlags2"].get<u32>();
client.buttonItem0 = payload["buttonItem0"].get<u8>();
client.itemAction = payload["itemAction"].get<s8>();
client.heldItemAction = payload["heldItemAction"].get<s8>();
client.modelGroup = payload["modelGroup"].get<u8>();
client.invincibilityTimer = payload["invincibilityTimer"].get<s8>();
client.unk_862 = payload["unk_862"].get<s16>();
client.actionVar1 = payload["actionVar1"].get<s8>();
}
if (shouldRefreshActors) {
RefreshClientActors();
}
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,41 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/OTRGlobals.h"
/**
* REQUEST_TEAM_STATE
*
* Requests team state from the server, which will pass on the request to any connected teammates, or send the last known
* state if no teammates are connected.
*
* This fires when loading into a file while Anchor is connected, or when Anchor is connected while a file is already
* loaded
*
* Note: This can additionally be fired with a button in the menus to fix any desyncs that may have occurred in the save
* state
*/
void Anchor::SendPacket_RequestTeamState() {
if (!IsSaveLoaded()) {
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()) {
return;
}
SendPacket_UpdateTeamState();
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,36 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
/**
* REQUEST_TELEPORT
*
* Because we don't have all the necessary information to directly teleport to a player, we emit a request,
* in which they will respond with a TELEPORT_TO packet, with the necessary information.
*/
void Anchor::SendPacket_RequestTeleport(uint32_t clientId) {
if (!IsSaveLoaded()) {
return;
}
nlohmann::json payload;
payload["type"] = REQUEST_TELEPORT;
payload["targetClientId"] = clientId;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_RequestTeleport(nlohmann::json payload) {
if (!IsSaveLoaded()) {
return;
}
uint32_t clientId = payload["clientId"].get<uint32_t>();
SendPacket_TeleportTo(clientId);
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,21 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Notification/Notification.h"
/**
* SERVER_MESSAGE
*/
void Anchor::HandlePacket_ServerMessage(nlohmann::json payload) {
Notification::Emit({
.prefix = "Server:",
.prefixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f),
.message = payload["message"].get<std::string>(),
});
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,59 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/OTRGlobals.h"
static bool isResultOfHandling = false;
/**
* SET_CHECK_STATUS
*
* Fired when a check status is updated or skipped
*/
void Anchor::SendPacket_SetCheckStatus(RandomizerCheck rc) {
if (!IsSaveLoaded() || isResultOfHandling) {
return;
}
auto randoContext = Rando::Context::GetInstance();
nlohmann::json payload;
payload["type"] = SET_CHECK_STATUS;
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
payload["addToQueue"] = true;
payload["rc"] = rc;
payload["status"] = randoContext->GetItemLocation(rc)->GetCheckStatus();
payload["skipped"] = randoContext->GetItemLocation(rc)->GetIsSkipped();
payload["quiet"] = true;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) {
if (!IsSaveLoaded()) {
return;
}
auto randoContext = Rando::Context::GetInstance();
RandomizerCheck rc = payload["rc"].get<RandomizerCheck>();
RandomizerCheckStatus status = payload["status"].get<RandomizerCheckStatus>();
bool skipped = payload["skipped"].get<bool>();
isResultOfHandling = true;
if (randoContext->GetItemLocation(rc)->GetCheckStatus() != status) {
randoContext->GetItemLocation(rc)->SetCheckStatus(status);
}
if (randoContext->GetItemLocation(rc)->GetIsSkipped() != skipped) {
randoContext->GetItemLocation(rc)->SetIsSkipped(skipped);
}
isResultOfHandling = false;
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,54 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/OTRGlobals.h"
/**
* SET_FLAG
*
* Fired when a flag is set in the save context
*/
void Anchor::SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag) {
if (!IsSaveLoaded()) {
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()) {
return;
}
s16 sceneNum = payload["sceneNum"].get<s16>();
s16 flagType = payload["flagType"].get<s16>();
s16 flag = payload["flag"].get<s16>();
if (sceneNum == SCENE_ID_MAX) {
auto effect = new GameInteractionEffect::SetFlag();
effect->parameters[0] = payload["flagType"].get<int16_t>();
effect->parameters[1] = payload["flag"].get<int16_t>();
effect->Apply();
} else {
auto effect = new GameInteractionEffect::SetSceneFlag();
effect->parameters[0] = payload["sceneNum"].get<int16_t>();
effect->parameters[1] = payload["flagType"].get<int16_t>();
effect->parameters[2] = payload["flag"].get<int16_t>();
effect->Apply();
}
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,63 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
extern "C" {
#include "macros.h"
extern PlayState* gPlayState;
}
/**
* TELEPORT_TO
*
* See REQUEST_TELEPORT for more information, this is the second part of the process.
*/
void Anchor::SendPacket_TeleportTo(uint32_t clientId) {
if (!IsSaveLoaded()) {
return;
}
Player* player = GET_PLAYER(gPlayState);
nlohmann::json payload;
payload["type"] = TELEPORT_TO;
payload["targetClientId"] = clientId;
payload["entranceIndex"] = gSaveContext.entranceIndex;
payload["roomIndex"] = gPlayState->roomCtx.curRoom.num;
payload["posRot"] = player->actor.world;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_TeleportTo(nlohmann::json payload) {
if (!IsSaveLoaded()) {
return;
}
s32 entranceIndex = payload["entranceIndex"].get<s32>();
s8 roomIndex = payload["roomIndex"].get<s8>();
PosRot posRot = payload["posRot"].get<PosRot>();
gPlayState->nextEntranceIndex = entranceIndex;
gPlayState->transitionTrigger = TRANS_TRIGGER_START;
gPlayState->transitionType = TRANS_TYPE_INSTANT;
gSaveContext.respawn[RESPAWN_MODE_DOWN].entranceIndex = entranceIndex;
gSaveContext.respawn[RESPAWN_MODE_DOWN].roomIndex = roomIndex;
gSaveContext.respawn[RESPAWN_MODE_DOWN].pos = posRot.pos;
gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = posRot.rot.y;
gSaveContext.respawn[RESPAWN_MODE_DOWN].playerParams = 0xDFF;
gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK_FAST;
gSaveContext.respawnFlag = 1;
static HOOK_ID hookId = 0;
hookId = REGISTER_VB_SHOULD(VB_INFLICT_VOID_DAMAGE, {
*should = false;
GameInteractor::Instance->UnregisterGameHookForID<GameInteractor::OnVanillaBehavior>(hookId);
});
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,55 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/OTRGlobals.h"
/**
* UNSET_FLAG
*
* Fired when a flag is unset in the save context
*/
void Anchor::SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag) {
if (!IsSaveLoaded()) {
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()) {
return;
}
s16 sceneNum = payload["sceneNum"].get<s16>();
s16 flagType = payload["flagType"].get<s16>();
s16 flag = payload["flag"].get<s16>();
if (sceneNum == SCENE_ID_MAX) {
auto effect = new GameInteractionEffect::UnsetFlag();
effect->parameters[0] = payload["flagType"].get<int16_t>();
effect->parameters[1] = payload["flag"].get<int16_t>();
effect->Apply();
} else {
auto effect = new GameInteractionEffect::UnsetSceneFlag();
effect->parameters[0] = payload["sceneNum"].get<int16_t>();
effect->parameters[1] = payload["flagType"].get<int16_t>();
effect->parameters[2] = payload["flag"].get<int16_t>();
effect->Apply();
}
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,43 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/OTRGlobals.h"
extern "C" {
#include "macros.h"
}
/**
* UPDATE_BEANS_COUNT
*
* Keeps the client's bean count in sync as they buy/use them
*/
void Anchor::SendPacket_UpdateBeansCount() {
if (!IsSaveLoaded()) {
return;
}
nlohmann::json payload;
payload["type"] = UPDATE_BEANS_COUNT;
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
payload["addToQueue"] = true;
payload["amount"] = AMMO(ITEM_BEAN);
payload["amountBought"] = BEANS_BOUGHT;
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_UpdateBeansCount(nlohmann::json payload) {
if (!IsSaveLoaded()) {
return;
}
AMMO(ITEM_BEAN) = payload["amount"].get<s8>();
BEANS_BOUGHT = payload["amountBought"].get<s8>();
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,77 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/OTRGlobals.h"
extern "C" {
#include "variables.h"
extern PlayState* gPlayState;
}
/**
* UPDATE_CLIENT_STATE
*
* Contains a small subset of data that is cached on the server and important for the client to know for various reasons
*
* Sent on various events, such as changing scenes, soft resetting, finishing the game, opening file select, etc.
*
* Note: This packet should be cross version compatible, so if you add anything here don't assume all clients will be
* providing it, consider doing a `contains` check before accessing any version specific data
*/
nlohmann::json Anchor::PrepClientState() {
nlohmann::json payload;
payload["name"] = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), "");
payload["color"] = CVarGetColor24(CVAR_REMOTE_ANCHOR("Color"), { 100, 255, 100 });
payload["clientVersion"] = clientVersion;
payload["teamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
payload["online"] = true;
if (IsSaveLoaded()) {
payload["seed"] = IS_RANDO ? Rando::Context::GetInstance()->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<uint32_t>();
if (clients.contains(clientId)) {
AnchorClient client = payload["state"].get<AnchorClient>();
clients[clientId].clientId = clientId;
clients[clientId].name = client.name;
clients[clientId].color = client.color;
clients[clientId].clientVersion = client.clientVersion;
clients[clientId].teamId = client.teamId;
clients[clientId].online = client.online;
clients[clientId].seed = client.seed;
clients[clientId].isSaveLoaded = client.isSaveLoaded;
clients[clientId].isGameComplete = client.isGameComplete;
clients[clientId].sceneNum = client.sceneNum;
clients[clientId].entranceIndex = client.entranceIndex;
}
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,42 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/OTRGlobals.h"
/**
* UPDATE_DUNGEON_ITEMS
*
* This is for 2 things, first is updating the dungeon items in vanilla saves, and second is
* for ensuring the amount of keys used is synced as players are using them.
*/
void Anchor::SendPacket_UpdateDungeonItems() {
if (!IsSaveLoaded()) {
return;
}
nlohmann::json payload;
payload["type"] = UPDATE_DUNGEON_ITEMS;
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
payload["addToQueue"] = true;
payload["mapIndex"] = gSaveContext.mapIndex;
payload["dungeonItems"] = gSaveContext.inventory.dungeonItems[gSaveContext.mapIndex];
payload["dungeonKeys"] = gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex];
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) {
if (!IsSaveLoaded()) {
return;
}
u16 mapIndex = payload["mapIndex"].get<u16>();
gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get<u8>();
gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get<s8>();
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,47 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/OTRGlobals.h"
extern "C" {
#include "variables.h"
extern PlayState* gPlayState;
}
/**
* UPDATE_ROOM_STATE
*/
nlohmann::json Anchor::PrepRoomState() {
nlohmann::json payload;
payload["ownerClientId"] = ownClientId;
payload["pvpMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), 1);
payload["showLocationsMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), 1);
payload["teleportMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), 1);
return payload;
}
void Anchor::SendPacket_UpdateRoomState() {
nlohmann::json payload;
payload["type"] = UPDATE_ROOM_STATE;
payload["state"] = PrepRoomState();
Network::SendJsonToRemote(payload);
}
void Anchor::HandlePacket_UpdateRoomState(nlohmann::json payload) {
if (!payload.contains("state")) {
return;
}
roomState.ownerClientId = payload["state"]["ownerClientId"].get<uint32_t>();
roomState.pvpMode = payload["state"]["pvpMode"].get<u8>();
roomState.showLocationsMode = payload["state"]["showLocationsMode"].get<u8>();
roomState.teleportMode = payload["state"]["teleportMode"].get<u8>();
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -0,0 +1,266 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#include "soh/Enhancements/randomizer/entrance.h"
#include "soh/Enhancements/randomizer/dungeon.h"
#include "soh/OTRGlobals.h"
#include "soh/Notification/Notification.h"
extern "C" {
#include "variables.h"
extern PlayState* gPlayState;
}
/**
* UPDATE_TEAM_STATE
*
* Pushes the current save state to the server for other teammates to use.
*
* Fires when the server passes on a REQUEST_TEAM_STATE packet, or when this client saves the game
*
* When sending this packet we will assume that the team queue has been emptied for this client, so the queue
* stored in the server will be cleared.
*
* When receiving this packet, if there is items in the team queue, we will play them back in order.
*/
void Anchor::SendPacket_UpdateTeamState() {
if (!IsSaveLoaded()) {
return;
}
json payload;
payload["type"] = UPDATE_TEAM_STATE;
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
// Assume the team queue has been emptied, so clear it
payload["queue"] = json::array();
payload["state"] = gSaveContext;
// manually update current scene flags
payload["state"]["sceneFlags"][gPlayState->sceneNum * 4] = gPlayState->actorCtx.flags.chest;
payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 1] = gPlayState->actorCtx.flags.swch;
payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 2] = gPlayState->actorCtx.flags.clear;
payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 3] = gPlayState->actorCtx.flags.collect;
// The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player doesn't have to generate the seed themselves
// Currently it doesn't work :)
if (IS_RANDO) {
auto randoContext = Rando::Context::GetInstance();
payload["state"]["rando"] = json::object();
payload["state"]["rando"]["itemLocations"] = json::array();
for (int i = 0; i < RC_MAX; i++) {
payload["state"]["rando"]["itemLocations"][i] = json::array();
// payload["state"]["rando"]["itemLocations"][i]["rgID"] = randoContext->GetItemLocation(i)->GetPlacedRandomizerGet();
payload["state"]["rando"]["itemLocations"][i][0] = randoContext->GetItemLocation(i)->GetCheckStatus();
payload["state"]["rando"]["itemLocations"][i][1] = (u8)randoContext->GetItemLocation(i)->GetIsSkipped();
// if (randoContext->GetItemLocation(i)->GetPlacedRandomizerGet() == RG_ICE_TRAP) {
// payload["state"]["rando"]["itemLocations"][i]["fakeRgID"] = randoContext->GetItemOverride(i).LooksLike();
// payload["state"]["rando"]["itemLocations"][i]["trickName"] = json::object();
// payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"] = randoContext->GetItemOverride(i).GetTrickName().GetEnglish();
// payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"] = randoContext->GetItemOverride(i).GetTrickName().GetFrench();
// }
// if (randoContext->GetItemLocation(i)->HasCustomPrice()) {
// payload["state"]["rando"]["itemLocations"][i]["price"] = randoContext->GetItemLocation(i)->GetPrice();
// }
}
// auto entranceCtx = randoContext->GetEntranceShuffler();
// for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) {
// payload["state"]["rando"]["entrances"][i] = json::object();
// payload["state"]["rando"]["entrances"][i]["type"] = entranceCtx->entranceOverrides[i].type;
// payload["state"]["rando"]["entrances"][i]["index"] = entranceCtx->entranceOverrides[i].index;
// payload["state"]["rando"]["entrances"][i]["destination"] = entranceCtx->entranceOverrides[i].destination;
// payload["state"]["rando"]["entrances"][i]["override"] = entranceCtx->entranceOverrides[i].override;
// payload["state"]["rando"]["entrances"][i]["overrideDestination"] = entranceCtx->entranceOverrides[i].overrideDestination;
// }
// payload["state"]["rando"]["seed"] = json::array();
// for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) {
// payload["state"]["rando"]["seed"][i] = randoContext->hashIconIndexes[i];
// }
// payload["state"]["rando"]["inputSeed"] = randoContext->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() {
json payload;
payload["type"] = UPDATE_TEAM_STATE;
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
payload["queue"] = json::array();
payload["state"] = json::object();
SendJsonToRemote(payload);
}
void Anchor::HandlePacket_UpdateTeamState(nlohmann::json payload) {
isHandlingUpdateTeamState = true;
// This can happen in between file select and the game starting, so we cant use this check, but we need to ensure we
// be careful to wrap PlayState usage in this check
// if (!IsSaveLoaded()) {
// return;
// }
if (payload.contains("state")) {
SaveContext loadedData = payload["state"].get<SaveContext>();
gSaveContext.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++) {
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
u8 hasMasterSword = CHECK_OWNED_EQUIP(EQUIP_TYPE_SWORD, 1);
if (hasMasterSword) {
loadedData.inventory.equipment |= 0x2;
} else {
loadedData.inventory.equipment &= ~0x2;
}
// Restore bottle contents (unless it's ruto's letter)
for (int i = 0; i < 4; i++) {
if (gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_NONE && gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_LETTER_RUTO) {
loadedData.inventory.items[SLOT_BOTTLE_1 + i] = gSaveContext.inventory.items[SLOT_BOTTLE_1 + i];
}
}
// Restore ammo if it's non-zero, unless it's beans
for (int i = 0; i < ARRAY_COUNT(gSaveContext.inventory.ammo); i++) {
if (gSaveContext.inventory.ammo[i] != 0 && i != SLOT(ITEM_BEAN) && i != SLOT(ITEM_BEAN + 1)) {
loadedData.inventory.ammo[i] = gSaveContext.inventory.ammo[i];
}
}
gSaveContext.inventory = loadedData.inventory;
// The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player doesn't have to generate the seed themselves
// Currently it doesn't work :)
if (IS_RANDO && payload["state"].contains("rando")) {
auto randoContext = Rando::Context::GetInstance();
for (int i = 0; i < RC_MAX; i++) {
// randoContext->GetItemLocation(i)->RefPlacedItem() = payload["state"]["rando"]["itemLocations"][i]["rgID"].get<RandomizerGet>();
OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetCheckStatus(payload["state"]["rando"]["itemLocations"][i][0].get<RandomizerCheckStatus>());
OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetIsSkipped(payload["state"]["rando"]["itemLocations"][i][0].get<u8>());
// if (payload["state"]["rando"]["itemLocations"][i].contains("fakeRgID")) {
// randoContext->overrides.emplace(static_cast<RandomizerCheck>(i), Rando::ItemOverride(static_cast<RandomizerCheck>(i), payload["state"]["rando"]["itemLocations"][i]["fakeRgID"].get<RandomizerGet>()));
// randoContext->GetItemOverride(i).GetTrickName().english = payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"].get<std::string>();
// randoContext->GetItemOverride(i).GetTrickName().french = payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"].get<std::string>();
// }
// if (payload["state"]["rando"]["itemLocations"][i].contains("price")) {
// u16 price = payload["state"]["rando"]["itemLocations"][i]["price"].get<u16>();
// if (price > 0) {
// randoContext->GetItemLocation(i)->SetCustomPrice(price);
// }
// }
}
// auto entranceCtx = randoContext->GetEntranceShuffler();
// for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) {
// entranceCtx->entranceOverrides[i].type = payload["state"]["rando"]["entrances"][i]["type"].get<u16>();
// entranceCtx->entranceOverrides[i].index = payload["state"]["rando"]["entrances"][i]["index"].get<s16>();
// entranceCtx->entranceOverrides[i].destination = payload["state"]["rando"]["entrances"][i]["destination"].get<s16>();
// entranceCtx->entranceOverrides[i].override = payload["state"]["rando"]["entrances"][i]["override"].get<s16>();
// entranceCtx->entranceOverrides[i].overrideDestination = payload["state"]["rando"]["entrances"][i]["overrideDestination"].get<s16>();
// }
// for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) {
// randoContext->hashIconIndexes[i] = payload["state"]["rando"]["seed"][i].get<u8>();
// }
// randoContext->GetSettings()->SetSeedString(payload["state"]["rando"]["inputSeed"].get<std::string>());
// randoContext->GetSettings()->SetSeed(payload["state"]["rando"]["finalSeed"].get<u32>());
// for (int i = 0; i < RSK_MAX; i++) {
// randoContext->GetOption(RandomizerSettingKey(i)).SetSelectedIndex(payload["state"]["rando"]["randoSettings"][i].get<u8>());
// }
// randoContext->GetDungeons()->ClearAllMQ();
// for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) {
// if (payload["state"]["rando"]["masterQuestDungeons"][i].get<bool>()) {
// randoContext->GetDungeon(i)->SetMQ();
// }
// }
// randoContext->GetTrials()->SkipAll();
// for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) {
// if (payload["state"]["rando"]["requiredTrials"][i].get<bool>()) {
// randoContext->GetTrial(i)->SetAsRequired();
// }
// }
}
Notification::Emit({
.message = "Save updated from team",
});
}
if (payload.contains("queue")) {
for (auto& item : payload["queue"]) {
nlohmann::json itemPayload = nlohmann::json::parse(item.get<std::string>());
Anchor::Instance->OnIncomingJson(itemPayload);
}
}
isHandlingUpdateTeamState = false;
}
#endif // ENABLE_REMOTE_CONTROL

View File

@ -20,7 +20,7 @@ void Window::Draw() {
const float margin = 30.0f;
const float padding = 10.0f;
int position = CVarGetInteger(CVAR_SETTING("Notifications.Position"), 0);
int position = CVarGetInteger(CVAR_SETTING("Notifications.Position"), 3);
// Top Left
ImVec2 basePosition;

View File

@ -77,8 +77,10 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/CrowdControl/CrowdControl.h"
#include "soh/Network/Sail/Sail.h"
#include "soh/Network/Anchor/Anchor.h"
CrowdControl* CrowdControl::Instance;
Sail* Sail::Instance;
Anchor* Anchor::Instance;
#endif
#include "Enhancements/mods.h"
@ -1161,6 +1163,7 @@ extern "C" void InitOTR() {
#ifdef ENABLE_REMOTE_CONTROL
CrowdControl::Instance = new CrowdControl();
Sail::Instance = new Sail();
Anchor::Instance = new Anchor();
#endif
OTRMessage_Init();
@ -1196,6 +1199,9 @@ extern "C" void InitOTR() {
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
Sail::Instance->Enable();
}
if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) {
Anchor::Instance->Enable();
}
#endif
}
@ -1213,6 +1219,9 @@ extern "C" void DeinitOTR() {
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
Sail::Instance->Disable();
}
if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) {
Anchor::Instance->Disable();
}
SDLNet_Quit();
#endif

View File

@ -1223,7 +1223,7 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se
delete saveContext;
InitMeta(fileNum);
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum);
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum, sectionID);
SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum);
saveMtx.unlock();
}

View File

@ -36,6 +36,9 @@
#include "soh/Enhancements/debugger/MessageViewer.h"
#include "soh/Notification/Notification.h"
#include "soh/Enhancements/TimeDisplay/TimeDisplay.h"
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/Anchor/Anchor.h"
#endif
bool isBetaQuestEnabled = false;
@ -135,6 +138,9 @@ namespace SohGui {
std::shared_ptr<Notification::Window> mNotificationWindow;
std::shared_ptr<TimeDisplayWindow> mTimeDisplayWindow;
std::shared_ptr<AboutWindow> mAboutWindow;
#ifdef ENABLE_REMOTE_CONTROL
std::shared_ptr<AnchorRoomWindow> mAnchorRoomWindow;
#endif
void SetupGuiElements() {
auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui();
@ -224,6 +230,10 @@ namespace SohGui {
gui->AddGuiWindow(mTimeDisplayWindow);
mAboutWindow = std::make_shared<AboutWindow>(CVAR_WINDOW("AboutWindow"), "About");
gui->AddGuiWindow(mAboutWindow);
#ifdef ENABLE_REMOTE_CONTROL
mAnchorRoomWindow = std::make_shared<AnchorRoomWindow>(CVAR_WINDOW("AnchorRoom"), "Anchor Room");
gui->AddGuiWindow(mAnchorRoomWindow);
#endif
}
void Destroy() {
@ -261,6 +271,9 @@ namespace SohGui {
mPlandomizerWindow = nullptr;
mTimeDisplayWindow = nullptr;
mAboutWindow = nullptr;
#ifdef ENABLE_REMOTE_CONTROL
mAnchorRoomWindow = nullptr;
#endif
}
void RegisterPopup(std::string title, std::string message, std::string button1, std::string button2, std::function<void()> button1callback, std::function<void()> button2callback) {

View File

@ -21,6 +21,7 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "soh/Network/CrowdControl/CrowdControl.h"
#include "soh/Network/Sail/Sail.h"
#include "soh/Network/Anchor/Anchor.h"
#endif
@ -2127,6 +2128,7 @@ void DrawRemoteControlMenu() {
if (ImGui::BeginMenu("Network")) {
Sail::Instance->DrawMenu();
CrowdControl::Instance->DrawMenu();
Anchor::Instance->DrawMenu();
ImGui::EndMenu();
}
}

View File

@ -830,7 +830,7 @@ namespace UIWidgets {
return 0;
}
bool InputString(const char* label, std::string* value) {
return ImGui::InputText(label, (char*)value->c_str(), value->capacity() + 1, ImGuiInputTextFlags_CallbackResize, InputTextResizeCallback, value);
bool InputString(const char* label, std::string* value, ImGuiInputTextFlags flags) {
return ImGui::InputText(label, (char*)value->c_str(), value->capacity() + 1, ImGuiInputTextFlags_CallbackResize | flags, InputTextResizeCallback, value);
}
}

View File

@ -93,7 +93,7 @@ namespace UIWidgets {
void DrawFlagArray16(const std::string& name, uint16_t& flags);
void DrawFlagArray8(const std::string& name, uint8_t& flags);
bool StateButton(const char* str_id, const char* label);
bool InputString(const char* label, std::string* value);
bool InputString(const char* label, std::string* value, ImGuiInputTextFlags flags = 0);
}
#endif /* UIWidgets_hpp */

View File

@ -14,4 +14,5 @@
#define CVAR_GENERAL(var) CVAR_PREFIX_GENERAL "." var
#define CVAR_REMOTE(var) CVAR_PREFIX_REMOTE "." var
#define CVAR_REMOTE_CROWD_CONTROL(var) CVAR_REMOTE(".CrowdControl." var)
#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE(".Sail." var)
#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE(".Sail." var)
#define CVAR_REMOTE_ANCHOR(var) CVAR_REMOTE(".Anchor." var)

View File

@ -117,6 +117,7 @@ std::vector<std::string> sceneNames = {
"Castle Hedge Maze (Early)",
"Sasa Test",
"Treasure Chest Room",
"Unknown",
};
std::vector<std::string> itemNames = {

View File

@ -1231,14 +1231,20 @@ void Actor_Init(Actor* actor, PlayState* play) {
ActorShape_Init(&actor->shape, 0.0f, NULL, 0.0f);
if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex)) {
Actor_SetObjectDependency(play, actor);
actor->init(actor, play);
actor->init = NULL;
GameInteractor_ExecuteOnActorInit(actor);
if (GameInteractor_ShouldActorInit(actor)) {
actor->init(actor, play);
actor->init = NULL;
// For enemy health bar we need to know the max health during init
if (actor->category == ACTORCAT_ENEMY) {
actor->maximumHealth = actor->colChkInfo.health;
GameInteractor_ExecuteOnActorInit(actor);
// For enemy health bar we need to know the max health during init
if (actor->category == ACTORCAT_ENEMY) {
actor->maximumHealth = actor->colChkInfo.health;
}
} else {
actor->init = NULL;
Actor_Kill(actor);
}
}
}
@ -2210,6 +2216,10 @@ void Player_PlaySfx(Actor* actor, u16 sfxId) {
// Audio_PlaySoundGeneral(sfxId, &actor->projectedPos, 4, &D_801333E0 , &D_801333E0, &D_801333E8);
Audio_PlaySoundGeneral(sfxId, &actor->projectedPos, 4, &freqMultiplier, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
}
if (actor->id == ACTOR_PLAYER) {
GameInteractor_ExecuteOnPlayerSfx(sfxId);
}
}
void Audio_PlayActorSound2(Actor* actor, u16 sfxId) {
@ -2589,14 +2599,20 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) {
if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex))
{
Actor_SetObjectDependency(play, actor);
actor->init(actor, play);
actor->init = NULL;
GameInteractor_ExecuteOnActorInit(actor);
if (GameInteractor_ShouldActorInit(actor)) {
actor->init(actor, play);
actor->init = NULL;
// For enemy health bar we need to know the max health during init
if (actor->category == ACTORCAT_ENEMY) {
actor->maximumHealth = actor->colChkInfo.health;
GameInteractor_ExecuteOnActorInit(actor);
// For enemy health bar we need to know the max health during init
if (actor->category == ACTORCAT_ENEMY) {
actor->maximumHealth = actor->colChkInfo.health;
}
} else {
actor->init = NULL;
Actor_Kill(actor);
}
}
actor = actor->next;

View File

@ -767,6 +767,13 @@ void EnItem00_Update(Actor* thisx, PlayState* play) {
EnItem00* this = (EnItem00*)thisx;
s32 pad;
// #region SOH [Co-op]
if (Flags_GetCollectible(play, this->collectibleFlag)) {
Actor_Kill(&this->actor);
return;
}
// #endregion
// Rotate some drops when 3D drops are on, otherwise reset rotation back to 0 for billboard effect
if (
(this->actor.params == ITEM00_HEART && this->unk_15A >= 0) ||

View File

@ -211,7 +211,9 @@ void func_8086ED50(BgBombwall* this, PlayState* play) {
}
void func_8086ED70(BgBombwall* this, PlayState* play) {
if (this->collider.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
// #endregion
this->collider.base.acFlags &= ~AC_HIT;
func_8086EDFC(this, play);
Flags_SetSwitch(play, this->dyna.actor.params & 0x3F);

View File

@ -277,7 +277,9 @@ void BgBreakwall_Wait(BgBreakwall* this, PlayState* play) {
}
}
if (GameInteractor_Should(VB_BG_BREAKWALL_BREAK, this->collider.base.acFlags & 2 || blueFireArrowHit)) {
// #region SOH [Co-op]
if (GameInteractor_Should(VB_BG_BREAKWALL_BREAK, this->collider.base.acFlags & 2 || blueFireArrowHit) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
// #endregion
Vec3f effectPos;
s32 wallType = ((this->dyna.actor.params >> 13) & 3) & 0xFF;

View File

@ -268,7 +268,9 @@ void func_80882E54(BgHakaZou* this, PlayState* play) {
}
void func_80883000(BgHakaZou* this, PlayState* play) {
if (this->collider.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->switchFlag)) {
// #endregion
Flags_SetSwitch(play, this->switchFlag);
if (this->dyna.actor.params == STA_GIANT_BIRD_STATUE) {

View File

@ -126,8 +126,10 @@ void BgHidanDalm_Destroy(Actor* thisx, PlayState* play) {
void BgHidanDalm_Wait(BgHidanDalm* this, PlayState* play) {
Player* player = GET_PLAYER(play);
if ((this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) &&
(player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23)) {
// #region SOH [Co-op]
if (((this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) &&
(player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23)) || Flags_GetSwitch(play, this->switchFlag)) {
// #endregion
this->collider.base.acFlags &= ~AC_HIT;
if ((this->collider.elements[0].info.bumperFlags & BUMP_HIT) ||
(this->collider.elements[1].info.bumperFlags & BUMP_HIT)) {

View File

@ -278,7 +278,9 @@ void func_80888734(BgHidanHamstep* this) {
}
void func_808887C4(BgHidanHamstep* this, PlayState* play) {
if (this->collider.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params >> 8) & 0xFF)) {
// #endregion
OnePointCutscene_Init(play, 3310, 100, &this->dyna.actor, MAIN_CAM);
Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_HAMMER_SWITCH);
this->collider.base.acFlags = AC_NONE;

View File

@ -201,7 +201,9 @@ void func_8088960C(BgHidanHrock* this, PlayState* play) {
}
void func_808896B8(BgHidanHrock* this, PlayState* play) {
if (this->collider.base.acFlags & 2) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & 2) || Flags_GetSwitch(play, this->unk_16A)) {
// #endregion
this->collider.base.acFlags &= ~2;
this->actionFunc = func_808894B0;
this->dyna.actor.flags |= ACTOR_FLAG_UPDATE_CULLING_DISABLED;

View File

@ -303,7 +303,9 @@ void BgHidanKowarerukabe_Update(Actor* thisx, PlayState* play) {
BgHidanKowarerukabe* this = (BgHidanKowarerukabe*)thisx;
s32 pad;
if (Actor_GetCollidedExplosive(play, &this->collider.base) != NULL) {
// #region SOH [Co-op]
if ((Actor_GetCollidedExplosive(play, &this->collider.base) != NULL) || Flags_GetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F)) {
// #endregion
BgHidanKowarerukabe_Break(this, play);
Flags_SetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F);

View File

@ -333,10 +333,12 @@ void func_8089107C(BgIceShelter* this, PlayState* play) {
MeltOnIceArrowHit(this, this->cylinder2, type, play);
}
// Default blue fire check
if (this->cylinder1.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->cylinder1.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
this->cylinder1.base.acFlags &= ~AC_HIT;
if ((this->cylinder1.base.ac != NULL) && (this->cylinder1.base.ac->id == ACTOR_EN_ICE_HONO)) {
if (((this->cylinder1.base.ac != NULL) && (this->cylinder1.base.ac->id == ACTOR_EN_ICE_HONO)) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
// #endregion
if (type == 4) {
if (this->dyna.actor.parent != NULL) {
this->dyna.actor.parent->freezeTimer = 50;

View File

@ -142,7 +142,9 @@ void BgJyaBombchuiwa_SetupWaitForExplosion(BgJyaBombchuiwa* this, PlayState* pla
}
void BgJyaBombchuiwa_WaitForExplosion(BgJyaBombchuiwa* this, PlayState* play) {
if ((this->collider.base.acFlags & AC_HIT) || (this->timer > 0)) {
// #region SOH [Co-op]
if (((this->collider.base.acFlags & AC_HIT) || (this->timer > 0)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) {
// #endregion
if (this->timer == 0) {
OnePointCutscene_Init(play, 3410, -99, &this->actor, MAIN_CAM);
}

View File

@ -163,7 +163,9 @@ void BgJyaBombiwa_Break(BgJyaBombiwa* this, PlayState* play) {
void BgJyaBombiwa_Update(Actor* thisx, PlayState* play) {
BgJyaBombiwa* this = (BgJyaBombiwa*)thisx;
if (this->collider.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
// #endregion
BgJyaBombiwa_Break(this, play);
Flags_SetSwitch(play, this->dyna.actor.params & 0x3F);
SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN);

View File

@ -467,7 +467,9 @@ void BgMizuBwall_SpawnDebris(BgMizuBwall* this, PlayState* play) {
void BgMizuBwall_Idle(BgMizuBwall* this, PlayState* play) {
BgMizuBwall_SetAlpha(this, play);
if (this->collider.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, ((u16)this->dyna.actor.params >> 8) & 0x3F)) {
// #endregion
this->collider.base.acFlags &= ~AC_HIT;
Flags_SetSwitch(play, ((u16)this->dyna.actor.params >> 8) & 0x3F);
this->breakTimer = 1;

View File

@ -183,7 +183,9 @@ void BgSpot08Bakudankabe_Destroy(Actor* thisx, PlayState* play) {
void BgSpot08Bakudankabe_Update(Actor* thisx, PlayState* play) {
BgSpot08Bakudankabe* this = (BgSpot08Bakudankabe*)thisx;
if (this->collider.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) {
// #endregion
func_808B0324(this, play);
Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F));
SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN);

View File

@ -135,7 +135,9 @@ void BgSpot11Bakudankabe_Destroy(Actor* thisx, PlayState* play) {
void BgSpot11Bakudankabe_Update(Actor* thisx, PlayState* play) {
BgSpot11Bakudankabe* this = (BgSpot11Bakudankabe*)thisx;
if (this->collider.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) {
// #endregion
func_808B2218(this, play);
Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F));
SoundSource_PlaySfxAtFixedWorldPos(play, &D_808B2738, 40, NA_SE_EV_WALL_BROKEN);

View File

@ -114,7 +114,9 @@ void BgSpot17Bakudankabe_Destroy(Actor* thisx, PlayState* play) {
void BgSpot17Bakudankabe_Update(Actor* thisx, PlayState* play) {
BgSpot17Bakudankabe* this = (BgSpot17Bakudankabe*)thisx;
if (this->dyna.actor.xzDistToPlayer < 650.0f && func_80033684(play, &this->dyna.actor) != NULL) {
// #region SOH [Co-op]
if ((this->dyna.actor.xzDistToPlayer < 650.0f && func_80033684(play, &this->dyna.actor) != NULL) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) {
// #endregion
func_808B6BC0(this, play);
Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F));
SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN);

View File

@ -146,7 +146,9 @@ void func_808BEFF4(BgYdanMaruta* this, PlayState* play) {
}
void func_808BF078(BgYdanMaruta* this, PlayState* play) {
if (this->collider.base.acFlags & AC_HIT) {
// #region SOH [Co-op]
if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->switchFlag)) {
// #endregion
this->unk_16A = 20;
Flags_SetSwitch(play, this->switchFlag);
Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME);

View File

@ -282,6 +282,12 @@ void BgYdanSp_FloorWebIdle(BgYdanSp* this, PlayState* play) {
webPos.x = this->dyna.actor.world.pos.x;
webPos.y = this->dyna.actor.world.pos.y - 50.0f;
webPos.z = this->dyna.actor.world.pos.z;
// #region SOH [Co-op]
if (Flags_GetSwitch(play, this->isDestroyedSwitchFlag)) {
BgYdanSp_BurnWeb(this, play);
return;
}
// #endregion
if (Player_IsBurningStickInRange(play, &webPos, 70.0f, 50.0f) != 0) {
this->dyna.actor.home.pos.x = player->meleeWeaponInfo[0].tip.x;
this->dyna.actor.home.pos.z = player->meleeWeaponInfo[0].tip.z;
@ -411,6 +417,11 @@ void BgYdanSp_WallWebIdle(BgYdanSp* this, PlayState* play) {
BgYdanSp_BurnWeb(this, play);
}
}
// #region SOH [Co-op]
if (Flags_GetSwitch(play, this->isDestroyedSwitchFlag)) {
BgYdanSp_BurnWeb(this, play);
}
// #endregion
CollisionCheck_SetAC(play, &play->colChkCtx, &this->trisCollider.base);
}

View File

@ -6,6 +6,7 @@
#include "z_door_gerudo.h"
#include "objects/object_door_gerudo/object_door_gerudo.h"
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
#define FLAGS 0
@ -103,6 +104,7 @@ void func_8099485C(DoorGerudo* this, PlayState* play) {
gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex] -= 1;
Flags_SetSwitch(play, this->dyna.actor.params & 0x3F);
Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK);
GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex);
} else {
s32 direction = func_80994750(this, play);

View File

@ -379,6 +379,7 @@ void func_80996B0C(DoorShutter* this, PlayState* play) {
if (this->doorType != SHUTTER_BOSS) {
gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]--;
Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK);
GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex);
} else {
Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK_B);
}
@ -645,6 +646,11 @@ void DoorShutter_Update(Actor* thisx, PlayState* play) {
if (!(player->stateFlags1 & (PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_GETTING_ITEM | PLAYER_STATE1_IN_ITEM_CS)) || (this->actionFunc == DoorShutter_SetupType)) {
this->actionFunc(this, play);
}
// #region SOH [Co-op]
if (Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) {
DECR(this->unk_16E);
}
// #endregion
}
Gfx* func_80997838(PlayState* play, DoorShutter* this, Gfx* p) {

View File

@ -207,6 +207,7 @@ void EnDoor_Idle(EnDoor* this, PlayState* play) {
Flags_SetSwitch(play, this->actor.params & 0x3F);
}
Audio_PlayActorSound2(&this->actor, NA_SE_EV_CHAIN_KEY_UNLOCK);
GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex);
}
} else if (!Player_InCsMode(play)) {
if (fabsf(playerPosRelToDoor.y) < 20.0f && fabsf(playerPosRelToDoor.x) < 20.0f &&
@ -234,6 +235,11 @@ void EnDoor_Idle(EnDoor* this, PlayState* play) {
this->actionFunc = EnDoor_AjarOpen;
}
}
// #region SOH [Co-op]
if (Flags_GetSwitch(play, this->actor.params & 0x3F)) {
DECR(this->lockTimer);
}
// #endregion
}
void EnDoor_WaitForCheck(EnDoor* this, PlayState* play) {

View File

@ -149,6 +149,13 @@ void func_80AFB950(EnSi* this, PlayState* play) {
void EnSi_Update(Actor* thisx, PlayState* play) {
EnSi* this = (EnSi*)thisx;
// #region SOH [Co-op]
if (GET_GS_FLAGS((thisx->params & 0x1F00) >> 8) & (thisx->params & 0xFF)) {
Actor_Kill(&this->actor);
return;
}
// #endregion
Actor_MoveXZGravity(&this->actor);
Actor_UpdateBgCheckInfo(play, &this->actor, 0.0f, 0.0f, 0.0f, 4);
this->actionFunc(this, play);

View File

@ -898,6 +898,13 @@ void func_80B0E9BC(EnSw* this, PlayState* play) {
void EnSw_Update(Actor* thisx, PlayState* play) {
EnSw* this = (EnSw*)thisx;
// #region SOH [Co-op]
if (GET_GS_FLAGS((thisx->params & 0x1F00) >> 8) & (thisx->params & 0xFF)) {
Actor_Kill(&this->actor);
return;
}
// #endregion
SkelAnime_Update(&this->skelAnime);
func_80B0C9F0(this, play);
this->actionFunc(this, play);

View File

@ -55,6 +55,13 @@ void ItemBHeart_Destroy(Actor* thisx, PlayState* play) {
void ItemBHeart_Update(Actor* thisx, PlayState* play) {
ItemBHeart* this = (ItemBHeart*)thisx;
// #region SOH [Co-op]
if (Flags_GetCollectible(play, 0x1F)) {
Actor_Kill(&this->actor);
return;
}
// #endregion
func_80B85264(this, play);
Actor_UpdateBgCheckInfo(play, &this->actor, 0.0f, 0.0f, 0.0f, 4);
if (Actor_HasParent(&this->actor, play)) {

View File

@ -125,8 +125,10 @@ void ObjBombiwa_Update(Actor* thisx, PlayState* play) {
ObjBombiwa* this = (ObjBombiwa*)thisx;
s32 pad;
// #region SOH [Co-op]
if ((func_80033684(play, &this->actor) != NULL) ||
((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040))) {
((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) {
// #endregion
ObjBombiwa_Break(this, play);
Flags_SetSwitch(play, this->actor.params & 0x3F);
SoundSource_PlaySfxAtFixedWorldPos(play, &this->actor.world.pos, 80, NA_SE_EV_WALL_BROKEN);

View File

@ -171,10 +171,12 @@ void ObjHamishi_Update(Actor* thisx, PlayState* play) {
ObjHamishi_Shake(this);
if ((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) {
// #region SOH [Co-op]
if (((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) {
this->collider.base.acFlags &= ~AC_HIT;
this->hitCount++;
if (this->hitCount < 2) {
if (this->hitCount < 2 && !Flags_GetSwitch(play, this->actor.params & 0x3F)) {
// #endregion
this->shakeFrames = 15;
this->shakePosSize = 2.0f;
this->shakeRotSize = 400.0f;