Anchor (#4910)
This is far and away the proudest thing I have been apart of in the Zelda community. Thank you to everyone who either directly or indirectly contributed to this effort. If I forgot to co-author you and you were involved, know that I simply forgot but still appreciated your help! Also thank you to the literal tens of thousands of people who have played Anchor, providing feedback and bug reports. Super thrilled to finally have this merged into the main Ship of Harkinian experience. Co-authored-by: mattman107 <65982675+mattman107@users.noreply.github.com> Co-authored-by: David Chavez <david@dcvz.io> Co-authored-by: MelonSpeedruns <melonspeedruns@outlook.com> Co-authored-by: aMannus <mannusmenting@gmail.com> Co-authored-by: Malkierian <malkierian@gmail.com> Co-authored-by: Caladius <Caladius@users.noreply.github.com> Co-authored-by: Patrick12115 <115201185+Patrick12115@users.noreply.github.com> Co-authored-by: PurpleHato <47987542+PurpleHato@users.noreply.github.com> Co-authored-by: balloondude2 <55861555+balloondude2@users.noreply.github.com> Co-authored-by: lilacLunatic <8488221+lilacLunatic@users.noreply.github.com> Co-authored-by: Felix Lee <flee135@users.noreply.github.com> Co-authored-by: Sirius902 <10891979+Sirius902@users.noreply.github.com>
This commit is contained in:
198
soh/soh/Network/Anchor/Anchor.cpp
Normal file
198
soh/soh/Network/Anchor/Anchor.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "Anchor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <libultraship/libultraship.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.hm64.org"),
|
||||
CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383));
|
||||
ownClientId = CVarGetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), 0);
|
||||
roomState.ownerClientId = 0;
|
||||
}
|
||||
|
||||
void Anchor::Disable() {
|
||||
Network::Disable();
|
||||
|
||||
clients.clear();
|
||||
RefreshClientActors();
|
||||
}
|
||||
|
||||
void Anchor::OnConnected() {
|
||||
SendPacket_Handshake();
|
||||
RegisterHooks();
|
||||
|
||||
if (IsSaveLoaded()) {
|
||||
SendPacket_RequestTeamState();
|
||||
}
|
||||
}
|
||||
|
||||
void Anchor::OnDisconnected() {
|
||||
RegisterHooks();
|
||||
}
|
||||
|
||||
void Anchor::SendJsonToRemote(nlohmann::json payload) {
|
||||
if (!isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
payload["clientId"] = ownClientId;
|
||||
if (!payload.contains("quiet")) {
|
||||
SPDLOG_DEBUG("[Anchor] Sending payload:\n{}", payload.dump());
|
||||
}
|
||||
Network::SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::OnIncomingJson(nlohmann::json payload) {
|
||||
// If it doesn't contain a type, it's not a valid payload
|
||||
if (!payload.contains("type")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's not a quiet payload, log it
|
||||
if (!payload.contains("quiet")) {
|
||||
SPDLOG_DEBUG("[Anchor] Received payload:\n{}", payload.dump());
|
||||
}
|
||||
|
||||
std::string packetType = payload["type"].get<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle PLAYER_UPDATE packets immediately, no need to queue
|
||||
if (packetType == PLAYER_UPDATE) {
|
||||
HandlePacket_PlayerUpdate(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue all packets to be processed on the game thread
|
||||
std::lock_guard<std::mutex> lock(incomingPacketQueueMutex);
|
||||
incomingPacketQueue.push(payload);
|
||||
}
|
||||
|
||||
void Anchor::ProcessIncomingPacketQueue() {
|
||||
std::lock_guard<std::mutex> lock(incomingPacketQueueMutex);
|
||||
|
||||
while (!incomingPacketQueue.empty()) {
|
||||
nlohmann::json payload = incomingPacketQueue.front();
|
||||
incomingPacketQueue.pop();
|
||||
|
||||
std::string packetType = payload["type"].get<std::string>();
|
||||
|
||||
isProcessingIncomingPacket = true;
|
||||
|
||||
// packetType here is a string so we can't use a switch statement
|
||||
if (packetType == ALL_CLIENT_STATE)
|
||||
HandlePacket_AllClientState(payload);
|
||||
else if (packetType == DAMAGE_PLAYER)
|
||||
HandlePacket_DamagePlayer(payload);
|
||||
else if (packetType == DISABLE_ANCHOR)
|
||||
HandlePacket_DisableAnchor(payload);
|
||||
else if (packetType == ENTRANCE_DISCOVERED)
|
||||
HandlePacket_EntranceDiscovered(payload);
|
||||
else if (packetType == GAME_COMPLETE)
|
||||
HandlePacket_GameComplete(payload);
|
||||
else if (packetType == GIVE_ITEM)
|
||||
HandlePacket_GiveItem(payload);
|
||||
else if (packetType == PLAYER_SFX)
|
||||
HandlePacket_PlayerSfx(payload);
|
||||
else if (packetType == UPDATE_TEAM_STATE)
|
||||
HandlePacket_UpdateTeamState(payload);
|
||||
else if (packetType == REQUEST_TEAM_STATE)
|
||||
HandlePacket_RequestTeamState(payload);
|
||||
else if (packetType == REQUEST_TELEPORT)
|
||||
HandlePacket_RequestTeleport(payload);
|
||||
else if (packetType == SERVER_MESSAGE)
|
||||
HandlePacket_ServerMessage(payload);
|
||||
else if (packetType == SET_CHECK_STATUS)
|
||||
HandlePacket_SetCheckStatus(payload);
|
||||
else if (packetType == SET_FLAG)
|
||||
HandlePacket_SetFlag(payload);
|
||||
else if (packetType == TELEPORT_TO)
|
||||
HandlePacket_TeleportTo(payload);
|
||||
else if (packetType == UNSET_FLAG)
|
||||
HandlePacket_UnsetFlag(payload);
|
||||
else if (packetType == UPDATE_BEANS_COUNT)
|
||||
HandlePacket_UpdateBeansCount(payload);
|
||||
else if (packetType == UPDATE_CLIENT_STATE)
|
||||
HandlePacket_UpdateClientState(payload);
|
||||
else if (packetType == UPDATE_ROOM_STATE)
|
||||
HandlePacket_UpdateRoomState(payload);
|
||||
else if (packetType == UPDATE_DUNGEON_ITEMS)
|
||||
HandlePacket_UpdateDungeonItems(payload);
|
||||
|
||||
isProcessingIncomingPacket = false;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Misc/Helpers
|
||||
|
||||
// Kills all existing anchor actors and respawns them with the new client data
|
||||
void Anchor::RefreshClientActors() {
|
||||
if (!IsSaveLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_NPC].head;
|
||||
|
||||
while (actor != NULL) {
|
||||
if (actor->id == ACTOR_EN_OE2 && actor->update == DummyPlayer_Update) {
|
||||
NameTag_RemoveAllForActor(actor);
|
||||
Actor_Kill(actor);
|
||||
}
|
||||
actor = actor->next;
|
||||
}
|
||||
|
||||
actorIndexToClientId.clear();
|
||||
refreshingActors = true;
|
||||
for (auto& [clientId, client] : clients) {
|
||||
if (!client.online || client.self) {
|
||||
continue;
|
||||
}
|
||||
|
||||
actorIndexToClientId.push_back(clientId);
|
||||
// We are using a hook `ShouldActorInit` to override the init/update/draw/destroy functions of the Player we
|
||||
// spawn We quickly store a mapping of "index" to clientId, then within the init function we use this to get the
|
||||
// clientId and store it on player->zTargetActiveTimer (unused s32 for the dummy) for convenience
|
||||
auto dummy = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_PLAYER, client.posRot.pos.x,
|
||||
client.posRot.pos.y, client.posRot.pos.z, client.posRot.rot.x, client.posRot.rot.y,
|
||||
client.posRot.rot.z, actorIndexToClientId.size() - 1, false);
|
||||
client.player = (Player*)dummy;
|
||||
}
|
||||
refreshingActors = false;
|
||||
}
|
||||
|
||||
bool Anchor::IsSaveLoaded() {
|
||||
if (gPlayState == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GET_PLAYER(gPlayState) == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gSaveContext.fileNum < 0 || gSaveContext.fileNum > 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gSaveContext.gameMode != GAMEMODE_NORMAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
181
soh/soh/Network/Anchor/Anchor.h
Normal file
181
soh/soh/Network/Anchor/Anchor.h
Normal file
@@ -0,0 +1,181 @@
|
||||
#ifndef NETWORK_ANCHOR_H
|
||||
#define NETWORK_ANCHOR_H
|
||||
#ifdef __cplusplus
|
||||
|
||||
#include "soh/Network/Network.h"
|
||||
#include <libultraship/libultraship.h>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
|
||||
extern "C" {
|
||||
#include "variables.h"
|
||||
#include "z64.h"
|
||||
}
|
||||
|
||||
void DummyPlayer_Init(Actor* actor, PlayState* play);
|
||||
void DummyPlayer_Update(Actor* actor, PlayState* play);
|
||||
void DummyPlayer_Draw(Actor* actor, PlayState* play);
|
||||
void DummyPlayer_Destroy(Actor* actor, PlayState* play);
|
||||
|
||||
typedef struct {
|
||||
uint32_t clientId;
|
||||
std::string name;
|
||||
Color_RGB8 color;
|
||||
std::string clientVersion;
|
||||
std::string teamId;
|
||||
bool online;
|
||||
bool self;
|
||||
uint32_t seed;
|
||||
bool isSaveLoaded;
|
||||
bool isGameComplete;
|
||||
s16 sceneNum;
|
||||
s32 entranceIndex;
|
||||
|
||||
// Only available in PLAYER_UPDATE packets
|
||||
s32 linkAge;
|
||||
PosRot posRot;
|
||||
Vec3s jointTable[24];
|
||||
Vec3s upperLimbRot;
|
||||
s8 currentBoots;
|
||||
s8 currentShield;
|
||||
s8 currentTunic;
|
||||
u32 stateFlags1;
|
||||
u32 stateFlags2;
|
||||
u8 buttonItem0;
|
||||
s8 itemAction;
|
||||
s8 heldItemAction;
|
||||
u8 modelGroup;
|
||||
s8 invincibilityTimer;
|
||||
s16 unk_862;
|
||||
s8 actionVar1;
|
||||
|
||||
// Ptr to the dummy player
|
||||
Player* player;
|
||||
} AnchorClient;
|
||||
|
||||
typedef struct {
|
||||
uint32_t ownerClientId;
|
||||
u8 pvpMode; // 0 = off, 1 = on, 2 = on with friendly fire
|
||||
u8 showLocationsMode; // 0 = none, 1 = team, 2 = all
|
||||
u8 teleportMode; // 0 = off, 1 = team, 2 = all
|
||||
u8 syncItemsAndFlags; // 0 = off, 1 = on
|
||||
} RoomState;
|
||||
|
||||
class Anchor : public Network {
|
||||
private:
|
||||
bool refreshingActors = false;
|
||||
bool justLoadedSave = false;
|
||||
bool isHandlingUpdateTeamState = false;
|
||||
bool isProcessingIncomingPacket = false;
|
||||
std::queue<nlohmann::json> incomingPacketQueue;
|
||||
std::mutex incomingPacketQueueMutex;
|
||||
|
||||
nlohmann::json PrepClientState();
|
||||
nlohmann::json PrepRoomState();
|
||||
void RegisterHooks();
|
||||
void RefreshClientActors();
|
||||
void HandlePacket_AllClientState(nlohmann::json payload);
|
||||
void HandlePacket_ConsumeAdultTradeItem(nlohmann::json payload);
|
||||
void HandlePacket_DamagePlayer(nlohmann::json payload);
|
||||
void HandlePacket_DisableAnchor(nlohmann::json payload);
|
||||
void HandlePacket_EntranceDiscovered(nlohmann::json payload);
|
||||
void HandlePacket_GameComplete(nlohmann::json payload);
|
||||
void HandlePacket_GiveItem(nlohmann::json payload);
|
||||
void HandlePacket_PlayerSfx(nlohmann::json payload);
|
||||
void HandlePacket_PlayerUpdate(nlohmann::json payload);
|
||||
void HandlePacket_RequestTeamState(nlohmann::json payload);
|
||||
void HandlePacket_RequestTeleport(nlohmann::json payload);
|
||||
void HandlePacket_ServerMessage(nlohmann::json payload);
|
||||
void HandlePacket_SetCheckStatus(nlohmann::json payload);
|
||||
void HandlePacket_SetFlag(nlohmann::json payload);
|
||||
void HandlePacket_TeleportTo(nlohmann::json payload);
|
||||
void HandlePacket_UnsetFlag(nlohmann::json payload);
|
||||
void HandlePacket_UpdateBeansCount(nlohmann::json payload);
|
||||
void HandlePacket_UpdateClientState(nlohmann::json payload);
|
||||
void HandlePacket_UpdateDungeonItems(nlohmann::json payload);
|
||||
void HandlePacket_UpdateRoomState(nlohmann::json payload);
|
||||
void HandlePacket_UpdateTeamState(nlohmann::json payload);
|
||||
|
||||
public:
|
||||
uint32_t ownClientId;
|
||||
inline static const std::string clientVersion = (char*)gBuildVersion;
|
||||
|
||||
// Packet types //
|
||||
inline static const std::string ALL_CLIENT_STATE = "ALL_CLIENT_STATE";
|
||||
inline static const std::string DAMAGE_PLAYER = "DAMAGE_PLAYER";
|
||||
inline static const std::string DISABLE_ANCHOR = "DISABLE_ANCHOR";
|
||||
inline static const std::string ENTRANCE_DISCOVERED = "ENTRANCE_DISCOVERED";
|
||||
inline static const std::string GAME_COMPLETE = "GAME_COMPLETE";
|
||||
inline static const std::string GIVE_ITEM = "GIVE_ITEM";
|
||||
inline static const std::string HANDSHAKE = "HANDSHAKE";
|
||||
inline static const std::string PLAYER_SFX = "PLAYER_SFX";
|
||||
inline static const std::string PLAYER_UPDATE = "PLAYER_UPDATE";
|
||||
inline static const std::string REQUEST_TEAM_STATE = "REQUEST_TEAM_STATE";
|
||||
inline static const std::string REQUEST_TELEPORT = "REQUEST_TELEPORT";
|
||||
inline static const std::string SERVER_MESSAGE = "SERVER_MESSAGE";
|
||||
inline static const std::string SET_CHECK_STATUS = "SET_CHECK_STATUS";
|
||||
inline static const std::string SET_FLAG = "SET_FLAG";
|
||||
inline static const std::string TELEPORT_TO = "TELEPORT_TO";
|
||||
inline static const std::string UNSET_FLAG = "UNSET_FLAG";
|
||||
inline static const std::string UPDATE_BEANS_COUNT = "UPDATE_BEANS_COUNT";
|
||||
inline static const std::string UPDATE_CLIENT_STATE = "UPDATE_CLIENT_STATE";
|
||||
inline static const std::string UPDATE_DUNGEON_ITEMS = "UPDATE_DUNGEON_ITEMS";
|
||||
inline static const std::string UPDATE_ROOM_STATE = "UPDATE_ROOM_STATE";
|
||||
inline static const std::string UPDATE_TEAM_STATE = "UPDATE_TEAM_STATE";
|
||||
|
||||
static Anchor* Instance;
|
||||
std::map<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 ProcessIncomingPacketQueue();
|
||||
void SendJsonToRemote(nlohmann::json packet);
|
||||
bool IsSaveLoaded();
|
||||
bool CanTeleportTo(uint32_t clientId);
|
||||
|
||||
void SendPacket_ClearTeamState(std::string teamId);
|
||||
void SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage);
|
||||
void SendPacket_EntranceDiscovered(u16 entranceIndex);
|
||||
void SendPacket_GameComplete();
|
||||
void SendPacket_GiveItem(u16 modId, s16 getItemId);
|
||||
void SendPacket_Handshake();
|
||||
void SendPacket_PlayerSfx(u16 sfxId);
|
||||
void SendPacket_PlayerUpdate();
|
||||
void SendPacket_RequestTeamState();
|
||||
void SendPacket_RequestTeleport(u32 clientId);
|
||||
void SendPacket_SetCheckStatus(RandomizerCheck rc);
|
||||
void SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag);
|
||||
void SendPacket_TeleportTo(u32 clientId);
|
||||
void SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag);
|
||||
void SendPacket_UpdateBeansCount();
|
||||
void SendPacket_UpdateClientState();
|
||||
void SendPacket_UpdateDungeonItems();
|
||||
void SendPacket_UpdateRoomState();
|
||||
void SendPacket_UpdateTeamState();
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
// Starting at 5 to continue from the last value in the PlayerDamageResponseType enum
|
||||
DUMMY_PLAYER_HIT_RESPONSE_STUN = 5,
|
||||
DUMMY_PLAYER_HIT_RESPONSE_FIRE,
|
||||
DUMMY_PLAYER_HIT_RESPONSE_NORMAL,
|
||||
} DummyPlayerDamageResponseType;
|
||||
|
||||
class AnchorRoomWindow : public Ship::GuiWindow {
|
||||
public:
|
||||
using GuiWindow::GuiWindow;
|
||||
|
||||
void InitElement() override{};
|
||||
void DrawElement() override;
|
||||
void Draw() override;
|
||||
void UpdateElement() override{};
|
||||
};
|
||||
|
||||
#endif // __cplusplus
|
||||
#endif // NETWORK_ANCHOR_H
|
||||
129
soh/soh/Network/Anchor/AnchorRoomWindow.cpp
Normal file
129
soh/soh/Network/Anchor/AnchorRoomWindow.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "Anchor.h"
|
||||
#include "soh/OTRGlobals.h"
|
||||
|
||||
extern "C" {
|
||||
#include "variables.h"
|
||||
#include "functions.h"
|
||||
extern PlayState* gPlayState;
|
||||
}
|
||||
|
||||
void AnchorRoomWindow::Draw() {
|
||||
if (!IsVisible() || !Anchor::Instance->isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg,
|
||||
ImVec4(0, 0, 0, CVarGetFloat(CVAR_SETTING("Notifications.BgOpacity"), 0.5f)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||
|
||||
auto vp = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowViewport(vp->ID);
|
||||
|
||||
ImGui::Begin("Anchor Room", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar);
|
||||
|
||||
DrawElement();
|
||||
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
void AnchorRoomWindow::DrawElement() {
|
||||
bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""));
|
||||
|
||||
if (isGlobalRoom) {
|
||||
u32 activeClients = 0;
|
||||
for (auto& [clientId, client] : Anchor::Instance->clients) {
|
||||
if (client.online) {
|
||||
activeClients++;
|
||||
}
|
||||
}
|
||||
ImGui::Text("Players Online: %d", activeClients);
|
||||
return;
|
||||
}
|
||||
|
||||
// First build a list of teams
|
||||
std::set<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->CanTeleportTo(client.clientId)) {
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
|
||||
if (ImGui::Button(ICON_FA_LOCATION_ARROW, ImVec2(20.0f, 20.0f))) {
|
||||
Anchor::Instance->SendPacket_RequestTeleport(client.clientId);
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
if (client.clientVersion != Anchor::clientVersion) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Incompatible version! Will not work together!");
|
||||
ImGui::Text("Yours: %s", Anchor::clientVersion.c_str());
|
||||
ImGui::Text("Theirs: %s", client.clientVersion.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
uint32_t seed = IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : 0;
|
||||
if (client.isSaveLoaded && Anchor::Instance->IsSaveLoaded() && client.seed != seed && client.online &&
|
||||
!client.self) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Seed mismatch! Continuing will break things!");
|
||||
ImGui::Text("Yours: %u", seed);
|
||||
ImGui::Text("Theirs: %u", client.seed);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
221
soh/soh/Network/Anchor/DummyPlayer.cpp
Normal file
221
soh/soh/Network/Anchor/DummyPlayer.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
#include "Anchor.h"
|
||||
#include "soh/Enhancements/nametag.h"
|
||||
#include "soh/frame_interpolation.h"
|
||||
|
||||
extern "C" {
|
||||
#include "macros.h"
|
||||
#include "variables.h"
|
||||
#include "functions.h"
|
||||
extern PlayState* gPlayState;
|
||||
|
||||
void Player_UseItem(PlayState* play, Player* player, s32 item);
|
||||
void Player_Draw(Actor* actor, PlayState* play);
|
||||
}
|
||||
|
||||
// Hijacking player->zTargetActiveTimer (unused s32 for the dummy) to store the clientId for convenience
|
||||
#define DUMMY_CLIENT_ID player->zTargetActiveTimer
|
||||
|
||||
static DamageTable DummyPlayerDamageTable = {
|
||||
/* Deku nut */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
|
||||
/* Deku stick */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Slingshot */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Explosive */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Boomerang */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
|
||||
/* Normal arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Hammer swing */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE),
|
||||
/* Hookshot */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN),
|
||||
/* Kokiri sword */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Master sword */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Giant's Knife */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Fire arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_FIRE),
|
||||
/* Ice arrow */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_ICE_TRAP),
|
||||
/* Light arrow */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK),
|
||||
/* Unk arrow 1 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
|
||||
/* Unk arrow 2 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
|
||||
/* Unk arrow 3 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE),
|
||||
/* Fire magic */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_FIRE),
|
||||
/* Ice magic */ DMG_ENTRY(3, PLAYER_HIT_RESPONSE_ICE_TRAP),
|
||||
/* Light magic */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK),
|
||||
/* Shield */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
|
||||
/* Mirror Ray */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
|
||||
/* Kokiri spin */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Giant spin */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Master spin */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Kokiri jump */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Giant jump */ DMG_ENTRY(8, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Master jump */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL),
|
||||
/* Unknown 1 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
|
||||
/* Unblockable */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
|
||||
/* Hammer jump */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE),
|
||||
/* Unknown 2 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE),
|
||||
};
|
||||
|
||||
void DummyPlayer_Init(Actor* actor, PlayState* play) {
|
||||
Player* player = (Player*)actor;
|
||||
|
||||
uint32_t clientId = Anchor::Instance->actorIndexToClientId[actor->params];
|
||||
DUMMY_CLIENT_ID = clientId;
|
||||
|
||||
if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
|
||||
Actor_Kill(actor);
|
||||
return;
|
||||
}
|
||||
|
||||
AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
|
||||
|
||||
// Hack to account for usage of gSaveContext in Player_Init
|
||||
s32 originalAge = gSaveContext.linkAge;
|
||||
gSaveContext.linkAge = client.linkAge;
|
||||
|
||||
// #region modeled after EnTorch2_Init and Player_Init
|
||||
actor->room = -1;
|
||||
player->itemAction = player->heldItemAction = -1;
|
||||
player->heldItemId = ITEM_NONE;
|
||||
Player_UseItem(play, player, ITEM_NONE);
|
||||
Player_SetModelGroup(player, Player_ActionToModelGroup(player, player->heldItemAction));
|
||||
play->playerInit(player, play, gPlayerSkelHeaders[client.linkAge]);
|
||||
|
||||
play->func_11D54(player, play);
|
||||
// #endregion
|
||||
|
||||
player->cylinder.base.acFlags = AC_ON | AC_TYPE_PLAYER;
|
||||
player->cylinder.base.ocFlags2 = OC2_TYPE_1;
|
||||
player->cylinder.info.bumperFlags = BUMP_ON | BUMP_HOOKABLE | BUMP_NO_HITMARK;
|
||||
player->actor.flags |= ACTOR_FLAG_HOOKSHOT_PULLS_PLAYER;
|
||||
player->cylinder.dim.radius = 30;
|
||||
player->actor.colChkInfo.damageTable = &DummyPlayerDamageTable;
|
||||
|
||||
gSaveContext.linkAge = originalAge;
|
||||
|
||||
bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""));
|
||||
|
||||
if (!isGlobalRoom) {
|
||||
NameTag_RegisterForActorWithOptions(actor, client.name.c_str(), {});
|
||||
}
|
||||
}
|
||||
|
||||
void Math_Vec3s_Copy(Vec3s* dest, Vec3s* src) {
|
||||
dest->x = src->x;
|
||||
dest->y = src->y;
|
||||
dest->z = src->z;
|
||||
}
|
||||
|
||||
// Update the actor with new data from the client
|
||||
void DummyPlayer_Update(Actor* actor, PlayState* play) {
|
||||
Player* player = (Player*)actor;
|
||||
|
||||
if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
|
||||
Actor_Kill(actor);
|
||||
return;
|
||||
}
|
||||
|
||||
AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
|
||||
|
||||
if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) {
|
||||
actor->world.pos.x = -9999.0f;
|
||||
actor->world.pos.y = -9999.0f;
|
||||
actor->world.pos.z = -9999.0f;
|
||||
actor->shape.shadowAlpha = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
actor->shape.shadowAlpha = 255;
|
||||
Math_Vec3s_Copy(&player->upperLimbRot, &client.upperLimbRot);
|
||||
Math_Vec3s_Copy(&actor->shape.rot, &client.posRot.rot);
|
||||
Math_Vec3f_Copy(&actor->world.pos, &client.posRot.pos);
|
||||
player->skelAnime.jointTable = client.jointTable;
|
||||
player->currentBoots = client.currentBoots;
|
||||
player->currentShield = client.currentShield;
|
||||
player->currentTunic = client.currentTunic;
|
||||
player->stateFlags1 = client.stateFlags1;
|
||||
player->stateFlags2 = client.stateFlags2;
|
||||
player->itemAction = client.itemAction;
|
||||
player->heldItemAction = client.heldItemAction;
|
||||
player->invincibilityTimer = client.invincibilityTimer;
|
||||
player->unk_862 = client.unk_862;
|
||||
player->av1.actionVar1 = client.actionVar1;
|
||||
|
||||
if (player->modelGroup != client.modelGroup) {
|
||||
// Hack to account for usage of gSaveContext
|
||||
s32 originalAge = gSaveContext.linkAge;
|
||||
gSaveContext.linkAge = client.linkAge;
|
||||
u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0];
|
||||
gSaveContext.equips.buttonItems[0] = client.buttonItem0;
|
||||
Player_SetModelGroup(player, client.modelGroup);
|
||||
gSaveContext.linkAge = originalAge;
|
||||
gSaveContext.equips.buttonItems[0] = originalButtonItem0;
|
||||
}
|
||||
|
||||
if (Anchor::Instance->roomState.pvpMode == 0 ||
|
||||
(Anchor::Instance->roomState.pvpMode == 1 &&
|
||||
client.teamId == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"))) {
|
||||
actor->flags |= ACTOR_FLAG_LOCK_ON_DISABLED;
|
||||
return;
|
||||
}
|
||||
|
||||
actor->flags &= ~ACTOR_FLAG_LOCK_ON_DISABLED;
|
||||
|
||||
if (player->cylinder.base.acFlags & AC_HIT && player->invincibilityTimer == 0) {
|
||||
Anchor::Instance->SendPacket_DamagePlayer(client.clientId, player->actor.colChkInfo.damageEffect,
|
||||
player->actor.colChkInfo.damage);
|
||||
if (player->actor.colChkInfo.damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) {
|
||||
Actor_SetColorFilter(&player->actor, 0, 0xFF, 0, 24);
|
||||
} else {
|
||||
player->invincibilityTimer = 20;
|
||||
}
|
||||
}
|
||||
|
||||
Collider_UpdateCylinder(&player->actor, &player->cylinder);
|
||||
|
||||
if (!(player->stateFlags2 & PLAYER_STATE2_FROZEN)) {
|
||||
if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_HANGING_OFF_LEDGE |
|
||||
PLAYER_STATE1_CLIMBING_LEDGE | PLAYER_STATE1_ON_HORSE))) {
|
||||
CollisionCheck_SetOC(play, &play->colChkCtx, &player->cylinder.base);
|
||||
}
|
||||
|
||||
if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_DAMAGED)) &&
|
||||
(player->invincibilityTimer <= 0)) {
|
||||
CollisionCheck_SetAC(play, &play->colChkCtx, &player->cylinder.base);
|
||||
|
||||
if (player->invincibilityTimer < 0) {
|
||||
CollisionCheck_SetAT(play, &play->colChkCtx, &player->cylinder.base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_IN_ITEM_CS | PLAYER_STATE1_IN_CUTSCENE)) {
|
||||
player->actor.colChkInfo.mass = MASS_IMMOVABLE;
|
||||
} else {
|
||||
player->actor.colChkInfo.mass = 50;
|
||||
}
|
||||
|
||||
Collider_ResetCylinderAC(play, &player->cylinder.base);
|
||||
}
|
||||
|
||||
void DummyPlayer_Draw(Actor* actor, PlayState* play) {
|
||||
Player* player = (Player*)actor;
|
||||
|
||||
if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) {
|
||||
Actor_Kill(actor);
|
||||
return;
|
||||
}
|
||||
|
||||
AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID];
|
||||
|
||||
if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hack to account for usage of gSaveContext in Player_Draw
|
||||
s32 originalAge = gSaveContext.linkAge;
|
||||
gSaveContext.linkAge = client.linkAge;
|
||||
u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0];
|
||||
gSaveContext.equips.buttonItems[0] = client.buttonItem0;
|
||||
|
||||
Player_Draw((Actor*)player, play);
|
||||
gSaveContext.linkAge = originalAge;
|
||||
gSaveContext.equips.buttonItems[0] = originalButtonItem0;
|
||||
}
|
||||
|
||||
void DummyPlayer_Destroy(Actor* actor, PlayState* play) {
|
||||
}
|
||||
361
soh/soh/Network/Anchor/HookHandlers.cpp
Normal file
361
soh/soh/Network/Anchor/HookHandlers.cpp
Normal file
@@ -0,0 +1,361 @@
|
||||
#include "Anchor.h"
|
||||
#include <libultraship/libultraship.h>
|
||||
#include "soh/Enhancements/game-interactor/GameInteractor.h"
|
||||
|
||||
extern "C" {
|
||||
#include "variables.h"
|
||||
#include "functions.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.h"
|
||||
#include "src/overlays/actors/ovl_Door_Shutter/z_door_shutter.h"
|
||||
#include "src/overlays/actors/ovl_En_Door/z_en_door.h"
|
||||
#include "src/overlays/actors/ovl_En_Si/z_en_si.h"
|
||||
#include "src/overlays/actors/ovl_En_Sw/z_en_sw.h"
|
||||
#include "src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.h"
|
||||
#include "src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.h"
|
||||
#include "src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.h"
|
||||
#include "src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.h"
|
||||
|
||||
extern PlayState* gPlayState;
|
||||
|
||||
void func_8086ED70(BgBombwall* bgBombwall, PlayState* play);
|
||||
void BgBreakwall_Wait(BgBreakwall* bgBreakwall, PlayState* play);
|
||||
void func_80883000(BgHakaZou* bgHakaZou, PlayState* play);
|
||||
void func_808887C4(BgHidanHamstep* bgHidanHamstep, PlayState* play);
|
||||
void func_808896B8(BgHidanHrock* bgHidanHrock, PlayState* play);
|
||||
void func_8089107C(BgIceShelter* bgIceShelter, PlayState* play);
|
||||
void func_808911BC(BgIceShelter* bgIceShelter);
|
||||
void ObjBombiwa_Break(ObjBombiwa* objBombiwa, PlayState* play);
|
||||
void ObjHamishi_Break(ObjHamishi* objHamishi, PlayState* play);
|
||||
void BgJyaBombchuiwa_WaitForExplosion(BgJyaBombchuiwa* bgJyaBombchuiwa, PlayState* play);
|
||||
void BgMizuBwall_Idle(BgMizuBwall* bgMizuBwall, PlayState* play);
|
||||
void func_808B6BC0(BgSpot17Bakudankabe* bgSpot17Bakudankabe, PlayState* play);
|
||||
void func_808BF078(BgYdanMaruta* bgYdanMaruta, PlayState* play);
|
||||
void BgYdanSp_FloorWebIdle(BgYdanSp* bgYdanSp, PlayState* play);
|
||||
void BgYdanSp_WallWebIdle(BgYdanSp* bgYdanSp, PlayState* play);
|
||||
void BgYdanSp_BurnWeb(BgYdanSp* bgYdanSp, PlayState* play);
|
||||
void EnDoor_Idle(EnDoor* enDoor, PlayState* play);
|
||||
}
|
||||
|
||||
void Anchor::RegisterHooks() {
|
||||
|
||||
// #region Hooks that are required for basic Anchor functionality
|
||||
|
||||
COND_HOOK(OnSceneSpawnActors, isConnected, [&]() {
|
||||
SendPacket_UpdateClientState();
|
||||
|
||||
if (IsSaveLoaded()) {
|
||||
RefreshClientActors();
|
||||
}
|
||||
});
|
||||
|
||||
COND_HOOK(OnPresentFileSelect, isConnected, [&]() { SendPacket_UpdateClientState(); });
|
||||
|
||||
COND_ID_HOOK(ShouldActorInit, ACTOR_PLAYER, isConnected, [&](void* actorRef, bool* should) {
|
||||
Actor* actor = (Actor*)actorRef;
|
||||
|
||||
if (refreshingActors) {
|
||||
// By the time we get here, the actor was already added to the ACTORCAT_PLAYER list, so we need to move it
|
||||
Actor_ChangeCategory(gPlayState, &gPlayState->actorCtx, actor, ACTORCAT_NPC);
|
||||
actor->id = ACTOR_EN_OE2;
|
||||
actor->category = ACTORCAT_NPC;
|
||||
actor->init = DummyPlayer_Init;
|
||||
actor->update = DummyPlayer_Update;
|
||||
actor->draw = DummyPlayer_Draw;
|
||||
actor->destroy = DummyPlayer_Destroy;
|
||||
}
|
||||
});
|
||||
|
||||
COND_HOOK(OnPlayerUpdate, isConnected, [&]() {
|
||||
if (justLoadedSave) {
|
||||
justLoadedSave = false;
|
||||
SendPacket_RequestTeamState();
|
||||
}
|
||||
SendPacket_PlayerUpdate();
|
||||
});
|
||||
|
||||
COND_HOOK(OnGameFrameUpdate, isConnected, [&]() { ProcessIncomingPacketQueue(); });
|
||||
|
||||
COND_HOOK(OnPlayerSfx, isConnected, [&](u16 sfxId) { SendPacket_PlayerSfx(sfxId); });
|
||||
|
||||
COND_HOOK(OnLoadGame, isConnected, [&](s16 fileNum) { justLoadedSave = true; });
|
||||
|
||||
COND_HOOK(OnSaveFile, isConnected, [&](s16 fileNum, int sectionID) {
|
||||
if (sectionID == 0) {
|
||||
SendPacket_UpdateTeamState();
|
||||
}
|
||||
});
|
||||
|
||||
COND_HOOK(OnFlagSet, isConnected,
|
||||
[&](s16 flagType, s16 flag) { SendPacket_SetFlag(SCENE_ID_MAX, flagType, flag); });
|
||||
|
||||
COND_HOOK(OnFlagUnset, isConnected,
|
||||
[&](s16 flagType, s16 flag) { SendPacket_UnsetFlag(SCENE_ID_MAX, flagType, flag); });
|
||||
|
||||
COND_HOOK(OnSceneFlagSet, isConnected,
|
||||
[&](s16 sceneNum, s16 flagType, s16 flag) { SendPacket_SetFlag(sceneNum, flagType, flag); });
|
||||
|
||||
COND_HOOK(OnSceneFlagUnset, isConnected,
|
||||
[&](s16 sceneNum, s16 flagType, s16 flag) { SendPacket_UnsetFlag(sceneNum, flagType, flag); });
|
||||
|
||||
COND_HOOK(OnRandoSetCheckStatus, isConnected, [&](RandomizerCheck rc, RandomizerCheckStatus status) {
|
||||
if (!isHandlingUpdateTeamState) {
|
||||
SendPacket_SetCheckStatus(rc);
|
||||
}
|
||||
});
|
||||
|
||||
COND_HOOK(OnRandoSetIsSkipped, isConnected, [&](RandomizerCheck rc, bool isSkipped) {
|
||||
if (!isHandlingUpdateTeamState) {
|
||||
SendPacket_SetCheckStatus(rc);
|
||||
}
|
||||
});
|
||||
|
||||
COND_HOOK(OnRandoEntranceDiscovered, isConnected,
|
||||
[&](u16 entranceIndex, u8 isReversedEntrance) { SendPacket_EntranceDiscovered(entranceIndex); });
|
||||
|
||||
COND_ID_HOOK(OnBossDefeat, ACTOR_BOSS_GANON2, isConnected, [&](void* refActor) { SendPacket_GameComplete(); });
|
||||
|
||||
COND_HOOK(OnItemReceive, isConnected, [&](GetItemEntry itemEntry) {
|
||||
// Handle vanilla dungeon items a bit differently
|
||||
if (itemEntry.modIndex == MOD_NONE &&
|
||||
(itemEntry.itemId >= ITEM_KEY_BOSS && itemEntry.itemId <= ITEM_KEY_SMALL)) {
|
||||
SendPacket_UpdateDungeonItems();
|
||||
return;
|
||||
}
|
||||
|
||||
SendPacket_GiveItem(itemEntry.tableId, itemEntry.getItemId);
|
||||
});
|
||||
|
||||
COND_HOOK(OnDungeonKeyUsed, isConnected, [&](uint16_t mapIndex) {
|
||||
// Handle vanilla dungeon items a bit differently
|
||||
SendPacket_UpdateDungeonItems();
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Hooks that are purely to sync actor states across the clients, not super essential
|
||||
|
||||
COND_ID_HOOK(OnActorUpdate, ACTOR_EN_ITEM00, isConnected, [&](void* refActor) {
|
||||
EnItem00* actor = static_cast<EnItem00*>(refActor);
|
||||
|
||||
if (Flags_GetCollectible(gPlayState, actor->collectibleFlag)) {
|
||||
Actor_Kill(&actor->actor);
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_BOMBWALL, isConnected, [&](void* refActor, bool* should) {
|
||||
BgBombwall* actor = static_cast<BgBombwall*>(refActor);
|
||||
|
||||
if (actor->actionFunc == func_8086ED70 && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_BREAKWALL, isConnected, [&](void* refActor, bool* should) {
|
||||
BgBreakwall* actor = static_cast<BgBreakwall*>(refActor);
|
||||
|
||||
if (actor->actionFunc == BgBreakwall_Wait && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HAKA_ZOU, isConnected, [&](void* refActor, bool* should) {
|
||||
BgHakaZou* actor = static_cast<BgHakaZou*>(refActor);
|
||||
|
||||
if (actor->actionFunc == func_80883000 && Flags_GetSwitch(gPlayState, actor->switchFlag)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HIDAN_HAMSTEP, isConnected, [&](void* refActor, bool* should) {
|
||||
BgHidanHamstep* actor = static_cast<BgHidanHamstep*>(refActor);
|
||||
|
||||
if (actor->actionFunc == func_808887C4 && Flags_GetSwitch(gPlayState, (actor->dyna.actor.params >> 8) & 0xFF)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HIDAN_HROCK, isConnected, [&](void* refActor, bool* should) {
|
||||
BgHidanHrock* actor = static_cast<BgHidanHrock*>(refActor);
|
||||
|
||||
if (actor->actionFunc == func_808896B8 && Flags_GetSwitch(gPlayState, actor->unk_16A)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_ICE_SHELTER, isConnected, [&](void* refActor, bool* should) {
|
||||
BgIceShelter* actor = static_cast<BgIceShelter*>(refActor);
|
||||
|
||||
if (actor->actionFunc == func_8089107C && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) {
|
||||
func_808911BC(actor);
|
||||
Audio_PlayActorSound2(&actor->dyna.actor, NA_SE_EV_ICE_MELT);
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_JYA_BOMBCHUIWA, isConnected, [&](void* refActor, bool* should) {
|
||||
BgJyaBombchuiwa* actor = static_cast<BgJyaBombchuiwa*>(refActor);
|
||||
|
||||
if (actor->actionFunc == BgJyaBombchuiwa_WaitForExplosion &&
|
||||
Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_JYA_BOMBIWA, isConnected, [&](void* refActor, bool* should) {
|
||||
BgJyaBombiwa* actor = static_cast<BgJyaBombiwa*>(refActor);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_MIZU_BWALL, isConnected, [&](void* refActor, bool* should) {
|
||||
BgMizuBwall* actor = static_cast<BgMizuBwall*>(refActor);
|
||||
|
||||
if (actor->actionFunc == BgMizuBwall_Idle &&
|
||||
Flags_GetSwitch(gPlayState, ((u16)actor->dyna.actor.params >> 8) & 0x3F)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT08_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) {
|
||||
BgSpot08Bakudankabe* actor = static_cast<BgSpot08Bakudankabe*>(refActor);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT11_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) {
|
||||
BgSpot11Bakudankabe* actor = static_cast<BgSpot11Bakudankabe*>(refActor);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT17_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) {
|
||||
BgSpot17Bakudankabe* actor = static_cast<BgSpot17Bakudankabe*>(refActor);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) {
|
||||
func_808B6BC0(actor, gPlayState);
|
||||
SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN);
|
||||
Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME);
|
||||
Actor_Kill(&actor->dyna.actor);
|
||||
*should = false;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_YDAN_MARUTA, isConnected, [&](void* refActor, bool* should) {
|
||||
BgYdanMaruta* actor = static_cast<BgYdanMaruta*>(refActor);
|
||||
|
||||
if (actor->actionFunc == func_808BF078 && Flags_GetSwitch(gPlayState, actor->switchFlag)) {
|
||||
actor->collider.base.acFlags |= AC_HIT;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_YDAN_SP, isConnected, [&](void* refActor, bool* should) {
|
||||
BgYdanSp* actor = static_cast<BgYdanSp*>(refActor);
|
||||
|
||||
if ((actor->actionFunc == BgYdanSp_FloorWebIdle || actor->actionFunc == BgYdanSp_WallWebIdle) &&
|
||||
Flags_GetSwitch(gPlayState, actor->isDestroyedSwitchFlag)) {
|
||||
BgYdanSp_BurnWeb(actor, gPlayState);
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_DOOR_SHUTTER, isConnected, [&](void* refActor, bool* should) {
|
||||
DoorShutter* actor = static_cast<DoorShutter*>(refActor);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) {
|
||||
DECR(actor->unk_16E);
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_DOOR, isConnected, [&](void* refActor, bool* should) {
|
||||
EnDoor* actor = static_cast<EnDoor*>(refActor);
|
||||
|
||||
if (actor->actionFunc == EnDoor_Idle && Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) {
|
||||
DECR(actor->lockTimer);
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_SI, isConnected, [&](void* refActor, bool* should) {
|
||||
EnSi* actor = static_cast<EnSi*>(refActor);
|
||||
|
||||
if (GET_GS_FLAGS((actor->actor.params & 0x1F00) >> 8) & (actor->actor.params & 0xFF)) {
|
||||
Actor_Kill(&actor->actor);
|
||||
*should = false;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_SW, isConnected, [&](void* refActor, bool* should) {
|
||||
EnSw* actor = static_cast<EnSw*>(refActor);
|
||||
|
||||
if (GET_GS_FLAGS((actor->actor.params & 0x1F00) >> 8) & (actor->actor.params & 0xFF)) {
|
||||
Actor_Kill(&actor->actor);
|
||||
*should = false;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_ITEM_B_HEART, isConnected, [&](void* refActor, bool* should) {
|
||||
ItemBHeart* actor = static_cast<ItemBHeart*>(refActor);
|
||||
|
||||
if (Flags_GetCollectible(gPlayState, 0x1F)) {
|
||||
Actor_Kill(&actor->actor);
|
||||
*should = false;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_BOMBIWA, isConnected, [&](void* refActor, bool* should) {
|
||||
ObjBombiwa* actor = static_cast<ObjBombiwa*>(refActor);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) {
|
||||
ObjBombiwa_Break(actor, gPlayState);
|
||||
SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->actor.world.pos, 80, NA_SE_EV_WALL_BROKEN);
|
||||
Actor_Kill(&actor->actor);
|
||||
*should = false;
|
||||
}
|
||||
});
|
||||
|
||||
COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_HAMISHI, isConnected, [&](void* refActor, bool* should) {
|
||||
ObjHamishi* actor = static_cast<ObjHamishi*>(refActor);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) {
|
||||
ObjHamishi_Break(actor, gPlayState);
|
||||
SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->actor.world.pos, 40, NA_SE_EV_WALL_BROKEN);
|
||||
Actor_Kill(&actor->actor);
|
||||
*should = false;
|
||||
}
|
||||
});
|
||||
|
||||
COND_VB_SHOULD(VB_HAMMER_TOTEM_BREAK, isConnected, {
|
||||
BgHidanDalm* actor = va_arg(args, BgHidanDalm*);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, actor->switchFlag)) {
|
||||
*should = true;
|
||||
}
|
||||
});
|
||||
|
||||
COND_VB_SHOULD(VB_FIRE_TEMPLE_BOMBABLE_WALL_BREAK, isConnected, {
|
||||
BgHidanKowarerukabe* actor = va_arg(args, BgHidanKowarerukabe*);
|
||||
|
||||
if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params >> 8) & 0x3F)) {
|
||||
*should = true;
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
}
|
||||
205
soh/soh/Network/Anchor/JsonConversions.hpp
Normal file
205
soh/soh/Network/Anchor/JsonConversions.hpp
Normal file
@@ -0,0 +1,205 @@
|
||||
#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{
|
||||
{ "triforcePiecesCollected", shipRandomizerSaveContextData.triforcePiecesCollected },
|
||||
};
|
||||
}
|
||||
|
||||
inline void from_json(const json& j, ShipRandomizerSaveContextData& shipRandomizerSaveContextData) {
|
||||
j.at("triforcePiecesCollected").get_to(shipRandomizerSaveContextData.triforcePiecesCollected);
|
||||
}
|
||||
|
||||
inline void to_json(json& j, const ShipQuestSpecificSaveContextData& shipQuestSpecificSaveContextData) {
|
||||
j = json{
|
||||
{ "randomizer", shipQuestSpecificSaveContextData.randomizer },
|
||||
};
|
||||
}
|
||||
|
||||
inline void from_json(const json& j, ShipQuestSpecificSaveContextData& shipQuestSpecificSaveContextData) {
|
||||
j.at("randomizer").get_to(shipQuestSpecificSaveContextData.randomizer);
|
||||
}
|
||||
|
||||
inline void to_json(json& j, const ShipQuestSaveContextData& shipQuestSaveContextData) {
|
||||
j = json{
|
||||
{ "id", shipQuestSaveContextData.id },
|
||||
{ "data", shipQuestSaveContextData.data },
|
||||
};
|
||||
}
|
||||
|
||||
inline void from_json(const json& j, ShipQuestSaveContextData& shipQuestSaveContextData) {
|
||||
j.at("id").get_to(shipQuestSaveContextData.id);
|
||||
j.at("data").get_to(shipQuestSaveContextData.data);
|
||||
}
|
||||
|
||||
inline void to_json(json& j, const ShipSaveContextData& shipSaveContextData) {
|
||||
j = json{
|
||||
{ "stats", shipSaveContextData.stats },
|
||||
{ "quest", shipSaveContextData.quest },
|
||||
{ "randomizerInf", shipSaveContextData.randomizerInf },
|
||||
};
|
||||
}
|
||||
|
||||
inline void from_json(const json& j, ShipSaveContextData& shipSaveContextData) {
|
||||
j.at("stats").get_to(shipSaveContextData.stats);
|
||||
j.at("quest").get_to(shipSaveContextData.quest);
|
||||
j.at("randomizerInf").get_to(shipSaveContextData.randomizerInf);
|
||||
}
|
||||
|
||||
inline void to_json(json& j, const SaveContext& saveContext) {
|
||||
std::vector<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
|
||||
239
soh/soh/Network/Anchor/Menu.cpp
Normal file
239
soh/soh/Network/Anchor/Menu.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
#include "Anchor.h"
|
||||
#include <libultraship/libultraship.h>
|
||||
#include "soh/SohGui/SohGui.hpp"
|
||||
#include "soh/SohGui/SohMenu.h"
|
||||
#include "soh/util.h"
|
||||
|
||||
namespace SohGui {
|
||||
extern std::shared_ptr<SohMenu> mSohMenu;
|
||||
extern std::shared_ptr<AnchorRoomWindow> mAnchorRoomWindow;
|
||||
} // namespace SohGui
|
||||
|
||||
static const char* pvpModes[3] = { "Off", "On", "On + Friendly Fire" };
|
||||
static std::vector<const char*> teleportModes = { "None", "Team Only", "All" };
|
||||
static std::vector<const char*> showLocationsModes = { "None", "Team Only", "All" };
|
||||
|
||||
void AnchorMainMenu(WidgetInfo& info) {
|
||||
auto anchor = Anchor::Instance;
|
||||
|
||||
std::string host = CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org");
|
||||
uint16_t port = CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383);
|
||||
std::string anchorTeamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
std::string anchorRoomId = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "");
|
||||
std::string anchorName = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), "");
|
||||
bool isFormValid = !SohUtils::IsStringEmpty(host) && port > 1024 && port < 65535 &&
|
||||
!SohUtils::IsStringEmpty(anchorRoomId) && !SohUtils::IsStringEmpty(anchorName);
|
||||
|
||||
ImGui::SeparatorText("Connection Settings");
|
||||
|
||||
ImGui::BeginDisabled(anchor->isEnabled);
|
||||
ImGui::Text("Host & Port");
|
||||
if (UIWidgets::InputString("##Host", &host,
|
||||
UIWidgets::InputOptions()
|
||||
.Size(ImGui::GetContentRegionAvail() -
|
||||
ImVec2((ImGui::GetFontSize() * 5 + ImGui::GetStyle().ItemSpacing.x), 0))
|
||||
.Color(THEME_COLOR))) {
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("Host"), host.c_str());
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
UIWidgets::PushStyleInput(THEME_COLOR);
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5);
|
||||
if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) {
|
||||
CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), port);
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
}
|
||||
UIWidgets::PopStyleInput();
|
||||
|
||||
ImGui::Text("Name");
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
if (UIWidgets::InputString("##Name", &anchorName, UIWidgets::InputOptions().Color(THEME_COLOR))) {
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("Name"), anchorName.c_str());
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
}
|
||||
ImGui::Text("Room ID");
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
if (UIWidgets::InputString("##RoomId", &anchorRoomId,
|
||||
UIWidgets::InputOptions().IsSecret(anchor->isEnabled).Color(THEME_COLOR))) {
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), anchorRoomId.c_str());
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
}
|
||||
ImGui::Text("Team ID (Items & Flags Shared)");
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
if (UIWidgets::InputString("##TeamId", &anchorTeamId, UIWidgets::InputOptions().Color(THEME_COLOR))) {
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), anchorTeamId.c_str());
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
if (UIWidgets::Button("Restore Defaults", UIWidgets::ButtonOptions()
|
||||
.Size(ImVec2(ImGui::GetContentRegionAvail().x / 2, 0))
|
||||
.Color(UIWidgets::Colors::Red))) {
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org");
|
||||
CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383);
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), "");
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("Name"), "");
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (UIWidgets::Button("Global Room", UIWidgets::ButtonOptions()
|
||||
.Color(UIWidgets::Colors::Blue)
|
||||
.Tooltip("Always-online public room so you don't have to experience "
|
||||
"Hyrule alone. PVP and syncing are disabled."))) {
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org");
|
||||
CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383);
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), "soh-global");
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
}
|
||||
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::BeginDisabled(!isFormValid);
|
||||
const char* buttonLabel = anchor->isEnabled ? "Disable" : "Enable";
|
||||
UIWidgets::PushStyleButton(anchor->isEnabled ? UIWidgets::ColorValues.at(UIWidgets::Colors::Red)
|
||||
: UIWidgets::ColorValues.at(UIWidgets::Colors::Green));
|
||||
if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) {
|
||||
if (anchor->isEnabled) {
|
||||
CVarClear(CVAR_REMOTE_ANCHOR("Enabled"));
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
anchor->Disable();
|
||||
} else {
|
||||
CVarSetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 1);
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
anchor->Enable();
|
||||
}
|
||||
}
|
||||
UIWidgets::PopStyleButton();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (!anchor->isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!anchor->isConnected) {
|
||||
ImGui::Text("Connecting...");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Current Room");
|
||||
ImGui::Text("%s Connected", ICON_FA_CHECK);
|
||||
|
||||
UIWidgets::PushStyleButton(THEME_COLOR);
|
||||
if (ImGui::Button("Request Team State")) {
|
||||
anchor->SendPacket_RequestTeamState();
|
||||
}
|
||||
UIWidgets::Tooltip("Try this if you are missing items or flags that your team members have collected");
|
||||
UIWidgets::PopStyleButton();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
UIWidgets::WindowButton("Toggle Anchor Room Window", CVAR_WINDOW("AnchorRoom"), SohGui::mAnchorRoomWindow);
|
||||
if (!SohGui::mAnchorRoomWindow->IsVisible()) {
|
||||
SohGui::mAnchorRoomWindow->DrawElement();
|
||||
}
|
||||
}
|
||||
|
||||
void AnchorAdminMenu(WidgetInfo& info) {
|
||||
auto anchor = Anchor::Instance;
|
||||
bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""));
|
||||
|
||||
if (!anchor->isEnabled || !anchor->isConnected || anchor->roomState.ownerClientId != anchor->ownClientId ||
|
||||
isGlobalRoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Room Settings (Admin Only)");
|
||||
|
||||
UIWidgets::PushStyleButton(THEME_COLOR);
|
||||
if (ImGui::Button("Clear All Team State")) {
|
||||
std::set<std::string> teams;
|
||||
for (auto& [clientId, client] : Anchor::Instance->clients) {
|
||||
teams.insert(client.teamId);
|
||||
}
|
||||
for (auto& team : teams) {
|
||||
anchor->SendPacket_ClearTeamState(team);
|
||||
}
|
||||
}
|
||||
UIWidgets::PopStyleButton();
|
||||
|
||||
if (UIWidgets::CVarCombobox("PvP Mode:", CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), pvpModes,
|
||||
UIWidgets::ComboboxOptions()
|
||||
.DefaultIndex(1)
|
||||
.LabelPosition(UIWidgets::LabelPositions::Above)
|
||||
.Color(THEME_COLOR))) {
|
||||
anchor->SendPacket_UpdateRoomState();
|
||||
}
|
||||
if (UIWidgets::CVarCombobox("Show Locations For:", CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"),
|
||||
showLocationsModes,
|
||||
UIWidgets::ComboboxOptions()
|
||||
.DefaultIndex(1)
|
||||
.LabelPosition(UIWidgets::LabelPositions::Above)
|
||||
.Color(THEME_COLOR))) {
|
||||
anchor->SendPacket_UpdateRoomState();
|
||||
}
|
||||
if (UIWidgets::CVarCombobox("Allow Teleporting To:", CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), teleportModes,
|
||||
UIWidgets::ComboboxOptions()
|
||||
.DefaultIndex(1)
|
||||
.LabelPosition(UIWidgets::LabelPositions::Above)
|
||||
.Color(THEME_COLOR))) {
|
||||
anchor->SendPacket_UpdateRoomState();
|
||||
}
|
||||
if (UIWidgets::CVarCheckbox("Sync Items & Flags", CVAR_REMOTE_ANCHOR("RoomSettings.SyncItemsAndFlags"),
|
||||
UIWidgets::CheckboxOptions().DefaultValue(true).Color(THEME_COLOR))) {
|
||||
anchor->SendPacket_UpdateRoomState();
|
||||
}
|
||||
}
|
||||
|
||||
void AnchorInstructionsMenu(WidgetInfo& info) {
|
||||
auto anchor = Anchor::Instance;
|
||||
|
||||
ImGui::SeparatorText("Usage Instructions");
|
||||
|
||||
ImGui::TextWrapped("1. All players involved should start at the file select screen");
|
||||
|
||||
ImGui::TextWrapped("2. Come up with a unique Room ID (this is basically your password) and enter it, along with "
|
||||
"your desired player name and team ID and click Enable");
|
||||
|
||||
ImGui::TextWrapped("3. The host should configure the randomizer settings and generate a seed, then share the newly "
|
||||
"generated JSON spoiler file with other players.");
|
||||
|
||||
ImGui::TextWrapped("4. All players should load the same JSON spoiler file (drag it into SoH window), make sure "
|
||||
"seed icons match, then create a new file.");
|
||||
|
||||
ImGui::TextWrapped("5. All players should now load into their game. IMPORTANT! If using an existing save/seed "
|
||||
"ensure the player with the most progress loads the file first.");
|
||||
|
||||
ImGui::TextWrapped("6. After everyone has loaded in, verify on the network tab that it doesn't warn about anyone "
|
||||
"being on a wrong version or seed.");
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Note: Team ID is used to group players together in the same team, sharing items and flags. Make sure all "
|
||||
"players who want to share progress use the same Team ID. All players with the same Team ID should be using "
|
||||
"the same randomizer seed, while players on different teams can use different seeds.");
|
||||
}
|
||||
|
||||
void RegisterAnchorMenu() {
|
||||
WidgetPath path = { "Network", "Anchor", SECTION_COLUMN_1 };
|
||||
SohGui::mSohMenu->AddWidget(path, "AnchorMainMenu", WIDGET_CUSTOM)
|
||||
.CustomFunction(AnchorMainMenu)
|
||||
.HideInSearch(true);
|
||||
path.column = SECTION_COLUMN_2;
|
||||
SohGui::mSohMenu->AddWidget(path, "AnchorAdminMenu", WIDGET_CUSTOM)
|
||||
.CustomFunction(AnchorAdminMenu)
|
||||
.HideInSearch(true);
|
||||
SohGui::mSohMenu->AddWidget(path, "AnchorInstructionsMenu", WIDGET_CUSTOM)
|
||||
.CustomFunction(AnchorInstructionsMenu)
|
||||
.HideInSearch(true);
|
||||
}
|
||||
|
||||
static RegisterMenuInitFunc menuInitFunc(RegisterAnchorMenu);
|
||||
71
soh/soh/Network/Anchor/Packets/AllClientState.cpp
Normal file
71
soh/soh/Network/Anchor/Packets/AllClientState.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#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>>();
|
||||
bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""));
|
||||
|
||||
// add new clients
|
||||
for (auto& client : newClients) {
|
||||
if (client.self) {
|
||||
ownClientId = client.clientId;
|
||||
CVarSetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), ownClientId);
|
||||
Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
|
||||
clients[client.clientId].self = true;
|
||||
} else {
|
||||
clients[client.clientId].self = false;
|
||||
if (clients.contains(client.clientId)) {
|
||||
if (clients[client.clientId].online != client.online && !isGlobalRoom) {
|
||||
Notification::Emit({
|
||||
.prefix = client.name,
|
||||
.message = client.online ? "Connected" : "Disconnected",
|
||||
});
|
||||
}
|
||||
} else if (client.online && !isGlobalRoom) {
|
||||
Notification::Emit({
|
||||
.prefix = client.name,
|
||||
.message = "Connected",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clients[client.clientId].clientId = client.clientId;
|
||||
clients[client.clientId].name = client.name;
|
||||
clients[client.clientId].color = client.color;
|
||||
clients[client.clientId].clientVersion = client.clientVersion;
|
||||
clients[client.clientId].teamId = client.teamId;
|
||||
clients[client.clientId].online = client.online;
|
||||
clients[client.clientId].seed = client.seed;
|
||||
clients[client.clientId].isSaveLoaded = client.isSaveLoaded;
|
||||
clients[client.clientId].isGameComplete = client.isGameComplete;
|
||||
clients[client.clientId].sceneNum = client.sceneNum;
|
||||
clients[client.clientId].entranceIndex = client.entranceIndex;
|
||||
}
|
||||
|
||||
// remove clients that are no longer in the list
|
||||
std::vector<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();
|
||||
}
|
||||
65
soh/soh/Network/Anchor/Packets/DamagePlayer.cpp
Normal file
65
soh/soh/Network/Anchor/Packets/DamagePlayer.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#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);
|
||||
|
||||
// Prevent incoming damage during cutscenes or item get sequences
|
||||
if (Player_InBlockingCsMode(gPlayState, self) || self->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS ||
|
||||
self->stateFlags1 & PLAYER_STATE1_GETTING_ITEM) {
|
||||
return;
|
||||
}
|
||||
|
||||
u8 damageEffect = payload["damageEffect"].get<u8>();
|
||||
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);
|
||||
}
|
||||
14
soh/soh/Network/Anchor/Packets/DisableAnchor.cpp
Normal file
14
soh/soh/Network/Anchor/Packets/DisableAnchor.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#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();
|
||||
}
|
||||
33
soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp
Normal file
33
soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "soh/Network/Anchor/Anchor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <libultraship/libultraship.h>
|
||||
#include "soh/Enhancements/game-interactor/GameInteractor.h"
|
||||
#include "soh/Enhancements/randomizer/randomizer_entrance.h"
|
||||
#include "soh/OTRGlobals.h"
|
||||
|
||||
/**
|
||||
* ENTRANCE_DISCOVERED
|
||||
*/
|
||||
|
||||
void Anchor::SendPacket_EntranceDiscovered(u16 entranceIndex) {
|
||||
if (!IsSaveLoaded() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = ENTRANCE_DISCOVERED;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
payload["entranceIndex"] = entranceIndex;
|
||||
payload["quiet"] = true;
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_EntranceDiscovered(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
u16 entranceIndex = payload["entranceIndex"].get<u16>();
|
||||
Entrance_SetEntranceDiscovered(entranceIndex, 1);
|
||||
}
|
||||
42
soh/soh/Network/Anchor/Packets/GameComplete.cpp
Normal file
42
soh/soh/Network/Anchor/Packets/GameComplete.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#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;
|
||||
bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""));
|
||||
|
||||
Notification::Emit({
|
||||
.prefix = isGlobalRoom ? "Someone" : anchorClient.name,
|
||||
.message = RandomElement(gameCompleteMessages),
|
||||
});
|
||||
}
|
||||
108
soh/soh/Network/Anchor/Packets/GiveItem.cpp
Normal file
108
soh/soh/Network/Anchor/Packets/GiveItem.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#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
|
||||
*/
|
||||
|
||||
uint8_t incomingIceTrapsFromAnchor = 0;
|
||||
|
||||
void Anchor::SendPacket_GiveItem(u16 modId, s16 getItemId) {
|
||||
if (!IsSaveLoaded() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (modId == MOD_RANDOMIZER && getItemId == RG_ICE_TRAP && incomingIceTrapsFromAnchor > 0) {
|
||||
incomingIceTrapsFromAnchor = MAX(incomingIceTrapsFromAnchor - 1, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore sending master sword in final Ganon fight
|
||||
if (modId == MOD_RANDOMIZER && getItemId == RG_MASTER_SWORD && gPlayState->sceneNum == SCENE_GANON_BOSS) {
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = GIVE_ITEM;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
payload["addToQueue"] = true;
|
||||
payload["modId"] = modId;
|
||||
payload["getItemId"] = getItemId;
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_GiveItem(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t clientId = payload["clientId"].get<uint32_t>();
|
||||
AnchorClient& client = clients[clientId];
|
||||
u16 modId = payload["modId"].get<u16>();
|
||||
u16 getItemId = payload["getItemId"].get<u16>();
|
||||
|
||||
GetItemEntry getItemEntry;
|
||||
if (modId == MOD_NONE) {
|
||||
getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, getItemId);
|
||||
} else {
|
||||
getItemEntry = Rando::StaticData::RetrieveItem(static_cast<RandomizerGet>(getItemId)).GetGIEntry_Copy();
|
||||
}
|
||||
|
||||
if (getItemEntry.modIndex == MOD_NONE) {
|
||||
if (getItemEntry.getItemId == GI_SWORD_BGS) {
|
||||
gSaveContext.bgsFlag = true;
|
||||
}
|
||||
Item_Give(gPlayState, getItemEntry.itemId);
|
||||
} else if (getItemEntry.modIndex == MOD_RANDOMIZER) {
|
||||
if (getItemEntry.getItemId == RG_ICE_TRAP) {
|
||||
gSaveContext.ship.pendingIceTrapCount++;
|
||||
incomingIceTrapsFromAnchor++;
|
||||
} else {
|
||||
Randomizer_Item_Give(gPlayState, getItemEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// Full heal if getting a heart container or piece
|
||||
if (getItemEntry.gid == GID_HEART_CONTAINER || getItemEntry.gid == GID_HEART_PIECE) {
|
||||
gSaveContext.healthAccumulator = 0x140;
|
||||
}
|
||||
|
||||
// Handle if the player gets a 4th heart piece (usually handled in z_message)
|
||||
s32 heartPieces = (s32)(gSaveContext.inventory.questItems & 0xF0000000) >> (QUEST_HEART_PIECE + 4);
|
||||
if (heartPieces >= 4) {
|
||||
gSaveContext.inventory.questItems &= ~0xF0000000;
|
||||
gSaveContext.inventory.questItems += (heartPieces % 4) << (QUEST_HEART_PIECE + 4);
|
||||
gSaveContext.healthCapacity += 0x10 * (heartPieces / 4);
|
||||
gSaveContext.health += 0x10 * (heartPieces / 4);
|
||||
}
|
||||
|
||||
if (getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK) {
|
||||
if (getItemEntry.modIndex == MOD_NONE) {
|
||||
Notification::Emit({
|
||||
.itemIcon = GetTextureForItemId(getItemEntry.itemId),
|
||||
.prefix = client.name,
|
||||
.message = "found",
|
||||
.suffix = SohUtils::GetItemName(getItemEntry.itemId),
|
||||
});
|
||||
} else if (getItemEntry.modIndex == MOD_RANDOMIZER) {
|
||||
Notification::Emit({
|
||||
.prefix = client.name,
|
||||
.message = "found",
|
||||
.suffix = Rando::StaticData::RetrieveItem((RandomizerGet)getItemEntry.getItemId).GetName().english,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
22
soh/soh/Network/Anchor/Packets/Handshake.cpp
Normal file
22
soh/soh/Network/Anchor/Packets/Handshake.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#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);
|
||||
}
|
||||
47
soh/soh/Network/Anchor/Packets/PlayerSfx.cpp
Normal file
47
soh/soh/Network/Anchor/Packets/PlayerSfx.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#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);
|
||||
}
|
||||
117
soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp
Normal file
117
soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#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 (size_t i = 0; i < 24; i++) {
|
||||
Vec3s joint = player->skelAnime.jointTable[i];
|
||||
jointArray.push_back(joint.x);
|
||||
jointArray.push_back(joint.y);
|
||||
jointArray.push_back(joint.z);
|
||||
}
|
||||
payload["jointTable"] = jointArray;
|
||||
payload["upperLimbRot"] = player->upperLimbRot;
|
||||
payload["currentBoots"] = player->currentBoots;
|
||||
payload["currentShield"] = player->currentShield;
|
||||
payload["currentTunic"] = player->currentTunic;
|
||||
payload["stateFlags1"] = player->stateFlags1;
|
||||
payload["stateFlags2"] = player->stateFlags2;
|
||||
payload["buttonItem0"] = gSaveContext.equips.buttonItems[0];
|
||||
payload["itemAction"] = player->itemAction;
|
||||
payload["heldItemAction"] = player->heldItemAction;
|
||||
payload["modelGroup"] = player->modelGroup;
|
||||
payload["invincibilityTimer"] = player->invincibilityTimer;
|
||||
payload["unk_862"] = player->unk_862;
|
||||
payload["actionVar1"] = player->av1.actionVar1;
|
||||
payload["quiet"] = true;
|
||||
|
||||
for (auto& [clientId, client] : clients) {
|
||||
if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded && !client.self) {
|
||||
payload["targetClientId"] = clientId;
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) {
|
||||
uint32_t clientId = payload["clientId"].get<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();
|
||||
}
|
||||
}
|
||||
37
soh/soh/Network/Anchor/Packets/RequestTeamState.cpp
Normal file
37
soh/soh/Network/Anchor/Packets/RequestTeamState.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#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() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = REQUEST_TEAM_STATE;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_RequestTeamState(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
SendPacket_UpdateTeamState();
|
||||
}
|
||||
91
soh/soh/Network/Anchor/Packets/RequestTeleport.cpp
Normal file
91
soh/soh/Network/Anchor/Packets/RequestTeleport.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#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 (!CanTeleportTo(clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = REQUEST_TELEPORT;
|
||||
payload["targetClientId"] = clientId;
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_RequestTeleport(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t clientId = payload["clientId"].get<uint32_t>();
|
||||
SendPacket_TeleportTo(clientId);
|
||||
}
|
||||
|
||||
// Reusable function to check if teleporting to a client is allowed
|
||||
bool Anchor::CanTeleportTo(uint32_t clientId) {
|
||||
// Teleporting is disabled
|
||||
if (roomState.teleportMode == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// You're not loaded into a save
|
||||
if (!IsSaveLoaded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The client doesn't exist
|
||||
if (clients.find(clientId) == clients.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AnchorClient& client = clients[clientId];
|
||||
|
||||
// The client is yourself
|
||||
if (client.self) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The client isn't online or loaded into a save
|
||||
if (!client.online || !client.isSaveLoaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Teleporting to team only, but the client is not on your team
|
||||
std::string ownTeamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
if (roomState.teleportMode == 1 && client.teamId != ownTeamId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Problematic scenes for teleporting
|
||||
if (client.sceneNum == SCENE_ID_MAX || client.sceneNum == SCENE_GROTTOS || client.sceneNum == SCENE_MARKET_DAY ||
|
||||
client.sceneNum == SCENE_MARKET_NIGHT || client.sceneNum == SCENE_MARKET_RUINS ||
|
||||
client.sceneNum == SCENE_MARKET_ENTRANCE_DAY || client.sceneNum == SCENE_MARKET_ENTRANCE_NIGHT ||
|
||||
client.sceneNum == SCENE_MARKET_ENTRANCE_RUINS || client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_DAY ||
|
||||
client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_NIGHT ||
|
||||
client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_RUINS || client.sceneNum == SCENE_BACK_ALLEY_DAY ||
|
||||
client.sceneNum == SCENE_BACK_ALLEY_NIGHT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Child can't teleport to Ganon's Castle exterior
|
||||
if (client.sceneNum == SCENE_OUTSIDE_GANONS_CASTLE && gSaveContext.linkAge == LINK_AGE_CHILD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Adult can't teleport to Hyrule Castle exterior
|
||||
if (client.sceneNum == SCENE_HYRULE_CASTLE && gSaveContext.linkAge == LINK_AGE_ADULT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
17
soh/soh/Network/Anchor/Packets/ServerMessage.cpp
Normal file
17
soh/soh/Network/Anchor/Packets/ServerMessage.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#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>(),
|
||||
});
|
||||
}
|
||||
51
soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp
Normal file
51
soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "soh/Network/Anchor/Anchor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <libultraship/libultraship.h>
|
||||
#include "soh/Enhancements/game-interactor/GameInteractor.h"
|
||||
#include "soh/OTRGlobals.h"
|
||||
|
||||
/**
|
||||
* SET_CHECK_STATUS
|
||||
*
|
||||
* Fired when a check status is updated or skipped
|
||||
*/
|
||||
|
||||
void Anchor::SendPacket_SetCheckStatus(RandomizerCheck rc) {
|
||||
if (!IsSaveLoaded() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto randoContext = Rando::Context::GetInstance();
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = SET_CHECK_STATUS;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
payload["addToQueue"] = true;
|
||||
payload["rc"] = rc;
|
||||
payload["status"] = randoContext->GetItemLocation(rc)->GetCheckStatus();
|
||||
payload["skipped"] = randoContext->GetItemLocation(rc)->GetIsSkipped();
|
||||
payload["quiet"] = true;
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto randoContext = Rando::Context::GetInstance();
|
||||
|
||||
RandomizerCheck rc = payload["rc"].get<RandomizerCheck>();
|
||||
RandomizerCheckStatus status = payload["status"].get<RandomizerCheckStatus>();
|
||||
bool skipped = payload["skipped"].get<bool>();
|
||||
|
||||
if (randoContext->GetItemLocation(rc)->GetCheckStatus() != status) {
|
||||
randoContext->GetItemLocation(rc)->SetCheckStatus(status);
|
||||
}
|
||||
if (randoContext->GetItemLocation(rc)->GetIsSkipped() != skipped) {
|
||||
randoContext->GetItemLocation(rc)->SetIsSkipped(skipped);
|
||||
}
|
||||
CheckTracker::RecalculateAllAreaTotals();
|
||||
CheckTracker::RecalculateAvailableChecks();
|
||||
}
|
||||
73
soh/soh/Network/Anchor/Packets/SetFlag.cpp
Normal file
73
soh/soh/Network/Anchor/Packets/SetFlag.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#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"
|
||||
|
||||
extern PlayState* gPlayState;
|
||||
}
|
||||
|
||||
/**
|
||||
* SET_FLAG
|
||||
*
|
||||
* Fired when a flag is set in the save context
|
||||
*/
|
||||
|
||||
void Anchor::SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = SET_FLAG;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
payload["addToQueue"] = true;
|
||||
payload["sceneNum"] = sceneNum;
|
||||
payload["flagType"] = flagType;
|
||||
payload["flag"] = flag;
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_SetFlag(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
s16 sceneNum = payload["sceneNum"].get<s16>();
|
||||
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] = flagType;
|
||||
effect->parameters[1] = flag;
|
||||
effect->Apply();
|
||||
|
||||
// Special case: If King Zora moved, and the player has Ruto's Letter, convert it to an empty bottle
|
||||
if (flagType == FLAG_EVENT_CHECK_INF && flag == EVENTCHKINF_KING_ZORA_MOVED &&
|
||||
Inventory_HasSpecificBottle(ITEM_LETTER_RUTO)) {
|
||||
Inventory_ReplaceItem(gPlayState, ITEM_LETTER_RUTO, ITEM_BOTTLE);
|
||||
}
|
||||
} else {
|
||||
// Special case: Ignore water temple water level flags, stored at 0x1C, 0x1D, 0x1E.
|
||||
if (sceneNum == SCENE_WATER_TEMPLE && flagType == FLAG_SCENE_SWITCH &&
|
||||
(flag == 0x1C || flag == 0x1D || flag == 0x1E)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: Ignore forest temple elevator flag, stored at 0x1B.
|
||||
if (sceneNum == SCENE_FOREST_TEMPLE && flagType == FLAG_SCENE_SWITCH && flag == 0x1B) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto effect = new GameInteractionEffect::SetSceneFlag();
|
||||
effect->parameters[0] = sceneNum;
|
||||
effect->parameters[1] = flagType;
|
||||
effect->parameters[2] = flag;
|
||||
effect->Apply();
|
||||
}
|
||||
}
|
||||
59
soh/soh/Network/Anchor/Packets/TeleportTo.cpp
Normal file
59
soh/soh/Network/Anchor/Packets/TeleportTo.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#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);
|
||||
});
|
||||
}
|
||||
109
soh/soh/Network/Anchor/Packets/UnsetFlag.cpp
Normal file
109
soh/soh/Network/Anchor/Packets/UnsetFlag.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#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/ShuffleTradeItems.h"
|
||||
extern PlayState* gPlayState;
|
||||
}
|
||||
|
||||
/**
|
||||
* UNSET_FLAG
|
||||
*
|
||||
* Fired when a flag is unset in the save context
|
||||
*/
|
||||
|
||||
void Anchor::SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = UNSET_FLAG;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
payload["addToQueue"] = true;
|
||||
payload["sceneNum"] = sceneNum;
|
||||
payload["flagType"] = flagType;
|
||||
payload["flag"] = flag;
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_UnsetFlag(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
s16 sceneNum = payload["sceneNum"].get<s16>();
|
||||
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] = flagType;
|
||||
effect->parameters[1] = flag;
|
||||
effect->Apply();
|
||||
|
||||
// Special case: If an adult trade item flag is unset, replace the item if the player has it equipped
|
||||
if (flagType == FLAG_RANDOMIZER_INF &&
|
||||
(flag >= RAND_INF_ADULT_TRADES_HAS_POCKET_EGG && flag <= RAND_INF_ADULT_TRADES_HAS_CLAIM_CHECK)) {
|
||||
u16 itemToReplace = ITEM_POCKET_EGG;
|
||||
switch (flag) {
|
||||
case RAND_INF_ADULT_TRADES_HAS_POCKET_EGG:
|
||||
itemToReplace = ITEM_POCKET_EGG;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_POCKET_CUCCO:
|
||||
itemToReplace = ITEM_POCKET_CUCCO;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_COJIRO:
|
||||
itemToReplace = ITEM_COJIRO;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_ODD_MUSHROOM:
|
||||
itemToReplace = ITEM_ODD_MUSHROOM;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_ODD_POTION:
|
||||
itemToReplace = ITEM_ODD_POTION;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_SAW:
|
||||
itemToReplace = ITEM_SAW;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_SWORD_BROKEN:
|
||||
itemToReplace = ITEM_SWORD_BROKEN;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_PRESCRIPTION:
|
||||
itemToReplace = ITEM_PRESCRIPTION;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_FROG:
|
||||
itemToReplace = ITEM_FROG;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_EYEDROPS:
|
||||
itemToReplace = ITEM_EYEDROPS;
|
||||
break;
|
||||
case RAND_INF_ADULT_TRADES_HAS_CLAIM_CHECK:
|
||||
itemToReplace = ITEM_CLAIM_CHECK;
|
||||
break;
|
||||
}
|
||||
Inventory_ReplaceItem(gPlayState, itemToReplace, Randomizer_GetNextAdultTradeItem());
|
||||
}
|
||||
} else {
|
||||
// Special case: Ignore water temple water level flags, stored at 0x1C, 0x1D, 0x1E.
|
||||
if (sceneNum == SCENE_WATER_TEMPLE && flagType == FLAG_SCENE_SWITCH &&
|
||||
(flag == 0x1C || flag == 0x1D || flag == 0x1E)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: Ignore forest temple elevator flag, stored at 0x1B.
|
||||
if (sceneNum == SCENE_FOREST_TEMPLE && flagType == FLAG_SCENE_SWITCH && flag == 0x1B) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto effect = new GameInteractionEffect::UnsetSceneFlag();
|
||||
effect->parameters[0] = sceneNum;
|
||||
effect->parameters[1] = flagType;
|
||||
effect->parameters[2] = flag;
|
||||
effect->Apply();
|
||||
}
|
||||
}
|
||||
39
soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp
Normal file
39
soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#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() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = UPDATE_BEANS_COUNT;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
payload["addToQueue"] = true;
|
||||
payload["amount"] = AMMO(ITEM_BEAN);
|
||||
payload["amountBought"] = BEANS_BOUGHT;
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_UpdateBeansCount(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
AMMO(ITEM_BEAN) = payload["amount"].get<s8>();
|
||||
BEANS_BOUGHT = payload["amountBought"].get<s8>();
|
||||
}
|
||||
73
soh/soh/Network/Anchor/Packets/UpdateClientState.cpp
Normal file
73
soh/soh/Network/Anchor/Packets/UpdateClientState.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
38
soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp
Normal file
38
soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#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() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json payload;
|
||||
payload["type"] = UPDATE_DUNGEON_ITEMS;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
payload["addToQueue"] = true;
|
||||
payload["mapIndex"] = gSaveContext.mapIndex;
|
||||
payload["dungeonItems"] = gSaveContext.inventory.dungeonItems[gSaveContext.mapIndex];
|
||||
payload["dungeonKeys"] = gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex];
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) {
|
||||
if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
u16 mapIndex = payload["mapIndex"].get<u16>();
|
||||
gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get<u8>();
|
||||
gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get<s8>();
|
||||
}
|
||||
55
soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp
Normal file
55
soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#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;
|
||||
bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""));
|
||||
|
||||
if (isGlobalRoom) {
|
||||
// Global room uses hardcoded settings
|
||||
payload["pvpMode"] = 0;
|
||||
payload["showLocationsMode"] = 0;
|
||||
payload["teleportMode"] = 0;
|
||||
payload["syncItemsAndFlags"] = 0;
|
||||
} else {
|
||||
payload["pvpMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), 1);
|
||||
payload["showLocationsMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), 1);
|
||||
payload["teleportMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), 1);
|
||||
payload["syncItemsAndFlags"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.SyncItemsAndFlags"), 1);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
void Anchor::SendPacket_UpdateRoomState() {
|
||||
nlohmann::json payload;
|
||||
payload["type"] = UPDATE_ROOM_STATE;
|
||||
payload["state"] = PrepRoomState();
|
||||
|
||||
Network::SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_UpdateRoomState(nlohmann::json payload) {
|
||||
if (!payload.contains("state")) {
|
||||
return;
|
||||
}
|
||||
|
||||
roomState.ownerClientId = payload["state"]["ownerClientId"].get<uint32_t>();
|
||||
roomState.pvpMode = payload["state"]["pvpMode"].get<u8>();
|
||||
roomState.showLocationsMode = payload["state"]["showLocationsMode"].get<u8>();
|
||||
roomState.teleportMode = payload["state"]["teleportMode"].get<u8>();
|
||||
roomState.syncItemsAndFlags = payload["state"]["syncItemsAndFlags"].get<u8>();
|
||||
}
|
||||
300
soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp
Normal file
300
soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
#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() || !roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
json payload;
|
||||
payload["type"] = UPDATE_TEAM_STATE;
|
||||
payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default");
|
||||
|
||||
// Assume the team queue has been emptied, so clear it
|
||||
payload["queue"] = json::array();
|
||||
|
||||
payload["state"] = gSaveContext;
|
||||
// manually update current scene flags
|
||||
payload["state"]["sceneFlags"][gPlayState->sceneNum * 4] = gPlayState->actorCtx.flags.chest;
|
||||
payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 1] = gPlayState->actorCtx.flags.swch;
|
||||
payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 2] = gPlayState->actorCtx.flags.clear;
|
||||
payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 3] = gPlayState->actorCtx.flags.collect;
|
||||
|
||||
// The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player
|
||||
// doesn't have to generate the seed themselves Currently it doesn't work :)
|
||||
if (IS_RANDO) {
|
||||
auto randoContext = Rando::Context::GetInstance();
|
||||
|
||||
payload["state"]["rando"] = json::object();
|
||||
payload["state"]["rando"]["itemLocations"] = json::array();
|
||||
for (int i = 0; i < RC_MAX; i++) {
|
||||
payload["state"]["rando"]["itemLocations"][i] = json::array();
|
||||
// payload["state"]["rando"]["itemLocations"][i]["rgID"] =
|
||||
// randoContext->GetItemLocation(i)->GetPlacedRandomizerGet();
|
||||
payload["state"]["rando"]["itemLocations"][i][0] = randoContext->GetItemLocation(i)->GetCheckStatus();
|
||||
payload["state"]["rando"]["itemLocations"][i][1] = (u8)randoContext->GetItemLocation(i)->GetIsSkipped();
|
||||
|
||||
// if (randoContext->GetItemLocation(i)->GetPlacedRandomizerGet() == RG_ICE_TRAP) {
|
||||
// payload["state"]["rando"]["itemLocations"][i]["fakeRgID"] =
|
||||
// randoContext->GetItemOverride(i).LooksLike();
|
||||
// payload["state"]["rando"]["itemLocations"][i]["trickName"] = json::object();
|
||||
// payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"] =
|
||||
// randoContext->GetItemOverride(i).GetTrickName().GetEnglish();
|
||||
// payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"] =
|
||||
// randoContext->GetItemOverride(i).GetTrickName().GetFrench();
|
||||
// }
|
||||
// if (randoContext->GetItemLocation(i)->HasCustomPrice()) {
|
||||
// payload["state"]["rando"]["itemLocations"][i]["price"] =
|
||||
// randoContext->GetItemLocation(i)->GetPrice();
|
||||
// }
|
||||
}
|
||||
|
||||
// auto entranceCtx = randoContext->GetEntranceShuffler();
|
||||
// for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) {
|
||||
// payload["state"]["rando"]["entrances"][i] = json::object();
|
||||
// payload["state"]["rando"]["entrances"][i]["type"] = entranceCtx->entranceOverrides[i].type;
|
||||
// payload["state"]["rando"]["entrances"][i]["index"] = entranceCtx->entranceOverrides[i].index;
|
||||
// payload["state"]["rando"]["entrances"][i]["destination"] = entranceCtx->entranceOverrides[i].destination;
|
||||
// payload["state"]["rando"]["entrances"][i]["override"] = entranceCtx->entranceOverrides[i].override;
|
||||
// payload["state"]["rando"]["entrances"][i]["overrideDestination"] =
|
||||
// entranceCtx->entranceOverrides[i].overrideDestination;
|
||||
// }
|
||||
|
||||
// payload["state"]["rando"]["seed"] = json::array();
|
||||
// for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) {
|
||||
// payload["state"]["rando"]["seed"][i] = randoContext->hashIconIndexes[i];
|
||||
// }
|
||||
// payload["state"]["rando"]["inputSeed"] = randoContext->GetSeedString();
|
||||
// payload["state"]["rando"]["finalSeed"] = randoContext->GetSeed();
|
||||
|
||||
// payload["state"]["rando"]["randoSettings"] = json::array();
|
||||
// for (int i = 0; i < RSK_MAX; i++) {
|
||||
// payload["state"]["rando"]["randoSettings"][i] =
|
||||
// randoContext->GetOption((RandomizerSettingKey(i))).GetSelectedOptionIndex();
|
||||
// }
|
||||
|
||||
// payload["state"]["rando"]["masterQuestDungeonCount"] = randoContext->GetDungeons()->CountMQ();
|
||||
// payload["state"]["rando"]["masterQuestDungeons"] = json::array();
|
||||
// for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) {
|
||||
// payload["state"]["rando"]["masterQuestDungeons"][i] = randoContext->GetDungeon(i)->IsMQ();
|
||||
// }
|
||||
// for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) {
|
||||
// payload["state"]["rando"]["requiredTrials"][i] = randoContext->GetTrial(i)->IsRequired();
|
||||
// }
|
||||
}
|
||||
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::SendPacket_ClearTeamState(std::string teamId) {
|
||||
json payload;
|
||||
payload["type"] = UPDATE_TEAM_STATE;
|
||||
payload["targetTeamId"] = teamId;
|
||||
payload["queue"] = json::array();
|
||||
payload["state"] = json::object();
|
||||
SendJsonToRemote(payload);
|
||||
}
|
||||
|
||||
void Anchor::HandlePacket_UpdateTeamState(nlohmann::json payload) {
|
||||
if (!roomState.syncItemsAndFlags) {
|
||||
return;
|
||||
}
|
||||
|
||||
isHandlingUpdateTeamState = true;
|
||||
// This can happen in between file select and the game starting, so we cant use this check, but we need to ensure we
|
||||
// be careful to wrap PlayState usage in this check
|
||||
// if (!IsSaveLoaded()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (payload.contains("state")) {
|
||||
SaveContext loadedData = payload["state"].get<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++) {
|
||||
if (i == SCENE_WATER_TEMPLE) {
|
||||
// Keep water temple water level flags
|
||||
u32 mask = (1 << 0x1C) | (1 << 0x1D) | (1 << 0x1E);
|
||||
loadedData.sceneFlags[i].swch =
|
||||
(loadedData.sceneFlags[i].swch & ~mask) | (gSaveContext.sceneFlags[i].swch & mask);
|
||||
}
|
||||
|
||||
if (i == SCENE_FOREST_TEMPLE) {
|
||||
// Keep forest temple elevator flag
|
||||
u32 mask = (1 << 0x1B);
|
||||
loadedData.sceneFlags[i].swch =
|
||||
(loadedData.sceneFlags[i].swch & ~mask) | (gSaveContext.sceneFlags[i].swch & mask);
|
||||
}
|
||||
|
||||
gSaveContext.sceneFlags[i] = loadedData.sceneFlags[i];
|
||||
if (IsSaveLoaded() && gPlayState->sceneNum == i) {
|
||||
gPlayState->actorCtx.flags.chest = loadedData.sceneFlags[i].chest;
|
||||
gPlayState->actorCtx.flags.swch = loadedData.sceneFlags[i].swch;
|
||||
gPlayState->actorCtx.flags.clear = loadedData.sceneFlags[i].clear;
|
||||
gPlayState->actorCtx.flags.collect = loadedData.sceneFlags[i].collect;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 14; i++) {
|
||||
gSaveContext.eventChkInf[i] = loadedData.eventChkInf[i];
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
gSaveContext.itemGetInf[i] = loadedData.itemGetInf[i];
|
||||
}
|
||||
|
||||
// Skip last row of infTable, don't want to sync swordless flag
|
||||
for (int i = 0; i < 29; i++) {
|
||||
gSaveContext.infTable[i] = loadedData.infTable[i];
|
||||
}
|
||||
|
||||
for (int i = 0; i < ceil((RAND_INF_MAX + 15) / 16); i++) {
|
||||
gSaveContext.ship.randomizerInf[i] = loadedData.ship.randomizerInf[i];
|
||||
}
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
gSaveContext.gsFlags[i] = loadedData.gsFlags[i];
|
||||
}
|
||||
|
||||
gSaveContext.ship.stats.fileCreatedAt = loadedData.ship.stats.fileCreatedAt;
|
||||
|
||||
// Restore master sword state
|
||||
// Disabling this for now, not really sure I understand why I did this in the past
|
||||
// u8 hasMasterSword = CHECK_OWNED_EQUIP(EQUIP_TYPE_SWORD, 1);
|
||||
// if (hasMasterSword) {
|
||||
// loadedData.inventory.equipment |= 0x2;
|
||||
// } else {
|
||||
// loadedData.inventory.equipment &= ~0x2;
|
||||
// }
|
||||
|
||||
// Restore bottle contents (unless it's ruto's letter)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_NONE &&
|
||||
gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_LETTER_RUTO) {
|
||||
loadedData.inventory.items[SLOT_BOTTLE_1 + i] = gSaveContext.inventory.items[SLOT_BOTTLE_1 + i];
|
||||
}
|
||||
}
|
||||
|
||||
// Restore ammo if it's non-zero, unless it's beans
|
||||
for (int i = 0; i < ARRAY_COUNT(gSaveContext.inventory.ammo); i++) {
|
||||
if (gSaveContext.inventory.ammo[i] != 0 && i != SLOT(ITEM_BEAN) && i != SLOT(ITEM_BEAN + 1)) {
|
||||
loadedData.inventory.ammo[i] = gSaveContext.inventory.ammo[i];
|
||||
}
|
||||
}
|
||||
|
||||
gSaveContext.inventory = loadedData.inventory;
|
||||
|
||||
// The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player
|
||||
// doesn't have to generate the seed themselves Currently it doesn't work :)
|
||||
if (IS_RANDO && payload["state"].contains("rando")) {
|
||||
auto randoContext = Rando::Context::GetInstance();
|
||||
|
||||
for (int i = 0; i < RC_MAX; i++) {
|
||||
// randoContext->GetItemLocation(i)->RefPlacedItem() =
|
||||
// payload["state"]["rando"]["itemLocations"][i]["rgID"].get<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>());
|
||||
incomingPacketQueue.push(itemPayload);
|
||||
}
|
||||
}
|
||||
isHandlingUpdateTeamState = false;
|
||||
}
|
||||
@@ -77,6 +77,7 @@
|
||||
#include "SaveManager.h"
|
||||
#include "soh/Network/CrowdControl/CrowdControl.h"
|
||||
#include "soh/Network/Sail/Sail.h"
|
||||
#include "soh/Network/Anchor/Anchor.h"
|
||||
#include "Enhancements/mods.h"
|
||||
#include "Enhancements/game-interactor/GameInteractor.h"
|
||||
#include "Enhancements/randomizer/draw.h"
|
||||
@@ -142,6 +143,7 @@ AudioCollection* AudioCollection::Instance;
|
||||
SpeechSynthesizer* SpeechSynthesizer::Instance;
|
||||
CrowdControl* CrowdControl::Instance;
|
||||
Sail* Sail::Instance;
|
||||
Anchor* Anchor::Instance;
|
||||
|
||||
extern "C" char** cameraStrings;
|
||||
std::vector<std::shared_ptr<std::string>> cameraStdStrings;
|
||||
@@ -1289,6 +1291,7 @@ extern "C" void InitOTR(int argc, char* argv[]) {
|
||||
|
||||
CrowdControl::Instance = new CrowdControl();
|
||||
Sail::Instance = new Sail();
|
||||
Anchor::Instance = new Anchor();
|
||||
|
||||
OTRMessage_Init();
|
||||
OTRAudio_Init();
|
||||
@@ -1325,6 +1328,9 @@ extern "C" void InitOTR(int argc, char* argv[]) {
|
||||
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
|
||||
Sail::Instance->Enable();
|
||||
}
|
||||
if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) {
|
||||
Anchor::Instance->Enable();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void SaveManager_ThreadPoolWait() {
|
||||
@@ -1340,6 +1346,9 @@ extern "C" void DeinitOTR() {
|
||||
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
|
||||
Sail::Instance->Disable();
|
||||
}
|
||||
if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) {
|
||||
Anchor::Instance->Disable();
|
||||
}
|
||||
#ifdef ENABLE_REMOTE_CONTROL
|
||||
SDLNet_Quit();
|
||||
#endif
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "soh/Notification/Notification.h"
|
||||
#include "soh/Enhancements/TimeDisplay/TimeDisplay.h"
|
||||
#include "soh/Enhancements/mod_menu.h"
|
||||
#include "soh/Network/Anchor/Anchor.h"
|
||||
|
||||
namespace SohGui {
|
||||
|
||||
@@ -98,6 +99,7 @@ std::shared_ptr<RandomizerSettingsWindow> mRandomizerSettingsWindow;
|
||||
std::shared_ptr<SohModalWindow> mModalWindow;
|
||||
std::shared_ptr<Notification::Window> mNotificationWindow;
|
||||
std::shared_ptr<TimeDisplayWindow> mTimeDisplayWindow;
|
||||
std::shared_ptr<AnchorRoomWindow> mAnchorRoomWindow;
|
||||
|
||||
UIWidgets::Colors GetMenuThemeColor() {
|
||||
return mSohMenu->GetMenuThemeColor();
|
||||
@@ -205,6 +207,8 @@ void SetupGuiElements() {
|
||||
mNotificationWindow->Show();
|
||||
mTimeDisplayWindow = std::make_shared<TimeDisplayWindow>(CVAR_WINDOW("TimeDisplayEnabled"), "Additional Timers");
|
||||
gui->AddGuiWindow(mTimeDisplayWindow);
|
||||
mAnchorRoomWindow = std::make_shared<AnchorRoomWindow>(CVAR_WINDOW("AnchorRoom"), "Anchor Room");
|
||||
gui->AddGuiWindow(mAnchorRoomWindow);
|
||||
}
|
||||
|
||||
void Destroy() {
|
||||
@@ -240,6 +244,7 @@ void Destroy() {
|
||||
mTimeSplitWindow = nullptr;
|
||||
mPlandomizerWindow = nullptr;
|
||||
mTimeDisplayWindow = nullptr;
|
||||
mAnchorRoomWindow = nullptr;
|
||||
}
|
||||
|
||||
void RegisterPopup(std::string title, std::string message, std::string button1, std::string button2,
|
||||
|
||||
@@ -180,6 +180,8 @@ void SohMenu::AddMenuNetwork() {
|
||||
.RaceDisable(true)
|
||||
.Options(CheckboxOptions().Tooltip("Enemies spawned by CrowdControl won't be considered for \"clear enemy "
|
||||
"rooms\", so they don't need to be killed to complete these rooms."));
|
||||
path.sidebarName = "Anchor";
|
||||
AddSidebarEntry("Network", path.sidebarName, 2);
|
||||
}
|
||||
|
||||
} // namespace SohGui
|
||||
|
||||
@@ -767,15 +767,18 @@ bool InputString(const char* label, std::string* value, const InputOptions& opti
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ColorValues.at(Colors::Red));
|
||||
}
|
||||
float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x;
|
||||
if (options.alignment == ComponentAlignments::Left) {
|
||||
if (options.labelPosition == LabelPositions::Above) {
|
||||
ImGui::Text(label, *value->c_str());
|
||||
}
|
||||
} else if (options.alignment == ComponentAlignments::Right) {
|
||||
if (options.labelPosition == LabelPositions::Above) {
|
||||
ImGui::NewLine();
|
||||
ImGui::SameLine(width - ImGui::CalcTextSize(label).x);
|
||||
ImGui::Text(label, *value->c_str());
|
||||
ImVec2 labelSize = ImGui::CalcTextSize(label, NULL, true);
|
||||
if (labelSize.x != 0) {
|
||||
if (options.alignment == ComponentAlignments::Left) {
|
||||
if (options.labelPosition == LabelPositions::Above) {
|
||||
ImGui::Text(label, *value->c_str());
|
||||
}
|
||||
} else if (options.alignment == ComponentAlignments::Right) {
|
||||
if (options.labelPosition == LabelPositions::Above) {
|
||||
ImGui::NewLine();
|
||||
ImGui::SameLine(width - ImGui::CalcTextSize(label).x);
|
||||
ImGui::Text(label, *value->c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(width);
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
#define CVAR_REMOTE(var) CVAR_PREFIX_REMOTE "." var
|
||||
#define CVAR_REMOTE_CROWD_CONTROL(var) CVAR_REMOTE("CrowdControl." var)
|
||||
#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE("Sail." var)
|
||||
#define CVAR_REMOTE_ANCHOR(var) CVAR_REMOTE("Anchor." var)
|
||||
#define CVAR_GAMEPLAY_STATS(var) CVAR_PREFIX_GAMEPLAY_STATS "." var
|
||||
#define CVAR_TIME_DISPLAY(var) CVAR_PREFIX_TIME_DISPLAY "." var
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
struct EnSw;
|
||||
|
||||
typedef void (*EnSwActionFunc)(struct EnSw* this, PlayState* play);
|
||||
typedef void (*EnSwActionFunc)(struct EnSw* thisx, PlayState* play);
|
||||
|
||||
typedef struct EnSw {
|
||||
/* 0x0000 */ Actor actor;
|
||||
|
||||
Reference in New Issue
Block a user