From b61db770207664c17fb5c54f20b8547e56fde2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Fri, 5 Jun 2026 06:16:11 +0000 Subject: [PATCH] more hardening of code in Network/ (#6650) also unique_ptr --- soh/soh/Network/Anchor/JsonConversions.hpp | 3 + .../Network/Anchor/Packets/SetCheckStatus.cpp | 5 + soh/soh/Network/Anchor/Packets/SetFlag.cpp | 7 ++ soh/soh/Network/Anchor/Packets/TeleportTo.cpp | 6 ++ soh/soh/Network/Anchor/Packets/UnsetFlag.cpp | 7 ++ .../Anchor/Packets/UpdateDungeonItems.cpp | 7 ++ soh/soh/Network/CrowdControl/CrowdControl.cpp | 35 ++++--- soh/soh/Network/CrowdControl/CrowdControl.h | 4 +- soh/soh/Network/Network.cpp | 6 +- soh/soh/Network/Sail/Sail.cpp | 96 +++++++++---------- soh/soh/Network/Sail/Sail.h | 4 +- 11 files changed, 113 insertions(+), 67 deletions(-) diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp index 8b28ba40e1..0794662336 100644 --- a/soh/soh/Network/Anchor/JsonConversions.hpp +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -191,6 +191,9 @@ inline void from_json(const json& j, SaveContext& saveContext) { j.at("swordHealth").get_to(saveContext.swordHealth); std::vector sceneFlagsArray; j.at("sceneFlags").get_to(sceneFlagsArray); + if (sceneFlagsArray.size() < 124 * 4) { + sceneFlagsArray.resize(124 * 4, 0); + } for (int i = 0; i < 124; i++) { saveContext.sceneFlags[i].chest = sceneFlagsArray[i * 4]; saveContext.sceneFlags[i].swch = sceneFlagsArray[i * 4 + 1]; diff --git a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp index fb1c70dc9a..171ce3d39a 100644 --- a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp +++ b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp @@ -3,6 +3,7 @@ #include #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/OTRGlobals.h" +#include "soh/Enhancements/randomizer/randomizerEnums/RandomizerCheck.h" static bool isResultOfHandling = false; @@ -39,6 +40,10 @@ void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) { auto randoContext = Rando::Context::GetInstance(); RandomizerCheck rc = payload["rc"].get(); + if (rc < 0 || rc >= RC_MAX) { + SPDLOG_ERROR("[Anchor] SET_CHECK_STATUS: rc {} out of range", (int)rc); + return; + } RandomizerCheckStatus status = payload["status"].get(); bool skipped = payload["skipped"].get(); diff --git a/soh/soh/Network/Anchor/Packets/SetFlag.cpp b/soh/soh/Network/Anchor/Packets/SetFlag.cpp index 3468bab016..8b12bf6487 100644 --- a/soh/soh/Network/Anchor/Packets/SetFlag.cpp +++ b/soh/soh/Network/Anchor/Packets/SetFlag.cpp @@ -41,6 +41,13 @@ void Anchor::HandlePacket_SetFlag(nlohmann::json payload) { s16 flagType = payload["flagType"].get(); s16 flag = payload["flag"].get(); + // sceneNum == SCENE_ID_MAX is a sentinel meaning "global flag" (handled below); only larger + // values would index gSaveContext.sceneFlags out of bounds. + if (sceneNum < 0 || sceneNum > SCENE_ID_MAX) { + SPDLOG_ERROR("[Anchor] SET_FLAG: sceneNum {} out of range", sceneNum); + return; + } + if (sceneNum == SCENE_ID_MAX) { auto effect = new GameInteractionEffect::SetFlag(); effect->parameters[0] = flagType; diff --git a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp index 1d50be4495..2c000b1ce5 100644 --- a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp +++ b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp @@ -39,6 +39,12 @@ void Anchor::HandlePacket_TeleportTo(nlohmann::json payload) { s32 entranceIndex = payload["entranceIndex"].get(); s8 roomIndex = payload["roomIndex"].get(); + + if (entranceIndex < 0 || roomIndex < 0) { + SPDLOG_ERROR("[Anchor] TELEPORT_TO: invalid entranceIndex {} or roomIndex {}", entranceIndex, (int)roomIndex); + return; + } + PosRot posRot = payload["posRot"].get(); gPlayState->nextEntranceIndex = entranceIndex; diff --git a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp index cf2fbf5d40..e5e7de7a8b 100644 --- a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp +++ b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp @@ -41,6 +41,13 @@ void Anchor::HandlePacket_UnsetFlag(nlohmann::json payload) { s16 flagType = payload["flagType"].get(); s16 flag = payload["flag"].get(); + // sceneNum == SCENE_ID_MAX is a sentinel meaning "global flag" (handled below); only larger + // values would index gSaveContext.sceneFlags out of bounds. + if (sceneNum < 0 || sceneNum > SCENE_ID_MAX) { + SPDLOG_ERROR("[Anchor] UNSET_FLAG: sceneNum {} out of range", sceneNum); + return; + } + if (sceneNum == SCENE_ID_MAX) { auto effect = new GameInteractionEffect::UnsetFlag(); effect->parameters[0] = flagType; diff --git a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp index 9e0a432009..ed44356e33 100644 --- a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp +++ b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp @@ -1,6 +1,7 @@ #include "soh/Network/Anchor/Anchor.h" #include #include +#include #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/OTRGlobals.h" @@ -33,6 +34,12 @@ void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) { } u16 mapIndex = payload["mapIndex"].get(); + // dungeonKeys is shorter than dungeonItems (19 vs 20), so bound by the smaller of the two. + if (mapIndex >= ARRAY_COUNT(gSaveContext.inventory.dungeonItems) || + mapIndex >= ARRAY_COUNT(gSaveContext.inventory.dungeonKeys)) { + SPDLOG_ERROR("[Anchor] UPDATE_DUNGEON_ITEMS: mapIndex {} out of range", mapIndex); + return; + } gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get(); gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get(); } diff --git a/soh/soh/Network/CrowdControl/CrowdControl.cpp b/soh/soh/Network/CrowdControl/CrowdControl.cpp index 19ac74f276..7bbfb64b7e 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.cpp +++ b/soh/soh/Network/CrowdControl/CrowdControl.cpp @@ -29,19 +29,19 @@ void CrowdControl::OnDisconnected() { } void CrowdControl::OnIncomingJson(nlohmann::json payload) { - Effect* incomingEffect = ParseMessage(payload); + std::unique_ptr incomingEffect = ParseMessage(payload); if (!incomingEffect) { return; } // If effect is not a timed effect, execute and return result. if (!incomingEffect->timeRemaining) { - EffectResult result = CrowdControl::ExecuteEffect(incomingEffect); + EffectResult result = CrowdControl::ExecuteEffect(incomingEffect.get()); EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, result); } else { // If another timed effect is already active that conflicts with the incoming effect. bool isConflictingEffectActive = false; - for (Effect* effect : activeEffects) { + for (const auto& effect : activeEffects) { if (effect != incomingEffect && effect->category == incomingEffect->category && effect->id < incomingEffect->id) { isConflictingEffectActive = true; @@ -52,14 +52,14 @@ void CrowdControl::OnIncomingJson(nlohmann::json payload) { if (!isConflictingEffectActive) { // Check if effect can be applied, if it can't, let CC know. - EffectResult result = CrowdControl::CanApplyEffect(incomingEffect); + EffectResult result = CrowdControl::CanApplyEffect(incomingEffect.get()); if (result == EffectResult::Retry || result == EffectResult::Failure) { EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, result); return; } activeEffectsMutex.lock(); - activeEffects.push_back(incomingEffect); + activeEffects.push_back(std::move(incomingEffect)); activeEffectsMutex.unlock(); } } @@ -74,17 +74,15 @@ void CrowdControl::ProcessActiveEffects() { auto it = activeEffects.begin(); while (it != activeEffects.end()) { - Effect* effect = *it; + Effect* effect = it->get(); EffectResult result = CrowdControl::ExecuteEffect(effect); if (result == EffectResult::Success) { // If time remaining has reached 0, we have finished the effect. if (effect->timeRemaining <= 0) { - it = activeEffects.erase(std::remove(activeEffects.begin(), activeEffects.end(), effect), - activeEffects.end()); GameInteractor::RemoveEffect( *dynamic_cast(effect->giEffect.get())); - delete effect; + it = activeEffects.erase(it); } else { // If we have a success after previously being paused, tell CC to resume timer. if (effect->isPaused) { @@ -168,7 +166,7 @@ CrowdControl::EffectResult CrowdControl::TranslateGiEnum(GameInteractionEffectQu return result; } -CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { +std::unique_ptr CrowdControl::ParseMessage(nlohmann::json dataReceived) { if (!dataReceived.contains("id") || !dataReceived.contains("type")) { SPDLOG_ERROR("[CrowdControl] Invalid payload received:\n{}", dataReceived.dump()); return nullptr; @@ -176,13 +174,16 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { SPDLOG_INFO("[CrowdControl] Received payload:\n{}", dataReceived.dump()); - if (!dataReceived.contains("code")) { + // "parameters" is intentionally not required: most effects (spawn enemies, teleports, status + // effects, etc.) carry no parameters. Its absence is handled safely below, and any type error + // is caught by the guard in Network::HandleRemoteJson. + if (!dataReceived.contains("code") || !dataReceived.contains("viewer")) { // This seems to happen when the CC session ends - SPDLOG_ERROR("[CrowdControl] Payload does not contain code, ignoring."); + SPDLOG_ERROR("[CrowdControl] Payload does not contain code or viewer, ignoring."); return nullptr; } - Effect* effect = new Effect(); + auto effect = std::make_unique(); effect->lastExecutionResult = EffectResult::Initiate; effect->id = dataReceived["id"]; effect->viewerName = dataReceived["viewer"]; @@ -194,9 +195,15 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { receivedParameter = dataReceived["parameters"][0]; } + auto it = effectStringToEnum.find(effectName); + if (it == effectStringToEnum.end()) { + SPDLOG_ERROR("[CrowdControl] Unknown effect code: {}", effectName); + return nullptr; + } + // Assign GameInteractionEffect + values to CC effect. // Categories are mostly used for checking for conflicting timed effects. - switch (effectStringToEnum[effectName]) { + switch (it->second) { // Spawn Enemies and Objects case kEffectSpawnCuccoStorm: diff --git a/soh/soh/Network/CrowdControl/CrowdControl.h b/soh/soh/Network/CrowdControl/CrowdControl.h index 5cc0883d42..8effe1ebed 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.h +++ b/soh/soh/Network/CrowdControl/CrowdControl.h @@ -64,14 +64,14 @@ class CrowdControl : public Network { std::thread ccThreadProcess; - std::vector activeEffects; + std::vector> activeEffects; std::mutex activeEffectsMutex; void HandleRemoteData(nlohmann::json payload); void ProcessActiveEffects(); void EmitMessage(uint32_t eventId, long timeRemaining, EffectResult status); - Effect* ParseMessage(nlohmann::json payload); + std::unique_ptr ParseMessage(nlohmann::json payload); EffectResult ExecuteEffect(Effect* effect); EffectResult CanApplyEffect(Effect* effect); EffectResult TranslateGiEnum(GameInteractionEffectQueryResult giResult); diff --git a/soh/soh/Network/Network.cpp b/soh/soh/Network/Network.cpp index 6eae1f3f14..1145705079 100644 --- a/soh/soh/Network/Network.cpp +++ b/soh/soh/Network/Network.cpp @@ -157,5 +157,9 @@ void Network::HandleRemoteJson(std::string payload) { return; } - OnIncomingJson(jsonPayload); + try { + OnIncomingJson(jsonPayload); + } catch (const std::exception& e) { + SPDLOG_ERROR("[Network] Exception handling incoming JSON: {}", e.what()); + } catch (...) { SPDLOG_ERROR("[Network] Unknown exception handling incoming JSON"); } } diff --git a/soh/soh/Network/Sail/Sail.cpp b/soh/soh/Network/Sail/Sail.cpp index ddaf059c58..dcfcca8aca 100644 --- a/soh/soh/Network/Sail/Sail.cpp +++ b/soh/soh/Network/Sail/Sail.cpp @@ -2,8 +2,6 @@ #include #include #include -#include "soh/OTRGlobals.h" -#include "soh/util.h" template bool IsType(const SrcType* src) { return dynamic_cast(src) != nullptr; @@ -98,12 +96,12 @@ void Sail::OnIncomingJson(nlohmann::json payload) { return; } - GameInteractionEffectBase* giEffect = EffectFromJson(payload["effect"]); + auto giEffect = EffectFromJson(payload["effect"]); if (giEffect) { GameInteractionEffectQueryResult result; if (effectType == "remove") { - if (IsType(giEffect)) { - result = dynamic_cast(giEffect)->Remove(); + if (IsType(giEffect.get())) { + result = dynamic_cast(giEffect.get())->Remove(); } else { result = GameInteractionEffectQueryResult::NotPossible; } @@ -133,7 +131,7 @@ void Sail::OnIncomingJson(nlohmann::json payload) { } catch (...) { SPDLOG_ERROR("[Sail] Unknown exception handling remote JSON"); } } -GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { +std::unique_ptr Sail::EffectFromJson(nlohmann::json payload) { if (!payload.contains("name")) { return nullptr; } @@ -141,7 +139,7 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { std::string name = payload["name"].get(); if (name == "SetSceneFlag") { - auto effect = new GameInteractionEffect::SetSceneFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); @@ -149,7 +147,7 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { } return effect; } else if (name == "UnsetSceneFlag") { - auto effect = new GameInteractionEffect::UnsetSceneFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); @@ -157,171 +155,171 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { } return effect; } else if (name == "SetFlag") { - auto effect = new GameInteractionEffect::SetFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "UnsetFlag") { - auto effect = new GameInteractionEffect::UnsetFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "ModifyHeartContainers") { - auto effect = new GameInteractionEffect::ModifyHeartContainers(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "FillMagic") { - return new GameInteractionEffect::FillMagic(); + return std::make_unique(); } else if (name == "EmptyMagic") { - return new GameInteractionEffect::EmptyMagic(); + return std::make_unique(); } else if (name == "ModifyRupees") { - auto effect = new GameInteractionEffect::ModifyRupees(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "NoUI") { - return new GameInteractionEffect::NoUI(); + return std::make_unique(); } else if (name == "ModifyGravity") { - auto effect = new GameInteractionEffect::ModifyGravity(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyHealth") { - auto effect = new GameInteractionEffect::ModifyHealth(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetPlayerHealth") { - auto effect = new GameInteractionEffect::SetPlayerHealth(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "FreezePlayer") { - return new GameInteractionEffect::FreezePlayer(); + return std::make_unique(); } else if (name == "BurnPlayer") { - return new GameInteractionEffect::BurnPlayer(); + return std::make_unique(); } else if (name == "ElectrocutePlayer") { - return new GameInteractionEffect::ElectrocutePlayer(); + return std::make_unique(); } else if (name == "KnockbackPlayer") { - auto effect = new GameInteractionEffect::KnockbackPlayer(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyLinkSize") { - auto effect = new GameInteractionEffect::ModifyLinkSize(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "InvisibleLink") { - return new GameInteractionEffect::InvisibleLink(); + return std::make_unique(); } else if (name == "PacifistMode") { - return new GameInteractionEffect::PacifistMode(); + return std::make_unique(); } else if (name == "DisableZTargeting") { - return new GameInteractionEffect::DisableZTargeting(); + return std::make_unique(); } else if (name == "WeatherRainstorm") { - return new GameInteractionEffect::WeatherRainstorm(); + return std::make_unique(); } else if (name == "ReverseControls") { - return new GameInteractionEffect::ReverseControls(); + return std::make_unique(); } else if (name == "ForceEquipBoots") { - auto effect = new GameInteractionEffect::ForceEquipBoots(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyMovementSpeedMultiplier") { - auto effect = new GameInteractionEffect::ModifyMovementSpeedMultiplier(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "OneHitKO") { - return new GameInteractionEffect::OneHitKO(); + return std::make_unique(); } else if (name == "ModifyDefenseModifier") { - auto effect = new GameInteractionEffect::ModifyDefenseModifier(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "GiveOrTakeShield") { - auto effect = new GameInteractionEffect::GiveOrTakeShield(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "TeleportPlayer") { - auto effect = new GameInteractionEffect::TeleportPlayer(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ClearAssignedButtons") { - auto effect = new GameInteractionEffect::ClearAssignedButtons(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetTimeOfDay") { - auto effect = new GameInteractionEffect::SetTimeOfDay(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetCollisionViewer") { - return new GameInteractionEffect::SetCollisionViewer(); + return std::make_unique(); } else if (name == "RandomizeCosmetics") { - return new GameInteractionEffect::RandomizeCosmetics(); + return std::make_unique(); } else if (name == "PressButton") { - auto effect = new GameInteractionEffect::PressButton(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "PressRandomButton") { - auto effect = new GameInteractionEffect::PressRandomButton(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "AddOrTakeAmmo") { - auto effect = new GameInteractionEffect::AddOrTakeAmmo(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "RandomBombFuseTimer") { - return new GameInteractionEffect::RandomBombFuseTimer(); + return std::make_unique(); } else if (name == "DisableLedgeGrabs") { - return new GameInteractionEffect::DisableLedgeGrabs(); + return std::make_unique(); } else if (name == "RandomWind") { - return new GameInteractionEffect::RandomWind(); + return std::make_unique(); } else if (name == "RandomBonks") { - return new GameInteractionEffect::RandomBonks(); + return std::make_unique(); } else if (name == "PlayerInvincibility") { - return new GameInteractionEffect::PlayerInvincibility(); + return std::make_unique(); } else if (name == "SlipperyFloor") { - return new GameInteractionEffect::SlipperyFloor(); + return std::make_unique(); } else if (name == "SpawnEnemyWithOffset") { - auto effect = new GameInteractionEffect::SpawnEnemyWithOffset(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "SpawnActor") { - auto effect = new GameInteractionEffect::SpawnActor(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); diff --git a/soh/soh/Network/Sail/Sail.h b/soh/soh/Network/Sail/Sail.h index fa2f0ff581..418606e66c 100644 --- a/soh/soh/Network/Sail/Sail.h +++ b/soh/soh/Network/Sail/Sail.h @@ -2,12 +2,14 @@ #define NETWORK_SAIL_H #ifdef __cplusplus +#include + #include "soh/Network/Network.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" class Sail : public Network { private: - GameInteractionEffectBase* EffectFromJson(nlohmann::json payload); + std::unique_ptr EffectFromJson(nlohmann::json payload); void RegisterHooks(); public: