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 @@
+