diff --git a/config.lua.dist b/config.lua.dist index 10105287115..623619a8c01 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -605,3 +605,17 @@ metricsPrometheusAddress = "0.0.0.0:9464" --- OStream metricsEnableOstream = false metricsOstreamInterval = 1000 + +-- OTC Features +-- NOTE: Features added in this list will be forced to be used on OTCR +-- These features can be found in "modules/gamelib/const.lua" +OTCRFeatures = { + enableFeature = { + 101, -- g_game.enableFeature(GameItemShader) + 102, -- g_game.enableFeature(GameCreatureAttachedEffect) + 103, -- g_game.enableFeature(GameCreatureShader) + 118 -- g_game.enableFeature(GameWingsAurasEffectsShader) + }, + disableFeature = { + } +} diff --git a/data/XML/attachedeffects.xml b/data/XML/attachedeffects.xml new file mode 100644 index 00000000000..c84fb9567a5 --- /dev/null +++ b/data/XML/attachedeffects.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index af7c72c24bc..ea5d9a711e3 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -371,6 +371,8 @@ bool ConfigManager::load() { loadStringConfig(L, WORLD_TYPE, "worldType", "pvp"); loadStringConfig(L, LOGLEVEL, "logLevel", "info"); + loadLuaOTCFeatures(L); + loaded = true; lua_close(L); return true; @@ -519,3 +521,43 @@ float ConfigManager::getFloat(const ConfigKey_t &key, const std::source_location g_logger().warn("[{}] accessing invalid or wrong type index: {}[{}]. Called line: {}:{}, in {}", __FUNCTION__, magic_enum::enum_name(key), fmt::underlying(key), location.line(), location.column(), location.function_name()); return 0.0f; } + +void ConfigManager::loadLuaOTCFeatures(lua_State* L) { + lua_getglobal(L, "OTCRFeatures"); + if (!lua_istable(L, -1)) { + return; + } + + lua_pushstring(L, "enableFeature"); + lua_gettable(L, -2); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + const auto feature = static_cast(lua_tointeger(L, -1)); + enabledFeaturesOTC.push_back(feature); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_pushstring(L, "disableFeature"); + lua_gettable(L, -2); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + const auto feature = static_cast(lua_tointeger(L, -1)); + disabledFeaturesOTC.push_back(feature); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_pop(L, 1); +} +OTCFeatures ConfigManager::getEnabledFeaturesOTC() const { + return enabledFeaturesOTC; +} + +OTCFeatures ConfigManager::getDisabledFeaturesOTC() const { + return disabledFeaturesOTC; +} diff --git a/src/config/configmanager.hpp b/src/config/configmanager.hpp index 51c23dfe0dc..7a31a4a5f72 100644 --- a/src/config/configmanager.hpp +++ b/src/config/configmanager.hpp @@ -12,6 +12,7 @@ #include "config_enums.hpp" using ConfigValue = std::variant; +using OTCFeatures = std::vector; class ConfigManager { public: @@ -40,6 +41,8 @@ class ConfigManager { [[nodiscard]] int32_t getNumber(const ConfigKey_t &key, const std::source_location &location = std::source_location::current()) const; [[nodiscard]] bool getBoolean(const ConfigKey_t &key, const std::source_location &location = std::source_location::current()) const; [[nodiscard]] float getFloat(const ConfigKey_t &key, const std::source_location &location = std::source_location::current()) const; + OTCFeatures getEnabledFeaturesOTC() const; + OTCFeatures getDisabledFeaturesOTC() const; private: mutable std::unordered_map m_configString; @@ -55,6 +58,9 @@ class ConfigManager { std::string configFileLua = { "config.lua" }; bool loaded = false; + OTCFeatures enabledFeaturesOTC = {}; + OTCFeatures disabledFeaturesOTC = {}; + void loadLuaOTCFeatures(lua_State* L); }; constexpr auto g_configManager = ConfigManager::getInstance; diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt index b48b9c42844..5e71d6113de 100644 --- a/src/creatures/CMakeLists.txt +++ b/src/creatures/CMakeLists.txt @@ -1,6 +1,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE appearance/mounts/mounts.cpp appearance/outfit/outfit.cpp + appearance/attachedeffects/attachedeffects.cpp combat/combat.cpp combat/condition.cpp combat/spells.cpp diff --git a/src/creatures/appearance/attachedeffects/attachedeffects.cpp b/src/creatures/appearance/attachedeffects/attachedeffects.cpp new file mode 100644 index 00000000000..98478010c6a --- /dev/null +++ b/src/creatures/appearance/attachedeffects/attachedeffects.cpp @@ -0,0 +1,123 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#include "creatures/appearance/attachedeffects/attachedeffects.hpp" + +#include "config/configmanager.hpp" +#include "game/game.hpp" +#include "utils/pugicast.hpp" +#include "utils/tools.hpp" + +bool Attachedeffects::reload() { + auras.clear(); + shaders.clear(); + effects.clear(); + wings.clear(); + return loadFromXml(); +} + +bool Attachedeffects::loadFromXml() { + pugi::xml_document doc; + auto folder = g_configManager().getString(CORE_DIRECTORY) + "/XML/attachedeffects.xml"; + pugi::xml_parse_result result = doc.load_file(folder.c_str()); + if (!result) { + printXMLError(__FUNCTION__, folder, result); + return false; + } + + for (auto auraNode : doc.child("attachedeffects").children("aura")) { + auras.push_back(std::make_shared( + pugi::cast(auraNode.attribute("id").value()), + auraNode.attribute("name").as_string() + )); + } + + for (auto shaderNode : doc.child("attachedeffects").children("shader")) { + shaders.push_back(std::make_shared( + pugi::cast(shaderNode.attribute("id").value()), + shaderNode.attribute("name").as_string() + )); + } + + for (auto effectNode : doc.child("attachedeffects").children("effect")) { + effects.push_back(std::make_shared( + pugi::cast(effectNode.attribute("id").value()), + effectNode.attribute("name").as_string() + )); + } + + for (auto wingNode : doc.child("attachedeffects").children("wing")) { + wings.push_back(std::make_shared( + pugi::cast(wingNode.attribute("id").value()), + wingNode.attribute("name").as_string() + )); + } + + return true; +} + +std::shared_ptr Attachedeffects::getAuraByID(uint8_t id) { + auto it = std::ranges::find_if(auras.begin(), auras.end(), [id](const std::shared_ptr &aura) { + return aura->id == id; + }); + return it != auras.end() ? *it : nullptr; +} + +std::shared_ptr Attachedeffects::getEffectByID(uint8_t id) { + auto it = std::ranges::find_if(effects.begin(), effects.end(), [id](const std::shared_ptr &effect) { + return effect->id == id; + }); + return it != effects.end() ? *it : nullptr; +} + +std::shared_ptr Attachedeffects::getWingByID(uint8_t id) { + auto it = std::ranges::find_if(wings.begin(), wings.end(), [id](const std::shared_ptr &wing) { + return wing->id == id; + }); + return it != wings.end() ? *it : nullptr; +} + +std::shared_ptr Attachedeffects::getShaderByID(uint8_t id) { + auto it = std::ranges::find_if(shaders.begin(), shaders.end(), [id](const std::shared_ptr &shader) { + return shader->id == id; + }); + return it != shaders.end() ? *it : nullptr; +} + +std::shared_ptr Attachedeffects::getAuraByName(const std::string &name) { + auto auraName = name.c_str(); + auto it = std::ranges::find_if(auras.begin(), auras.end(), [auraName](const std::shared_ptr &aura) { + return strcasecmp(auraName, aura->name.c_str()) == 0; + }); + return it != auras.end() ? *it : nullptr; +} + +std::shared_ptr Attachedeffects::getShaderByName(const std::string &name) { + auto shaderName = name.c_str(); + auto it = std::ranges::find_if(shaders.begin(), shaders.end(), [shaderName](const std::shared_ptr &shader) { + return strcasecmp(shaderName, shader->name.c_str()) == 0; + }); + return it != shaders.end() ? *it : nullptr; +} + +std::shared_ptr Attachedeffects::getEffectByName(const std::string &name) { + auto effectName = name.c_str(); + auto it = std::ranges::find_if(effects.begin(), effects.end(), [effectName](const std::shared_ptr &effect) { + return strcasecmp(effectName, effect->name.c_str()) == 0; + }); + return it != effects.end() ? *it : nullptr; +} + +std::shared_ptr Attachedeffects::getWingByName(const std::string &name) { + auto wingName = name.c_str(); + auto it = std::ranges::find_if(wings.begin(), wings.end(), [wingName](const std::shared_ptr &wing) { + return strcasecmp(wingName, wing->name.c_str()) == 0; + }); + return it != wings.end() ? *it : nullptr; +} diff --git a/src/creatures/appearance/attachedeffects/attachedeffects.hpp b/src/creatures/appearance/attachedeffects/attachedeffects.hpp new file mode 100644 index 00000000000..3cca7c1831c --- /dev/null +++ b/src/creatures/appearance/attachedeffects/attachedeffects.hpp @@ -0,0 +1,73 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#pragma once + +struct Aura { + Aura(uint16_t initId, const std::string &name) : + id(initId), name(name) { } + uint16_t id; + std::string name; +}; + +struct Shader { + Shader(uint8_t initId, const std::string &name) : + id(initId), name(name) { } + uint8_t id; + std::string name; +}; + +struct Effect { + Effect(uint16_t initId, const std::string &name) : + id(initId), name(name) { } + uint16_t id; + std::string name; +}; + +struct Wing { + Wing(uint16_t initId, const std::string &name) : + id(initId), name(name) { } + uint16_t id; + std::string name; +}; + +class Attachedeffects { +public: + bool reload(); + bool loadFromXml(); + + std::shared_ptr getAuraByID(uint8_t id); + std::shared_ptr getEffectByID(uint8_t id); + std::shared_ptr getWingByID(uint8_t id); + std::shared_ptr getShaderByID(uint8_t id); + + std::shared_ptr getAuraByName(const std::string &name); + std::shared_ptr getShaderByName(const std::string &name); + std::shared_ptr getEffectByName(const std::string &name); + std::shared_ptr getWingByName(const std::string &name); + + [[nodiscard]] const std::vector> &getAuras() const { + return auras; + } + [[nodiscard]] const std::vector> &getShaders() const { + return shaders; + } + [[nodiscard]] const std::vector> &getEffects() const { + return effects; + } + [[nodiscard]] const std::vector> &getWings() const { + return wings; + } + +private: + std::vector> auras; + std::vector> shaders; + std::vector> effects; + std::vector> wings; +}; diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 78294e7011d..5cc33aecdea 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -1886,3 +1886,21 @@ void Creature::safeCall(std::function &&action) const { action(); } } + +void Creature::attachEffectById(uint16_t id) { + auto it = std::ranges::find(attachedEffectList, id); + if (it != attachedEffectList.end()) { + return; + } + attachedEffectList.push_back(id); + g_game().sendAttachedEffect(static_self_cast(), id); +} + +void Creature::detachEffectById(uint16_t id) { + auto it = std::ranges::find(attachedEffectList, id); + if (it == attachedEffectList.end()) { + return; + } + attachedEffectList.erase(it); + g_game().sendDetachEffect(static_self_cast(), id); +} diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index 2f33dbdfea7..900991eec6f 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -699,6 +699,17 @@ class Creature : virtual public Thing, public SharedObject { void setCharmChanceModifier(int8_t value) { charmChanceModifier = value; } + std::string getShader() const { + return shader; + } + void setShader(const std::string_view shaderName) { + shader = shaderName; + } + void attachEffectById(uint16_t id); + void detachEffectById(uint16_t id); + std::vector getAttachedEffectList() const { + return attachedEffectList; + } protected: enum FlagAsyncClass_t : uint8_t { @@ -766,6 +777,10 @@ class Creature : virtual public Thing, public SharedObject { Outfit_t currentOutfit; Outfit_t defaultOutfit; + uint16_t currentWing; + uint16_t currentAura; + uint16_t currentEffect; + uint16_t currentShader; Position lastPosition; LightInfo internalLight; @@ -878,4 +893,6 @@ class Creature : virtual public Thing, public SharedObject { } uint8_t m_flagAsyncTask = 0; + std::vector attachedEffectList; + std::string shader; }; diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp index 017a8db7c62..857eeed67fc 100644 --- a/src/creatures/creatures_definitions.hpp +++ b/src/creatures/creatures_definitions.hpp @@ -1670,6 +1670,10 @@ struct Outfit_t { uint8_t lookMountLegs = 0; uint8_t lookMountFeet = 0; uint16_t lookFamiliarsType = 0; + uint16_t lookWing = 0; + uint16_t lookAura = 0; + uint16_t lookEffect = 0; + uint16_t lookShader = 0; }; struct voiceBlock_t { diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index d55cb8f5022..48e094bdc20 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -13,6 +13,7 @@ #include "config/configmanager.hpp" #include "core.hpp" #include "creatures/appearance/mounts/mounts.hpp" +#include "creatures/appearance/attachedeffects/attachedeffects.hpp" #include "creatures/combat/combat.hpp" #include "creatures/combat/condition.hpp" #include "creatures/interactions/chat.hpp" @@ -1055,6 +1056,14 @@ void Player::addStorageValue(const uint32_t key, const int32_t value, const bool } if (IS_IN_KEYRANGE(key, MOUNTS_RANGE)) { // do nothing + } else if (IS_IN_KEYRANGE(key, WING_RANGE)) { + // do nothing + } else if (IS_IN_KEYRANGE(key, EFFECT_RANGE)) { + // do nothing + } else if (IS_IN_KEYRANGE(key, AURA_RANGE)) { + // do nothing + } else if (IS_IN_KEYRANGE(key, SHADER_RANGE)) { + // do nothing } else if (IS_IN_KEYRANGE(key, FAMILIARS_RANGE)) { familiars.emplace_back( value >> 16 @@ -7168,6 +7177,806 @@ void Player::dismount() { defaultOutfit.lookMount = 0; } +// Wings + +uint8_t Player::getLastWing() const { + const int32_t value = getStorageValue(PSTRG_WING_CURRENTWING); + if (value > 0) { + return value; + } + const auto lastWing = kv()->get("last-wing"); + if (!lastWing.has_value()) { + return 0; + } + + return static_cast(lastWing->get()); +} + +uint8_t Player::getCurrentWing() const { + const int32_t value = getStorageValue(PSTRG_WING_CURRENTWING); + if (value > 0) { + return value; + } + return 0; +} + +void Player::setCurrentWing(uint8_t wing) { + addStorageValue(PSTRG_WING_CURRENTWING, wing); +} + +bool Player::hasAnyWing() const { + const auto &wings = g_game().attachedeffects->getWings(); + return std::ranges::any_of(wings, [&](const auto &wing) { + return hasWing(wing); + }); +} + +uint8_t Player::getRandomWingId() const { + std::vector availableWings; + const auto &wings = g_game().attachedeffects->getWings(); + for (const auto &wing : wings) { + if (hasWing(wing)) { + availableWings.emplace_back(wing->id); + } + } + + if (availableWings.empty()) { + return 0; + } + + const auto randomIndex = uniform_random(0, static_cast(availableWings.size() - 1)); + if (randomIndex >= 0 && static_cast(randomIndex) < availableWings.size()) { + return availableWings[randomIndex]; + } + + return 0; +} + +bool Player::toggleWing(bool wing) { + if ((OTSYS_TIME() - lastToggleWing) < 3000 && !wasWinged) { + sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return false; + } + + if (wing) { + if (isWinged()) { + return false; + } + + const auto &playerOutfit = Outfits::getInstance().getOutfitByLookType(getPlayer(), defaultOutfit.lookType); + if (!playerOutfit) { + return false; + } + + uint8_t currentWingId = getLastWing(); + if (currentWingId == 0) { + sendOutfitWindow(); + return false; + } + + if (isRandomMounted()) { + currentWingId = getRandomWingId(); + } + + const auto ¤tWing = g_game().attachedeffects->getWingByID(currentWingId); + if (!currentWing) { + return false; + } + + if (!hasWing(currentWing)) { + setCurrentWing(0); + kv()->set("last-wing", 0); + sendOutfitWindow(); + return false; + } + + if (hasCondition(CONDITION_OUTFIT)) { + sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return false; + } + + defaultOutfit.lookWing = currentWing->id; + setCurrentWing(currentWing->id); + kv()->set("last-wing", currentWing->id); + + } else { + if (!isWinged()) { + return false; + } + + diswing(); + } + + g_game().internalCreatureChangeOutfit(static_self_cast(), defaultOutfit); + lastToggleWing = OTSYS_TIME(); + return true; +} + +bool Player::tameWing(uint8_t wingId) { + const auto &wingPtr = g_game().attachedeffects->getWingByID(wingId); + if (!wingPtr) { + return false; + } + + const uint8_t tmpWingId = wingId - 1; + const uint32_t key = PSTRG_WING_RANGE_START + (tmpWingId / 31); + + int32_t value = getStorageValue(key); + if (value != -1) { + value |= (1 << (tmpWingId % 31)); + } else { + value = (1 << (tmpWingId % 31)); + } + + addStorageValue(key, value); + return true; +} + +bool Player::untameWing(uint8_t wingId) { + const auto &wingPtr = g_game().attachedeffects->getWingByID(wingId); + if (!wingPtr) { + return false; + } + + const uint8_t tmpWingId = wingId - 1; + const uint32_t key = PSTRG_WING_RANGE_START + (tmpWingId / 31); + + int32_t value = getStorageValue(key); + if (value == -1) { + return true; + } + + value &= ~(1 << (tmpWingId % 31)); + addStorageValue(key, value); + + if (getCurrentWing() == wingId) { + if (isWinged()) { + diswing(); + g_game().internalCreatureChangeOutfit(static_self_cast(), defaultOutfit); + } + + setCurrentWing(0); + kv()->set("last-wing", 0); + } + + return true; +} + +bool Player::hasWing(const std::shared_ptr &wing) const { + if (isAccessPlayer()) { + return true; + } + + const uint8_t tmpWingId = wing->id - 1; + + const int32_t value = getStorageValue(PSTRG_WING_RANGE_START + (tmpWingId / 31)); + if (value == -1) { + return false; + } + + return ((1 << (tmpWingId % 31)) & value) != 0; +} + +void Player::diswing() { + defaultOutfit.lookWing = 0; +} + +// Auras + +uint8_t Player::getLastAura() const { + const int32_t value = getStorageValue(PSTRG_AURA_CURRENTAURA); + if (value > 0) { + return value; + } + const auto lastAura = kv()->get("last-aura"); + if (!lastAura.has_value()) { + return 0; + } + + return static_cast(lastAura->get()); +} + +uint8_t Player::getCurrentAura() const { + const int32_t value = getStorageValue(PSTRG_AURA_CURRENTAURA); + if (value > 0) { + return value; + } + return 0; +} + +void Player::setCurrentAura(uint8_t aura) { + addStorageValue(PSTRG_AURA_CURRENTAURA, aura); +} + +bool Player::hasAnyAura() const { + const auto &auras = g_game().attachedeffects->getAuras(); + return std::ranges::any_of(auras, [&](const auto &aura) { + return hasAura(aura); + }); +} + +uint8_t Player::getRandomAuraId() const { + std::vector playerAuras; + const auto &auras = g_game().attachedeffects->getAuras(); + for (const auto &aura : auras) { + if (hasAura(aura)) { + playerAuras.emplace_back(aura->id); + } + } + + if (playerAuras.empty()) { + return 0; + } + + const auto randomIndex = uniform_random(0, static_cast(playerAuras.size() - 1)); + if (randomIndex >= 0 && static_cast(randomIndex) < playerAuras.size()) { + return playerAuras[randomIndex]; + } + + return 0; +} + +bool Player::toggleAura(bool aura) { + if ((OTSYS_TIME() - lastToggleAura) < 3000 && !wasAuraed) { + sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return false; + } + + if (aura) { + if (isAuraed()) { + return false; + } + + const auto &playerOutfit = Outfits::getInstance().getOutfitByLookType(getPlayer(), defaultOutfit.lookType); + if (!playerOutfit) { + return false; + } + + uint8_t currentAuraId = getLastAura(); + if (currentAuraId == 0) { + sendOutfitWindow(); + return false; + } + + if (isRandomMounted()) { + currentAuraId = getRandomAuraId(); + } + + const auto ¤tAura = g_game().attachedeffects->getAuraByID(currentAuraId); + if (!currentAura) { + return false; + } + + if (!hasAura(currentAura)) { + setCurrentAura(0); + kv()->set("last-aura", 0); + sendOutfitWindow(); + return false; + } + + if (hasCondition(CONDITION_OUTFIT)) { + sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return false; + } + + defaultOutfit.lookAura = currentAura->id; + setCurrentAura(currentAura->id); + kv()->set("last-aura", currentAura->id); + + } else { + if (!isAuraed()) { + return false; + } + + disaura(); + } + + g_game().internalCreatureChangeOutfit(static_self_cast(), defaultOutfit); + lastToggleAura = OTSYS_TIME(); + return true; +} + +bool Player::tameAura(uint8_t auraId) { + const auto &auraPtr = g_game().attachedeffects->getAuraByID(auraId); + if (!auraPtr) { + return false; + } + + const uint8_t tmpAuraId = auraId - 1; + const uint32_t key = PSTRG_AURA_RANGE_START + (tmpAuraId / 31); + + int32_t value = getStorageValue(key); + if (value != -1) { + value |= (1 << (tmpAuraId % 31)); + } else { + value = (1 << (tmpAuraId % 31)); + } + + addStorageValue(key, value); + return true; +} + +bool Player::untameAura(uint8_t auraId) { + const auto &auraPtr = g_game().attachedeffects->getAuraByID(auraId); + if (!auraPtr) { + return false; + } + + const uint8_t tmpAuraId = auraId - 1; + const uint32_t key = PSTRG_AURA_RANGE_START + (tmpAuraId / 31); + + int32_t value = getStorageValue(key); + if (value == -1) { + return true; + } + + value &= ~(1 << (tmpAuraId % 31)); + addStorageValue(key, value); + + if (getCurrentAura() == auraId) { + if (isAuraed()) { + disaura(); + g_game().internalCreatureChangeOutfit(static_self_cast(), defaultOutfit); + } + + setCurrentAura(0); + kv()->set("last-aura", 0); + } + + return true; +} + +bool Player::hasAura(const std::shared_ptr &aura) const { + if (isAccessPlayer()) { + return true; + } + const uint8_t tmpAuraId = aura->id - 1; + + const int32_t value = getStorageValue(PSTRG_AURA_RANGE_START + (tmpAuraId / 31)); + if (value == -1) { + return false; + } + + return ((1 << (tmpAuraId % 31)) & value) != 0; +} + +void Player::disaura() { + defaultOutfit.lookAura = 0; +} + +// Effects + +uint8_t Player::getLastEffect() const { + const int32_t value = getStorageValue(PSTRG_EFFECT_CURRENTEFFECT); + if (value > 0) { + return value; + } + + const auto lastEffect = kv()->get("last-effect"); + if (!lastEffect.has_value()) { + return 0; + } + + return static_cast(lastEffect->get()); +} + +uint8_t Player::getCurrentEffect() const { + const int32_t value = getStorageValue(PSTRG_EFFECT_CURRENTEFFECT); + if (value > 0) { + return value; + } + return 0; +} + +void Player::setCurrentEffect(uint8_t effect) { + addStorageValue(PSTRG_EFFECT_CURRENTEFFECT, effect); +} + +bool Player::hasAnyEffect() const { + const auto &effects = g_game().attachedeffects->getEffects(); + return std::ranges::any_of(effects, [&](const auto &effect) { + return hasEffect(effect); + }); +} + +uint8_t Player::getRandomEffectId() const { + std::vector playerEffects; + const auto &effects = g_game().attachedeffects->getEffects(); + for (const auto &effect : effects) { + if (hasEffect(effect)) { + playerEffects.emplace_back(effect->id); + } + } + + if (playerEffects.empty()) { + return 0; + } + + const auto randomIndex = uniform_random(0, static_cast(playerEffects.size() - 1)); + if (randomIndex >= 0 && static_cast(randomIndex) < playerEffects.size()) { + return playerEffects[randomIndex]; + } + + return 0; +} + +bool Player::toggleEffect(bool effect) { + if ((OTSYS_TIME() - lastToggleEffect) < 3000 && !wasEffected) { + sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return false; + } + + if (effect) { + if (isEffected()) { + return false; + } + + const auto &playerOutfit = Outfits::getInstance().getOutfitByLookType(getPlayer(), defaultOutfit.lookType); + if (!playerOutfit) { + return false; + } + + uint8_t currentEffectId = getLastEffect(); + if (currentEffectId == 0) { + sendOutfitWindow(); + return false; + } + + if (isRandomMounted()) { + currentEffectId = getRandomEffectId(); + } + + const auto ¤tEffect = g_game().attachedeffects->getEffectByID(currentEffectId); + if (!currentEffect) { + return false; + } + + if (!hasEffect(currentEffect)) { + setCurrentEffect(0); + kv()->set("last-effect", 0); + sendOutfitWindow(); + return false; + } + + if (hasCondition(CONDITION_OUTFIT)) { + sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return false; + } + + defaultOutfit.lookEffect = currentEffect->id; + setCurrentEffect(currentEffect->id); + kv()->set("last-effect", currentEffect->id); + + } else { + if (!isEffected()) { + return false; + } + + diseffect(); + } + + g_game().internalCreatureChangeOutfit(static_self_cast(), defaultOutfit); + lastToggleEffect = OTSYS_TIME(); + return true; +} + +bool Player::tameEffect(uint8_t effectId) { + const auto &effectPtr = g_game().attachedeffects->getEffectByID(effectId); + if (!effectPtr) { + return false; + } + + const uint8_t tmpEffectId = effectId - 1; + const uint32_t key = PSTRG_EFFECT_RANGE_START + (tmpEffectId / 31); + + int32_t value = getStorageValue(key); + if (value != -1) { + value |= (1 << (tmpEffectId % 31)); + } else { + value = (1 << (tmpEffectId % 31)); + } + + addStorageValue(key, value); + return true; +} + +bool Player::untameEffect(uint8_t effectId) { + const auto &effectPtr = g_game().attachedeffects->getEffectByID(effectId); + if (!effectPtr) { + return false; + } + + const uint8_t tmpEffectId = effectId - 1; + const uint32_t key = PSTRG_EFFECT_RANGE_START + (tmpEffectId / 31); + + int32_t value = getStorageValue(key); + if (value == -1) { + return true; + } + + value &= ~(1 << (tmpEffectId % 31)); + addStorageValue(key, value); + + if (getCurrentEffect() == effectId) { + if (isEffected()) { + diseffect(); + g_game().internalCreatureChangeOutfit(static_self_cast(), defaultOutfit); + } + + setCurrentEffect(0); + kv()->set("last-effect", 0); + } + + return true; +} + +bool Player::hasEffect(const std::shared_ptr &effect) const { + if (isAccessPlayer()) { + return true; + } + + const uint8_t tmpEffectId = effect->id - 1; + + const int32_t value = getStorageValue(PSTRG_EFFECT_RANGE_START + (tmpEffectId / 31)); + if (value == -1) { + return false; + } + + return ((1 << (tmpEffectId % 31)) & value) != 0; +} + +void Player::diseffect() { + defaultOutfit.lookEffect = 0; +} + +// Shaders +uint16_t Player::getRandomShader() const { + std::vector shadersId; + for (const auto &shader : g_game().attachedeffects->getShaders()) { + if (hasShader(shader.get())) { + shadersId.push_back(shader->id); + } + } + + if (shadersId.empty()) { + return 0; + } + + return shadersId[uniform_random(0, shadersId.size() - 1)]; +} + +uint16_t Player::getCurrentShader() const { + return currentShader; +} + +void Player::setCurrentShader(uint16_t shaderId) { + currentShader = shaderId; +} + +bool Player::toggleShader(bool shader) { + if ((OTSYS_TIME() - lastToggleShader) < 3000 && !wasShadered) { + sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return false; + } + + if (shader) { + if (isShadered()) { + return false; + } + + const auto &playerOutfit = Outfits::getInstance().getOutfitByLookType(getPlayer(), defaultOutfit.lookType); + if (!playerOutfit) { + return false; + } + + uint16_t currentShaderId = getCurrentShader(); + if (currentShaderId == 0) { + sendOutfitWindow(); + return false; + } + + auto currentShaderPtr = g_game().attachedeffects->getShaderByID(currentShaderId); + if (!currentShaderPtr) { + return false; + } + + Shader* currentShader = currentShaderPtr.get(); + + if (!hasShader(currentShader)) { + setCurrentShader(0); + sendOutfitWindow(); + return false; + } + + if (hasCondition(CONDITION_OUTFIT)) { + sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return false; + } + + defaultOutfit.lookShader = currentShader->id; + + } else { + if (!isShadered()) { + return false; + } + + disshader(); + } + + g_game().internalCreatureChangeOutfit(static_self_cast(), defaultOutfit); + lastToggleShader = OTSYS_TIME(); + return true; +} + +bool Player::tameShader(uint16_t shaderId) { + auto shaderPtr = g_game().attachedeffects->getShaderByID(shaderId); + if (!shaderPtr) { + return false; + } + + if (hasShader(shaderPtr.get())) { + return false; + } + + shaders.insert(shaderId); + return true; +} + +bool Player::untameShader(uint16_t shaderId) { + const auto &shaderPtr = g_game().attachedeffects->getShaderByID(shaderId); + if (!shaderPtr) { + return false; + } + + if (!hasShader(shaderPtr.get())) { + return false; + } + + shaders.erase(shaderId); + + if (getCurrentShader() == shaderId) { + if (isShadered()) { + disshader(); + g_game().internalCreatureChangeOutfit(static_self_cast(), defaultOutfit); + } + + setCurrentShader(0); + } + + return true; +} + +bool Player::hasShader(const Shader* shader) const { + if (isAccessPlayer()) { + return true; + } + + return shaders.find(shader->id) != shaders.end(); +} + +bool Player::hasShaders() const { + for (const auto &shader : g_game().attachedeffects->getShaders()) { + if (hasShader(shader.get())) { + return true; + } + } + return false; +} + +void Player::disshader() { + defaultOutfit.lookShader = 0; +} + +std::string Player::getCurrentShader_NAME() const { + uint16_t currentShaderId = getCurrentShader(); + const auto ¤tShader = g_game().attachedeffects->getShaderByID(static_cast(currentShaderId)); + + if (currentShader != nullptr) { + return currentShader->name; + } else { + return "Outfit - Default"; + } +} + +bool Player::addCustomOutfit(const std::string &type, const std::variant &idOrName) { + // test proposal + + uint16_t elementId; + if (std::holds_alternative(idOrName)) { + elementId = std::get(idOrName); + } else { + const std::string &name = std::get(idOrName); + + if (type == "wing") { + const auto &element = g_game().attachedeffects->getWingByName(name); + if (!element) { + return false; + } + elementId = element->id; + } else if (type == "aura") { + const auto &element = g_game().attachedeffects->getAuraByName(name); + if (!element) { + return false; + } + elementId = element->id; + } else if (type == "effect") { + const auto &element = g_game().attachedeffects->getEffectByName(name); + if (!element) { + return false; + } + elementId = element->id; + } else if (type == "shader") { + const auto &element = g_game().attachedeffects->getShaderByName(name); + if (!element) { + return false; + } + elementId = element->id; + } else { + return false; + } + } + + if (type == "wing") { + return tameWing(elementId); + } else if (type == "aura") { + return tameAura(elementId); + } else if (type == "effect") { + return tameEffect(elementId); + } else if (type == "shader") { + return tameShader(elementId); + } + return false; +} + +bool Player::removeCustomOutfit(const std::string &type, const std::variant &idOrName) { + // test proposal + uint16_t elementId; + if (std::holds_alternative(idOrName)) { + elementId = std::get(idOrName); + } else { + const std::string &name = std::get(idOrName); + + if (type == "wings") { + const auto &element = g_game().attachedeffects->getWingByName(name); + if (!element) { + return false; + } + elementId = element->id; + } else if (type == "aura") { + const auto &element = g_game().attachedeffects->getAuraByName(name); + if (!element) { + return false; + } + elementId = element->id; + } else if (type == "effect") { + const auto &element = g_game().attachedeffects->getEffectByName(name); + if (!element) { + return false; + } + elementId = element->id; + } else if (type == "shader") { + const auto &element = g_game().attachedeffects->getShaderByName(name); + if (!element) { + return false; + } + elementId = element->id; + } else { + return false; + } + } + + if (type == "wing") { + return untameWing(elementId); + } else if (type == "aura") { + return untameAura(elementId); + } else if (type == "effect") { + return untameEffect(elementId); + } else if (type == "shader") { + return untameShader(elementId); + } + return false; +} + bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries) { if (tries == 0 || skill == SKILL_LEVEL) { return false; @@ -9730,6 +10539,48 @@ void Player::sendLootContainers() const { } } +// OTCR Features + +void Player::sendAttachedEffect(const std::shared_ptr &creature, uint16_t effectId) const { + if (!client || !creature) { + return; + } + + client->sendAttachedEffect(creature, effectId); +} + +void Player::sendDetachEffect(const std::shared_ptr &creature, uint16_t effectId) const { + if (!client || !creature) { + return; + } + + client->sendDetachEffect(creature, effectId); +} + +void Player::sendShader(const std::shared_ptr &creature, const std::string &shaderName) const { + if (!client || !creature) { + return; + } + + client->sendShader(creature, shaderName); +} + +void Player::sendMapShader(const std::string &shaderName) const { + if (!client) { + return; + } + + client->sendMapShader(shaderName); +} + +void Player::sendPlayerTyping(const std::shared_ptr &creature, uint8_t typing) const { + if (!client) { + return; + } + + client->sendPlayerTyping(creature, typing); +} + void Player::sendSingleSoundEffect(const Position &pos, SoundEffect_t id, SourceEffect_t source) const { if (client) { client->sendSingleSoundEffect(pos, id, source); diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 28ef63ec8ef..6144d333ddf 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -50,11 +50,16 @@ class Container; class KV; class BedItem; class Npc; +class Attachedeffects; struct ModalWindow; struct Achievement; struct VIPGroup; struct Mount; +struct Wing; +struct Effect; +struct Shader; +struct Aura; struct OutfitEntry; struct Outfit; struct FamiliarEntry; @@ -200,6 +205,70 @@ class Player final : public Creature, public Cylinder, public Bankable { bool hasAnyMount() const; uint8_t getRandomMountId() const; void dismount(); + + // -- @ wings + uint8_t getLastWing() const; + uint8_t getCurrentWing() const; + void setCurrentWing(uint8_t wingId); + bool isWinged() const { + return defaultOutfit.lookWing != 0; + } + bool toggleWing(bool wing); + bool tameWing(uint8_t wingId); + bool untameWing(uint8_t wingId); + bool hasWing(const std::shared_ptr &wing) const; + bool hasAnyWing() const; + uint8_t getRandomWingId() const; + void diswing(); + + // -- @ + // -- @ Auras + uint8_t getLastAura() const; + uint8_t getCurrentAura() const; + void setCurrentAura(uint8_t auraId); + bool isAuraed() const { + return defaultOutfit.lookAura != 0; + } + bool toggleAura(bool aura); + bool tameAura(uint8_t auraId); + bool untameAura(uint8_t auraId); + bool hasAura(const std::shared_ptr &aura) const; + bool hasAnyAura() const; + uint8_t getRandomAuraId() const; + void disaura(); + // -- @ + // -- @ Effect + uint8_t getLastEffect() const; + uint8_t getCurrentEffect() const; + void setCurrentEffect(uint8_t effectId); + bool isEffected() const { + return defaultOutfit.lookEffect != 0; + } + bool toggleEffect(bool effect); + bool tameEffect(uint8_t effectId); + bool untameEffect(uint8_t effectId); + bool hasEffect(const std::shared_ptr &effect) const; + bool hasAnyEffect() const; + uint8_t getRandomEffectId() const; + void diseffect(); + // -- @ + // -- @ Shader + uint16_t getRandomShader() const; + uint16_t getCurrentShader() const; + void setCurrentShader(uint16_t shaderId); + bool isShadered() const { + return defaultOutfit.lookShader != 0; + } + bool toggleShader(bool shader); + bool tameShader(uint16_t shaderId); + bool untameShader(uint16_t shaderId); + bool hasShader(const Shader* shader) const; + bool hasShaders() const; + void disshader(); + std::string getCurrentShader_NAME() const; + bool addCustomOutfit(const std::string &type, const std::variant &idOrName); + bool removeCustomOutfit(const std::string &type, const std::variant &idOrName); + uint16_t getDodgeChance() const; uint8_t isRandomMounted() const; @@ -1296,6 +1365,18 @@ class Player final : public Creature, public Cylinder, public Bankable { uint16_t getPlayerVocationEnum() const; + void sendAttachedEffect(const std::shared_ptr &creature, uint16_t effectId) const; + void sendDetachEffect(const std::shared_ptr &creature, uint16_t effectId) const; + void sendShader(const std::shared_ptr &creature, const std::string &shaderName) const; + void sendMapShader(const std::string &shaderName) const; + const std::string &getMapShader() const { + return mapShader; + } + void setMapShader(const std::string_view shaderName) { + this->mapShader = shaderName; + } + void sendPlayerTyping(const std::shared_ptr &creature, uint8_t typing) const; + private: friend class PlayerLock; std::mutex mutex; @@ -1386,6 +1467,7 @@ class Player final : public Creature, public Cylinder, public Bankable { std::vector quickLootListItemIds; std::vector outfits; + std::unordered_set shaders; std::vector familiars; std::vector> preys; @@ -1405,6 +1487,7 @@ class Player final : public Creature, public Cylinder, public Bankable { std::string name; std::string guildNick; std::string loyaltyTitle; + std::string mapShader; Skill skills[SKILL_LAST + 1]; LightInfo itemsLight; @@ -1437,6 +1520,10 @@ class Player final : public Creature, public Cylinder, public Bankable { int64_t lastPing; int64_t lastPong; int64_t nextAction = 0; + int64_t lastToggleWing = 0; + int64_t lastToggleEffect = 0; + int64_t lastToggleAura = 0; + int64_t lastToggleShader = 0; int64_t nextPotionAction = 0; int64_t nextNecklaceAction = 0; int64_t nextRingAction = 0; @@ -1582,6 +1669,14 @@ class Player final : public Creature, public Cylinder, public Bankable { bool moved = false; bool m_isDead = false; bool imbuementTrackerWindowOpen = false; + bool wasWinged = false; + bool wasAuraed = false; + bool wasEffected = false; + bool wasShadered = false; + bool randomizeWing = false; + bool randomizeAura = false; + bool randomizeEffect = false; + bool randomizeShader = false; bool shouldForceLogout = true; bool connProtected = false; diff --git a/src/game/game.cpp b/src/game/game.cpp index d755c05bb9e..4b5396d6303 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -11,6 +11,7 @@ #include "config/configmanager.hpp" #include "creatures/appearance/mounts/mounts.hpp" +#include "creatures/appearance/attachedeffects/attachedeffects.hpp" #include "creatures/combat/condition.hpp" #include "creatures/combat/spells.hpp" #include "creatures/creature.hpp" @@ -218,6 +219,7 @@ Game::Game() { wildcardTree = std::make_shared(false); mounts = std::make_unique(); + attachedeffects = std::make_unique(); using enum CyclopediaBadge_t; using enum CyclopediaTitle_t; @@ -627,6 +629,7 @@ void Game::setGameState(GameState_t newState) { raids.startup(); mounts->loadFromXml(); + attachedeffects->loadFromXml(); loadMotdNum(); loadPlayersRecord(); @@ -6203,6 +6206,82 @@ void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit, uint8_t isMoun internalCreatureChangeOutfit(player, outfit); } + + // @ wings + if (outfit.lookWing != 0) { + const auto &wing = attachedeffects->getWingByID(outfit.lookWing); + if (!wing) { + return; + } + + player->detachEffectById(player->getCurrentWing()); + player->setCurrentWing(wing->id); + player->attachEffectById(wing->id); + } else { + if (player->isWinged()) { + player->diswing(); + } + player->detachEffectById(player->getCurrentWing()); + player->wasWinged = false; + } + // @ + // @ Effect + if (outfit.lookEffect != 0) { + const auto &effect = attachedeffects->getEffectByID(outfit.lookEffect); + if (!effect) { + return; + } + + player->detachEffectById(player->getCurrentEffect()); + player->setCurrentEffect(effect->id); + player->attachEffectById(effect->id); + } else { + if (player->isEffected()) { + player->diseffect(); + } + player->detachEffectById(player->getCurrentEffect()); + player->wasEffected = false; + } + // @ + // @ Aura + if (outfit.lookAura != 0) { + const auto &aura = attachedeffects->getAuraByID(outfit.lookAura); + if (!aura) { + return; + } + player->detachEffectById(player->getCurrentAura()); + player->setCurrentAura(aura->id); + player->attachEffectById(aura->id); + } else { + if (player->isAuraed()) { + player->disaura(); + } + player->detachEffectById(player->getCurrentAura()); + player->wasAuraed = false; + } + // @ + /// shaders + if (outfit.lookShader != 0) { + const auto &shaderPtr = attachedeffects->getShaderByID(outfit.lookShader); + if (!shaderPtr) { + return; + } + Shader* shader = shaderPtr.get(); + + if (!player->hasShader(shader)) { + return; + } + + player->setCurrentShader(shader->id); + player->sendShader(player, shader->name); + + } else { + if (player->isShadered()) { + player->disshader(); + } + player->sendShader(player, "Outfit - Default"); + player->wasShadered = false; + } } void Game::playerShowQuestLog(uint32_t playerId) { @@ -11006,6 +11085,94 @@ void Game::updatePlayersOnline() const { } } +void Game::sendAttachedEffect(const std::shared_ptr &creature, uint16_t effectId) { + auto spectators = Spectators().find(creature->getPosition(), true); + for (const auto &spectator : spectators) { + const auto &player = spectator->getPlayer(); + if (player) { + player->sendAttachedEffect(creature, effectId); + } + } +} + +void Game::sendDetachEffect(const std::shared_ptr &creature, uint16_t effectId) { + auto spectators = Spectators().find(creature->getPosition(), true); + for (const auto &spectator : spectators) { + const auto &player = spectator->getPlayer(); + if (player) { + player->sendDetachEffect(creature, effectId); + } + } +} + +void Game::updateCreatureShader(const std::shared_ptr &creature) { + auto spectators = Spectators().find(creature->getPosition(), true); + for (const auto &spectator : spectators) { + const auto &player = spectator->getPlayer(); + if (player) { + player->sendShader(creature, creature->getShader()); + } + } +} + +void Game::playerSetTyping(uint32_t playerId, uint8_t typing) { + const auto &player = getPlayerByID(playerId); + if (!player) { + return; + } + for (const auto &spectator : Spectators().find(player->getPosition(), true)) { + if (const auto &tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendPlayerTyping(player, typing); + } + } +} + +void Game::refreshItem(const std::shared_ptr &item) { + if (!item) { + return; + } + const auto &parent = item->getParent(); + if (!parent) { + return; + } + if (const auto &creature = parent->getCreature()) { + if (const auto &player = creature->getPlayer()) { + int32_t index = player->getThingIndex(item); + if (index > -1) { + player->sendInventoryItem(static_cast(index), item); + } + } + return; + } + if (const auto &container = parent->getContainer()) { + int32_t index = container->getThingIndex(item); + if (index > -1 && index <= std::numeric_limits::max()) { + const auto spectators = Spectators().find(container->getPosition(), false, 2, 2, 2, 2); + // send to client + for (const auto &spectator : spectators) { + const auto &player = spectator->getPlayer(); + if (!player) { + continue; + } + player->sendUpdateContainerItem(container, static_cast(index), item); + } + } + return; + } + if (const auto &tile = parent->getTile()) { + const auto spectators = Spectators().find(tile->getPosition(), true); + // send to client + for (const auto &spectator : spectators) { + const auto &player = spectator->getPlayer(); + if (!player) { + continue; + } + player->sendUpdateTileItem(tile, tile->getPosition(), item); + } + return; + } +} + void Game::playerCyclopediaHousesByTown(uint32_t playerId, const std::string &townName) { std::shared_ptr player = getPlayerByID(playerId); if (!player) { diff --git a/src/game/game.hpp b/src/game/game.hpp index 9a4b59f8a55..d929cbc2b10 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -38,6 +38,7 @@ class IOWheel; class ItemClassification; class Guild; class Mounts; +class Attachedeffects; class Spectators; class Player; class Account; @@ -567,6 +568,7 @@ class Game { Map map; std::unique_ptr mounts; [[no_unique_address]] Outfits outfits; + std::unique_ptr attachedeffects; Raids raids; std::unique_ptr m_appearancesPtr; @@ -719,6 +721,11 @@ class Game { const std::map &getBlessingNames(); const std::unordered_map &getHirelingSkills(); const std::unordered_map &getHirelingOutfits(); + void sendAttachedEffect(const std::shared_ptr &creature, uint16_t effectId); + void sendDetachEffect(const std::shared_ptr &creature, uint16_t effectId); + void updateCreatureShader(const std::shared_ptr &creature); + void playerSetTyping(uint32_t playerId, uint8_t typing); + void refreshItem(const std::shared_ptr &item); private: std::map m_achievements; diff --git a/src/items/item.cpp b/src/items/item.cpp index 71bcd1c765e..7b5867a15f1 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -3465,3 +3465,21 @@ int32_t ItemProperties::getDuration() const { return getAttribute(ItemAttribute_t::DURATION); } } + +void ItemProperties::setShader(const std::string &shaderName) { + if (shaderName.empty()) { + removeCustomAttribute("shader"); + return; + } + + setCustomAttribute("shader", shaderName); +} + +bool ItemProperties::hasShader() const { + return getCustomAttribute("shader") != nullptr; +} + +std::string ItemProperties::getShader() const { + const CustomAttribute* shader = getCustomAttribute("shader"); + return shader ? shader->getString() : ""; +} diff --git a/src/items/item.hpp b/src/items/item.hpp index d199fab15cf..de7222875ef 100644 --- a/src/items/item.hpp +++ b/src/items/item.hpp @@ -141,6 +141,12 @@ class ItemProperties { return getCorpseOwner() == static_cast(std::numeric_limits::max()); } + void setShader(const std::string &shaderName); + + bool hasShader() const; + + std::string getShader() const; + protected: std::unique_ptr &initAttributePtr() { if (!attributePtr) { diff --git a/src/lua/functions/creatures/combat/condition_functions.cpp b/src/lua/functions/creatures/combat/condition_functions.cpp index 0f2f9a56759..a7a9e0af2bd 100644 --- a/src/lua/functions/creatures/combat/condition_functions.cpp +++ b/src/lua/functions/creatures/combat/condition_functions.cpp @@ -223,6 +223,10 @@ int ConditionFunctions::luaConditionSetOutfit(lua_State* L) { outfit.lookHead = Lua::getNumber(L, 4); outfit.lookType = Lua::getNumber(L, 3); outfit.lookTypeEx = Lua::getNumber(L, 2); + outfit.lookWing = Lua::getNumber(L, 15); + outfit.lookAura = Lua::getNumber(L, 16); + outfit.lookEffect = Lua::getNumber(L, 17); + outfit.lookShader = Lua::getNumber(L, 18); } const std::shared_ptr &condition = Lua::getUserdataShared(L, 1)->dynamic_self_cast(); diff --git a/src/lua/functions/creatures/creature_functions.cpp b/src/lua/functions/creatures/creature_functions.cpp index 72b41d01928..f152df6918c 100644 --- a/src/lua/functions/creatures/creature_functions.cpp +++ b/src/lua/functions/creatures/creature_functions.cpp @@ -88,6 +88,11 @@ void CreatureFunctions::init(lua_State* L) { Lua::registerMethod(L, "Creature", "getIcons", CreatureFunctions::luaCreatureGetIcons); Lua::registerMethod(L, "Creature", "removeIcon", CreatureFunctions::luaCreatureRemoveIcon); Lua::registerMethod(L, "Creature", "clearIcons", CreatureFunctions::luaCreatureClearIcons); + Lua::registerMethod(L, "Creature", "attachEffectById", CreatureFunctions::luaCreatureAttachEffectById); + Lua::registerMethod(L, "Creature", "detachEffectById", CreatureFunctions::luaCreatureDetachEffectById); + Lua::registerMethod(L, "Creature", "getAttachedEffects", CreatureFunctions::luaCreatureGetAttachedEffects); + Lua::registerMethod(L, "Creature", "getShader", CreatureFunctions::luaCreatureGetShader); + Lua::registerMethod(L, "Creature", "setShader", CreatureFunctions::luaCreatureSetShader); CombatFunctions::init(L); MonsterFunctions::init(L); @@ -1186,3 +1191,75 @@ int CreatureFunctions::luaCreatureClearIcons(lua_State* L) { Lua::pushBoolean(L, true); return 1; } + +int CreatureFunctions::luaCreatureAttachEffectById(lua_State* L) { + // creature:attachEffectById(effectId, [temporary]) + const auto &creature = Lua::getUserdataShared(L, 1); + if (!creature) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + return 1; + } + uint16_t id = Lua::getNumber(L, 2); + bool temp = Lua::getBoolean(L, 3, false); + if (temp) { + g_game().sendAttachedEffect(creature, id); + } else { + creature->attachEffectById(id); + } + return 1; +} + +int CreatureFunctions::luaCreatureDetachEffectById(lua_State* L) { + // creature:detachEffectById(effectId) + const auto &creature = Lua::getUserdataShared(L, 1); + if (!creature) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + return 1; + } + uint16_t id = Lua::getNumber(L, 2); + creature->detachEffectById(id); + return 1; +} + +int CreatureFunctions::luaCreatureGetAttachedEffects(lua_State* L) { + // creature:getAttachedEffects() + const auto &creature = Lua::getUserdataShared(L, 1); + if (!creature) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + return 1; + } + + const auto &effects = creature->getAttachedEffectList(); + lua_createtable(L, effects.size(), 0); + for (size_t i = 0; i < effects.size(); ++i) { + lua_pushnumber(L, effects[i]); + lua_rawseti(L, -2, i + 1); + } + return 1; +} + +int CreatureFunctions::luaCreatureGetShader(lua_State* L) { + // creature:getShader() + const auto &creature = Lua::getUserdataShared(L, 1); + if (!creature) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + return 1; + } + + Lua::pushString(L, creature->getShader()); + + return 1; +} + +int CreatureFunctions::luaCreatureSetShader(lua_State* L) { + // creature:setShader(shaderName) + const auto &creature = Lua::getUserdataShared(L, 1); + if (!creature) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + return 1; + } + creature->setShader(Lua::getString(L, 2)); + g_game().updateCreatureShader(creature); + Lua::pushBoolean(L, true); + return 1; +} diff --git a/src/lua/functions/creatures/creature_functions.hpp b/src/lua/functions/creatures/creature_functions.hpp index 8d1c5b8ce52..cdfde71fbc2 100644 --- a/src/lua/functions/creatures/creature_functions.hpp +++ b/src/lua/functions/creatures/creature_functions.hpp @@ -113,4 +113,9 @@ class CreatureFunctions { static int luaCreatureGetIcon(lua_State* L); static int luaCreatureRemoveIcon(lua_State* L); static int luaCreatureClearIcons(lua_State* L); + static int luaCreatureAttachEffectById(lua_State* L); + static int luaCreatureDetachEffectById(lua_State* L); + static int luaCreatureGetAttachedEffects(lua_State* L); + static int luaCreatureGetShader(lua_State* L); + static int luaCreatureSetShader(lua_State* L); }; diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 97d3a07f876..38a0eab085f 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -409,6 +409,12 @@ void PlayerFunctions::init(lua_State* L) { Lua::registerMethod(L, "Player", "removeAnimusMastery", PlayerFunctions::luaPlayerRemoveAnimusMastery); Lua::registerMethod(L, "Player", "hasAnimusMastery", PlayerFunctions::luaPlayerHasAnimusMastery); + // OTCR Features + Lua::registerMethod(L, "Player", "getMapShader", PlayerFunctions::luaPlayerGetMapShader); + Lua::registerMethod(L, "Player", "setMapShader", PlayerFunctions::luaPlayerSetMapShader); + Lua::registerMethod(L, "Player", "removeCustomOutfit", PlayerFunctions::luaPlayerRemoveCustomOutfit); + Lua::registerMethod(L, "Player", "addCustomOutfit", PlayerFunctions::luaPlayerAddCustomOutfit); + GroupFunctions::init(L); GuildFunctions::init(L); MountFunctions::init(L); @@ -4914,3 +4920,74 @@ int PlayerFunctions::luaPlayerHasAnimusMastery(lua_State* L) { return 1; } + +int PlayerFunctions::luaPlayerGetMapShader(lua_State* L) { + // player:getMapShader() + const auto &player = Lua::getUserdataShared(L, 1); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + Lua::pushBoolean(L, false); + return 0; + } + + Lua::pushString(L, player->getMapShader()); + return 1; +} + +int PlayerFunctions::luaPlayerSetMapShader(lua_State* L) { + // player:setMapShader(shaderName, [temporary]) + const auto &player = Lua::getUserdataShared(L, 1); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + Lua::pushBoolean(L, false); + return 0; + } + const auto shaderName = Lua::getString(L, 2); + player->setMapShader(shaderName); + player->sendMapShader(shaderName); + Lua::pushBoolean(L, true); + return 1; +} + +int PlayerFunctions::luaPlayerAddCustomOutfit(lua_State* L) { + // player:addCustomOutfit(type, id or name) + const auto &player = Lua::getUserdataShared(L, 1); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + Lua::pushBoolean(L, false); + return 0; + } + + std::string type = Lua::getString(L, 2); + std::variant idOrName; + + if (Lua::isNumber(L, 3)) { + idOrName = Lua::getNumber(L, 3); + } else { + idOrName = Lua::getString(L, 3); + } + + Lua::pushBoolean(L, player->addCustomOutfit(type, idOrName)); + return 1; +} + +int PlayerFunctions::luaPlayerRemoveCustomOutfit(lua_State* L) { + // player:removeCustomOutfit(type, id or name) + const auto &player = Lua::getUserdataShared(L, 1); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + Lua::pushBoolean(L, false); + return 0; + } + + std::string type = Lua::getString(L, 2); + std::variant idOrName; + + if (Lua::isNumber(L, 3)) { + idOrName = Lua::getNumber(L, 3); + } else { + idOrName = Lua::getString(L, 3); + } + + Lua::pushBoolean(L, player->removeCustomOutfit(type, idOrName)); +} diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp index 54e57d56155..04e44ec1695 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -388,5 +388,10 @@ class PlayerFunctions { static int luaPlayerRemoveAnimusMastery(lua_State* L); static int luaPlayerHasAnimusMastery(lua_State* L); + static int luaPlayerGetMapShader(lua_State* L); + static int luaPlayerSetMapShader(lua_State* L); + static int luaPlayerAddCustomOutfit(lua_State* L); + static int luaPlayerRemoveCustomOutfit(lua_State* L); + friend class CreatureFunctions; }; diff --git a/src/lua/functions/items/item_functions.cpp b/src/lua/functions/items/item_functions.cpp index 11d719f44f7..38fe005ec83 100644 --- a/src/lua/functions/items/item_functions.cpp +++ b/src/lua/functions/items/item_functions.cpp @@ -92,6 +92,10 @@ void ItemFunctions::init(lua_State* L) { Lua::registerMethod(L, "Item", "canReceiveAutoCarpet", ItemFunctions::luaItemCanReceiveAutoCarpet); + Lua::registerMethod(L, "Item", "setShader", ItemFunctions::luaItemSetShader); + Lua::registerMethod(L, "Item", "getShader", ItemFunctions::luaItemGetShader); + Lua::registerMethod(L, "Item", "hasShader", ItemFunctions::luaItemHasShader); + ContainerFunctions::init(L); ImbuementFunctions::init(L); ItemTypeFunctions::init(L); @@ -1121,3 +1125,43 @@ int ItemFunctions::luaItemHasOwner(lua_State* L) { Lua::pushBoolean(L, item->hasOwner()); return 1; } + +int ItemFunctions::luaItemHasShader(lua_State* L) { + // item:hasShader() + const auto &item = Lua::getUserdataShared(L, 1); + if (!item) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); + return 1; + } + + Lua::pushBoolean(L, item->hasShader()); + return 1; +} + +int ItemFunctions::luaItemGetShader(lua_State* L) { + // item:getShader() + const auto &item = Lua::getUserdataShared(L, 1); + if (!item) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); + Lua::pushBoolean(L, false); + return 1; + } + + Lua::pushString(L, item->getShader()); + return 1; +} + +int ItemFunctions::luaItemSetShader(lua_State* L) { + // item:setShader(shaderName) + const auto &item = Lua::getUserdataShared(L, 1); + if (!item) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); + Lua::pushBoolean(L, false); + return 1; + } + + item->setShader(Lua::getString(L, 2)); + g_game().refreshItem(item); + Lua::pushBoolean(L, true); + return 1; +} diff --git a/src/lua/functions/items/item_functions.hpp b/src/lua/functions/items/item_functions.hpp index 44b97c0e113..0c870b96831 100644 --- a/src/lua/functions/items/item_functions.hpp +++ b/src/lua/functions/items/item_functions.hpp @@ -89,4 +89,8 @@ class ItemFunctions { static int luaItemIsOwner(lua_State* L); static int luaItemGetOwnerName(lua_State* L); static int luaItemHasOwner(lua_State* L); + + static int luaItemSetShader(lua_State* L); + static int luaItemGetShader(lua_State* L); + static int luaItemHasShader(lua_State* L); }; diff --git a/src/lua/functions/lua_functions_loader.cpp b/src/lua/functions/lua_functions_loader.cpp index d4aeac2a7f1..c0a15bcb3bd 100644 --- a/src/lua/functions/lua_functions_loader.cpp +++ b/src/lua/functions/lua_functions_loader.cpp @@ -449,7 +449,12 @@ Outfit_t Lua::getOutfit(lua_State* L, int32_t arg) { outfit.lookTypeEx = getField(L, arg, "lookTypeEx"); outfit.lookType = getField(L, arg, "lookType"); - lua_pop(L, 13); + outfit.lookWing = getField(L, arg, "lookShader"); + outfit.lookAura = getField(L, arg, "lookAura"); + outfit.lookEffect = getField(L, arg, "lookEffect"); + outfit.lookShader = getField(L, arg, "lookShader"); + + lua_pop(L, 17); return outfit; } @@ -631,7 +636,7 @@ void Lua::pushOutfit(lua_State* L, const Outfit_t &outfit) { return; } - lua_createtable(L, 0, 13); + lua_createtable(L, 0, 17); setField(L, "lookType", outfit.lookType); setField(L, "lookTypeEx", outfit.lookTypeEx); setField(L, "lookHead", outfit.lookHead); @@ -645,6 +650,10 @@ void Lua::pushOutfit(lua_State* L, const Outfit_t &outfit) { setField(L, "lookMountLegs", outfit.lookMountLegs); setField(L, "lookMountFeet", outfit.lookMountFeet); setField(L, "lookFamiliarsType", outfit.lookFamiliarsType); + setField(L, "lookWing ", outfit.lookWing); + setField(L, "lookAura", outfit.lookAura); + setField(L, "lookEffect ", outfit.lookEffect); + setField(L, "lookShader ", outfit.lookShader); } void Lua::registerClass(lua_State* L, const std::string &className, const std::string &baseClass, lua_CFunction newFunction /* = nullptr*/) { diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 4cdc5ecbdf1..697224e55ae 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -13,6 +13,7 @@ #include "config/configmanager.hpp" #include "core.hpp" #include "creatures/appearance/mounts/mounts.hpp" +#include "creatures/appearance/attachedeffects/attachedeffects.hpp" #include "creatures/combat/condition.hpp" #include "creatures/combat/spells.hpp" #include "creatures/interactions/chat.hpp" @@ -279,6 +280,10 @@ void ProtocolGame::AddItem(NetworkMessage &msg, uint16_t id, uint8_t count, uint msg.addByte(0xFF); } + // OTCR Features + if (isOTCR) { + msg.addString(""); // g_game.enableFeature(GameItemShader) + } return; } @@ -312,6 +317,11 @@ void ProtocolGame::AddItem(NetworkMessage &msg, uint16_t id, uint8_t count, uint if (it.isWrapKit && !oldProtocol) { msg.add(0x00); } + + // OTCR Features + if (isOTCR) { + msg.addString(""); // g_game.enableFeature(GameItemShader) + } } void ProtocolGame::AddItem(NetworkMessage &msg, const std::shared_ptr &item) { @@ -342,6 +352,10 @@ void ProtocolGame::AddItem(NetworkMessage &msg, const std::shared_ptr &ite msg.addByte(0xFF); } + // OTCR Features + if (isOTCR) { + msg.addString(item->getShader()); // g_game.enableFeature(GameItemShader) + } return; } @@ -456,6 +470,11 @@ void ProtocolGame::AddItem(NetworkMessage &msg, const std::shared_ptr &ite msg.add(0x00); } } + + // OTCR Features + if (isOTCR) { + msg.addString(item->getShader()); // g_game.enableFeature(GameItemShader) + } } void ProtocolGame::release() { @@ -478,6 +497,9 @@ void ProtocolGame::login(const std::string &name, uint32_t accountId, OperatingS // Extended opcodes if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) { isOTC = true; + if (isOTC && otclientV8 == 0) { + sendOTCRFeatures(); + } NetworkMessage opcodeMessage; opcodeMessage.addByte(0x32); opcodeMessage.addByte(0x00); @@ -1033,6 +1055,9 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby case 0x32: parseExtendedOpcode(msg); break; // otclient extended opcode + case 0x38: + parsePlayerTyping(msg); // player are typing or not + break; case 0x60: parseInventoryImbuements(msg); break; @@ -1702,6 +1727,15 @@ void ProtocolGame::parseSetOutfit(NetworkMessage &msg) { } uint8_t isMountRandomized = !oldProtocol ? msg.getByte() : 0; + // g_game.enableFeature(GameWingsAurasEffectsShader) + newOutfit.lookWing = isOTCR ? msg.get() : 0; + newOutfit.lookAura = isOTCR ? msg.get() : 0; + newOutfit.lookEffect = isOTCR ? msg.get() : 0; + std::string shaderName = isOTCR ? msg.getString() : ""; + if (!shaderName.empty()) { + const auto &shader = g_game().attachedeffects->getShaderByName(shaderName); + newOutfit.lookShader = shader ? shader->id : 0; + } g_game().playerChangeOutfit(player->getID(), newOutfit, isMountRandomized); } else if (outfitType == 1) { // This value probably has something to do with try outfit variable inside outfit window dialog @@ -2523,6 +2557,11 @@ void ProtocolGame::parseCyclopediaMonsterTracker(NetworkMessage &msg) { } } +void ProtocolGame::parsePlayerTyping(NetworkMessage &msg) { + uint8_t typing = msg.getByte(); + g_dispatcher().addEvent([self = getThis(), playerID = player->getID(), typing] { g_game().playerSetTyping(playerID, typing); }, __FUNCTION__); +} + void ProtocolGame::sendTeamFinderList() { if (!player || oldProtocol) { return; @@ -7157,6 +7196,24 @@ void ProtocolGame::sendOutfitWindow() { currentOutfit.lookMount = 0; } + const auto ¤tWing = g_game().attachedeffects->getWingByID(player->getCurrentWing()); + if (currentWing) { + currentOutfit.lookWing = currentWing->id; + } + // @ -- auras + const auto ¤tAura = g_game().attachedeffects->getAuraByID(player->getCurrentAura()); + if (currentAura) { + currentOutfit.lookAura = currentAura->id; + } + // @ -- effects + const auto ¤tEffect = g_game().attachedeffects->getEffectByID(player->getCurrentEffect()); + if (currentEffect) { + currentOutfit.lookEffect = currentEffect->id; + } + const auto ¤tShader = g_game().attachedeffects->getShaderByID(player->getCurrentShader()); + if (currentShader) { + currentOutfit.lookShader = currentShader->id; + } AddOutfit(msg, currentOutfit); if (oldProtocol) { @@ -7196,6 +7253,9 @@ void ProtocolGame::sendOutfitWindow() { msg.addString(mount->name); } + if (isOTCR) { + sendOutfitWindowCustomOTCR(msg); + } writeToOutputBuffer(msg); return; } @@ -7340,6 +7400,9 @@ void ProtocolGame::sendOutfitWindow() { // Version 12.81 - Random mount 'bool' msg.addByte(isSupportOutfit ? 0x00 : (player->isRandomMounted() ? 0x01 : 0x00)); + if (isOTCR) { + sendOutfitWindowCustomOTCR(msg); + } writeToOutputBuffer(msg); } @@ -7832,6 +7895,14 @@ void ProtocolGame::AddCreature(NetworkMessage &msg, const std::shared_ptrcanWalkthroughEx(creature) ? 0x00 : 0x01); + + if (isOTCR) { + msg.addString(creature->getShader()); // g_game.enableFeature(GameCreatureShader) + msg.addByte(static_cast(creature->getAttachedEffectList().size())); // g_game.enableFeature(GameCreatureAttachedEffect) + for (const uint16_t id : creature->getAttachedEffectList()) { + msg.add(id); // g_game.enableFeature(GameCreatureAttachedEffect) + } + } } void ProtocolGame::AddPlayerStats(NetworkMessage &msg) { @@ -7964,6 +8035,9 @@ void ProtocolGame::AddOutfit(NetworkMessage &msg, const Outfit_t &outfit, bool a msg.addByte(outfit.lookMountFeet); } } + if (isOTCR) { + AddOutfitCustomOTCR(msg, outfit); // g_game.enableFeature(GameWingsAurasEffectsShader) + } } void ProtocolGame::addImbuementInfo(NetworkMessage &msg, uint16_t imbuementId) const { @@ -8460,6 +8534,26 @@ void ProtocolGame::sendFeatures() { writeToOutputBuffer(msg); } +// OTCR +void ProtocolGame::sendOTCRFeatures() { + isOTCR = true; + const auto &enabledFeatures = g_configManager().getEnabledFeaturesOTC(); + const auto &disabledFeatures = g_configManager().getDisabledFeaturesOTC(); + NetworkMessage msg; + msg.addByte(0x43); + auto totalFeatures = static_cast(enabledFeatures.size() + disabledFeatures.size()); + msg.add(totalFeatures); + for (auto feature : enabledFeatures) { + msg.addByte(static_cast(feature)); + msg.addByte(0x01); + } + for (auto feature : disabledFeatures) { + msg.addByte(static_cast(feature)); + msg.addByte(0x00); + } + writeToOutputBuffer(msg); +} + void ProtocolGame::parseInventoryImbuements(NetworkMessage &msg) { if (oldProtocol) { return; @@ -9364,6 +9458,155 @@ void ProtocolGame::sendTakeScreenshot(Screenshot_t screenshotType) { writeToOutputBuffer(msg); } +void ProtocolGame::sendAttachedEffect(const std::shared_ptr &creature, uint16_t effectId) { + if (!isOTCR) { + return; + } + + NetworkMessage msg; + msg.addByte(0x34); + msg.add(creature->getID()); + msg.add(effectId); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendDetachEffect(const std::shared_ptr &creature, uint16_t effectId) { + if (!isOTCR) { + return; + } + + NetworkMessage msg; + msg.addByte(0x35); + msg.add(creature->getID()); + msg.add(effectId); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendShader(const std::shared_ptr &creature, const std::string &shaderName) { + if (!isOTCR) { + return; + } + + NetworkMessage msg; + msg.addByte(0x36); + msg.add(creature->getID()); + msg.addString(shaderName); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMapShader(const std::string &shaderName) { + if (!isOTCR) { + return; + } + + NetworkMessage msg; + msg.addByte(0x37); + msg.addString(shaderName); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendPlayerTyping(const std::shared_ptr &creature, uint8_t typing) { + if (!isOTCR) { + return; + } + + NetworkMessage msg; + msg.addByte(0x38); + msg.add(creature->getID()); + msg.addByte(typing); + writeToOutputBuffer(msg); +} + +void ProtocolGame::AddOutfitCustomOTCR(NetworkMessage &msg, const Outfit_t &outfit) { + if (!isOTCR) { + return; + } + + msg.add(outfit.lookWing); + msg.add(outfit.lookAura); + msg.add(outfit.lookEffect); + const auto &shader = g_game().attachedeffects->getShaderByID(outfit.lookShader); + msg.addString(shader ? shader->name : ""); +} + +void ProtocolGame::sendOutfitWindowCustomOTCR(NetworkMessage &msg) { + if (!isOTCR) { + return; + } + // wings + auto startWings = msg.getBufferPosition(); + uint16_t limitWings = std::numeric_limits::max(); + uint16_t wingSize = 0; + msg.skipBytes(1); + const auto &wings = g_game().attachedeffects->getWings(); + for (const auto &wing : wings) { + if (player->hasWing(wing)) { + msg.add(wing->id); + msg.addString(wing->name); + ++wingSize; + } + if (wingSize == limitWings) { + break; + } + } + auto endWings = msg.getBufferPosition(); + msg.setBufferPosition(startWings); + msg.add(wingSize); + msg.setBufferPosition(endWings); + // auras + auto startAuras = msg.getBufferPosition(); + uint16_t limitAuras = std::numeric_limits::max(); + uint16_t auraSize = 0; + msg.skipBytes(1); + const auto &auras = g_game().attachedeffects->getAuras(); + for (const auto &aura : auras) { + if (player->hasAura(aura)) { + msg.add(aura->id); + msg.addString(aura->name); + ++auraSize; + } + if (auraSize == limitAuras) { + break; + } + } + auto endAuras = msg.getBufferPosition(); + msg.setBufferPosition(startAuras); + msg.add(auraSize); + msg.setBufferPosition(endAuras); + // effects + auto startEffects = msg.getBufferPosition(); + uint16_t limitEffects = std::numeric_limits::max(); + uint16_t effectSize = 0; + msg.skipBytes(1); + const auto &effects = g_game().attachedeffects->getEffects(); + for (const auto &effect : effects) { + if (player->hasEffect(effect)) { + msg.add(effect->id); + msg.addString(effect->name); + ++effectSize; + } + if (effectSize == limitEffects) { + break; + } + } + auto endEffects = msg.getBufferPosition(); + msg.setBufferPosition(startEffects); + msg.add(effectSize); + msg.setBufferPosition(endEffects); + // shader + std::vector shaders; + for (const auto &shader : g_game().attachedeffects->getShaders()) { + if (player->hasShader(shader.get())) { + shaders.push_back(shader.get()); + } + } + msg.addByte(static_cast(shaders.size())); + for (const Shader* shader : shaders) { + msg.add(shader->id); + msg.addString(shader->name); + } +} + void ProtocolGame::parseCyclopediaHouseAuction(NetworkMessage &msg) { if (oldProtocol) { return; diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 7b7e0aacee3..f2f40181593 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -499,6 +499,16 @@ class ProtocolGame final : public Protocol { // OTCv8 void sendFeatures(); + // OTCR + void sendOTCRFeatures(); + void sendAttachedEffect(const std::shared_ptr &creature, uint16_t effectId); + void sendDetachEffect(const std::shared_ptr &creature, uint16_t effectId); + void sendShader(const std::shared_ptr &creature, const std::string &shaderName); + void sendMapShader(const std::string &shaderName); + void sendPlayerTyping(const std::shared_ptr &creature, uint8_t typing); + void parsePlayerTyping(NetworkMessage &msg); + void AddOutfitCustomOTCR(NetworkMessage &msg, const Outfit_t &outfit); + void sendOutfitWindowCustomOTCR(NetworkMessage &msg); void parseInventoryImbuements(NetworkMessage &msg); void sendInventoryImbuements(const std::map> &items); @@ -535,9 +545,10 @@ class ProtocolGame final : public Protocol { bool shouldAddExivaRestrictions = false; bool oldProtocol = false; + bool isOTC = false; + bool isOTCR = false; uint16_t otclientV8 = 0; - bool isOTC = false; void sendOpenStash(); void parseStashWithdraw(NetworkMessage &msg); diff --git a/src/utils/const.hpp b/src/utils/const.hpp index 4cc296e4a7e..0e828a6f2b9 100644 --- a/src/utils/const.hpp +++ b/src/utils/const.hpp @@ -48,6 +48,22 @@ static constexpr int32_t PSTRG_OUTFITS_RANGE_SIZE = 500; static constexpr int32_t PSTRG_MOUNTS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2001); static constexpr int32_t PSTRG_MOUNTS_RANGE_SIZE = 10; static constexpr int32_t PSTRG_MOUNTS_CURRENTMOUNT = (PSTRG_MOUNTS_RANGE_START + 10); +//[2012 - 2022]; +static constexpr int32_t PSTRG_WING_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2012); +static constexpr int32_t PSTRG_WING_RANGE_SIZE = 10; +static constexpr int32_t PSTRG_WING_CURRENTWING = (PSTRG_WING_RANGE_START + 10); +//[2023 - 2033]; +static constexpr int32_t PSTRG_EFFECT_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2023); +static constexpr int32_t PSTRG_EFFECT_RANGE_SIZE = 10; +static constexpr int32_t PSTRG_EFFECT_CURRENTEFFECT = (PSTRG_EFFECT_RANGE_START + 10); +//[2034 - 2044]; +static constexpr int32_t PSTRG_AURA_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2034); +static constexpr int32_t PSTRG_AURA_RANGE_SIZE = 10; +static constexpr int32_t PSTRG_AURA_CURRENTAURA = (PSTRG_AURA_RANGE_START + 10); +//[2045 - 2055]; +static constexpr int32_t PSTRG_SHADER_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2045); +static constexpr int32_t PSTRG_SHADER_RANGE_SIZE = 10; +static constexpr int32_t PSTRG_SHADER_CURRENTSHADER = (PSTRG_SHADER_RANGE_START + 10); // [3000 - 3500]; static constexpr int32_t PSTRG_FAMILIARS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 3000); static constexpr int32_t PSTRG_FAMILIARS_RANGE_SIZE = 500; diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index 38342c2b885..f9ecb4a5fe4 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -20,6 +20,7 @@ + @@ -237,6 +238,7 @@ +