diff --git a/modules/game_features/features.lua b/modules/game_features/features.lua index 4419ce6aae..a73a4a1384 100644 --- a/modules/game_features/features.lua +++ b/modules/game_features/features.lua @@ -4,6 +4,7 @@ controller:registerEvents(g_game, { -- g_game.enableFeature(GameKeepUnawareTiles) -- g_game.enableFeature(GameNegativeOffset) -- g_game.enableFeature(GameWingsAurasEffectsShader) + -- g_game.enableFeature(GameCreaturePaperdoll) -- g_game.enableFeature(GameAllowCustomBotScripts) g_game.enableFeature(GameFormatCreatureName) diff --git a/modules/game_interface/interface.otmod b/modules/game_interface/interface.otmod index d3fadec5e1..cfdd191ac0 100644 --- a/modules/game_interface/interface.otmod +++ b/modules/game_interface/interface.otmod @@ -40,6 +40,7 @@ Module - game_unjustifiedpoints - game_shaders - game_attachedeffects + - game_paperdolls - game_stash - game_healthcircle - game_shop diff --git a/modules/game_outfit/outfit.lua b/modules/game_outfit/outfit.lua index eb79540c22..8c6c0f4989 100644 --- a/modules/game_outfit/outfit.lua +++ b/modules/game_outfit/outfit.lua @@ -1,4 +1,4 @@ -local statesOutft ={ +local statesOutft = { available = 0, store = 1, goldenOutfitTooltip = 2 @@ -63,7 +63,7 @@ end local function attachEffectIfValid(UICreature, value) local creature = UICreature:getCreature() - if checkPresetsValidity({value}) then + if checkPresetsValidity({ value }) then if creature then creature:attachEffect(g_attachedEffects.getById(value)) end @@ -72,7 +72,7 @@ end local function attachOrDetachEffect(Id, attach) local creature = previewCreature:getCreature() - if checkPresetsValidity({Id}) then + if checkPresetsValidity({ Id }) then if creature then if attach then if not creature:getAttachedEffectById(Id) then @@ -108,7 +108,6 @@ local function showSelectionList(data, tempValue, tempField, onSelectCallback) end if data and #data > 0 then for _, itemData in ipairs(data) do - local button = g_ui.createWidget("SelectionButton", window.selectionList) button:setId(tostring(itemData[1])) @@ -117,10 +116,9 @@ local function showSelectionList(data, tempValue, tempField, onSelectCallback) button.outfit:setOutfit({ type = modules.game_attachedeffects.thingId(itemData[1]) }) - + button.outfit:setMarginBottom(15) button.outfit:setCenter(true) - elseif Category == 2 then button.outfit:setOutfit(previewCreature:getCreature():getOutfit()) button.outfit:getCreature():attachEffect(g_attachedEffects.getById(itemData[1])) @@ -149,7 +147,8 @@ local function showSelectionList(data, tempValue, tempField, onSelectCallback) window.listSearch:show() end -local AppearanceData = {"preset", "outfit", "mount", "familiar", "wings", "aura", "effects", "shader", "healthBar", "title"} +local AppearanceData = { "preset", "outfit", "mount", "familiar", "wings", "aura", "effects", "shader", "healthBar", + "title" } function init() connect(g_game, { @@ -172,7 +171,7 @@ function onOutfitChange(creature, outfit, oldOutfit) end function onMovementChange(checkBox, checked) - local walkingSpeed = checked and 1000 or 0 + local walkingSpeed = checked and 1000 or 0 local mainCreature = previewCreature:getCreature() if mainCreature then @@ -490,22 +489,22 @@ function create(player, outfitList, creatureMount, mountList, familiarList, wing window.preview.options.showFamiliar:setVisible(g_game.getFeature(GamePlayerFamiliars)) window.appearance.settings.familiar:setVisible(g_game.getFeature(GamePlayerFamiliars)) - + local checks = { - {window.preview.options.showWings, ServerData.wings}, - {window.preview.options.showAura, ServerData.auras}, - {window.preview.options.showShader, ServerData.shaders}, - {window.preview.options.showBars, ServerData.healthBars}, - {window.preview.options.showEffects, ServerData.effects}, - {window.preview.options.showTitle, ServerData.title}, - {window.preview.options.showFamiliar, ServerData.familiars}, - {window.appearance.settings.familiar, ServerData.familiars}, - {window.appearance.settings.wings, ServerData.wings}, - {window.appearance.settings.aura, ServerData.auras}, - {window.appearance.settings.shader, ServerData.shaders}, - {window.appearance.settings.healthBar, ServerData.healthBars}, - {window.appearance.settings.effects, ServerData.effects}, - {window.appearance.settings.title, ServerData.title}, + { window.preview.options.showWings, ServerData.wings }, + { window.preview.options.showAura, ServerData.auras }, + { window.preview.options.showShader, ServerData.shaders }, + { window.preview.options.showBars, ServerData.healthBars }, + { window.preview.options.showEffects, ServerData.effects }, + { window.preview.options.showTitle, ServerData.title }, + { window.preview.options.showFamiliar, ServerData.familiars }, + { window.appearance.settings.familiar, ServerData.familiars }, + { window.appearance.settings.wings, ServerData.wings }, + { window.appearance.settings.aura, ServerData.auras }, + { window.appearance.settings.shader, ServerData.shaders }, + { window.appearance.settings.healthBar, ServerData.healthBars }, + { window.appearance.settings.effects, ServerData.effects }, + { window.appearance.settings.title, ServerData.title }, } for _, check in ipairs(checks) do @@ -671,7 +670,6 @@ function deletePreset() updateAppearanceText("aura", "None") updateAppearanceText("wings", "None") updateAppearanceText("effects", "None") - end function savePreset() @@ -706,7 +704,7 @@ function savePreset() attachEffectIfValid(window.presetsList[presetId].creature, lastSelectAura) attachEffectIfValid(window.presetsList[presetId].creature, lastSelectEffects) attachEffectIfValid(window.presetsList[presetId].creature, lastSelectWings) - local presets = {lastSelectAura, lastSelectEffects, lastSelectWings} + local presets = { lastSelectAura, lastSelectEffects, lastSelectWings } local hasValidAE = checkPresetsValidity(presets) local thingType = g_things.getThingType(tempOutfit.type, ThingCategoryCreature) @@ -822,7 +820,7 @@ function showPresets() attachEffectIfValid(presetWidget.creature, preset.effects) attachEffectIfValid(presetWidget.creature, preset.wings) - local presets = {preset.auras, preset.effects, preset.wings} + local presets = { preset.auras, preset.effects, preset.wings } local hasValidAE = checkPresetsValidity(presets) local thingType = g_things.getThingType(tempOutfit.type, ThingCategoryCreature) @@ -843,7 +841,6 @@ function showPresets() if presetId == settings.currentPreset then focused = presetId - end end end @@ -952,7 +949,7 @@ function showMounts() if tempOutfit.mount == mountData[1] then focused = mountData[1] end - + local state = mountData[3] if state then button.state = state @@ -1005,11 +1002,11 @@ function showFamiliars() button.outfit:setOutfit({ type = familiarData[1] }) - + button.name:setText(familiarData[2]) button.outfit:setCenter(true) - + if tempOutfit.familiar == familiarData[1] then focused = familiarData[1] end @@ -1073,13 +1070,12 @@ function showShaders() }) button.outfit:setCenter(true) - + button.outfit:getCreature():setShader(shaderData[2]) button.name:setText(shaderData[2]) if tempOutfit.shaders == shaderData[2] then - focused = shaderData[2] end end @@ -1205,9 +1201,7 @@ function showTitle() end function onPresetSelect(list, focusedChild, unfocusedChild, reason) - if focusedChild then - local presetId = tonumber(focusedChild:getId()) local preset = settings.presets[presetId] tempOutfit = table.copy(preset.outfit) @@ -1236,7 +1230,7 @@ function onPresetSelect(list, focusedChild, unfocusedChild, reason) updateAppearanceText("shader", preset.shaders or "Outfit - Default") updateAppearanceText("effects", modules.game_attachedeffects.getName(preset.effects)) end - + previewCreature:getCreature():clearAttachedEffects() if settings.showEffects and preset.effects then @@ -1275,7 +1269,6 @@ function onPresetSelect(list, focusedChild, unfocusedChild, reason) lastSelectWings = preset.wings lastSelectEffects = preset.effects lastSelectShader = preset.shaders - end end @@ -1361,7 +1354,7 @@ function onAuraSelect(list, focusedChild, unfocusedChild, reason) if focusedChild then local auraType = tonumber(focusedChild:getId()) - if checkPresetsValidity({auraType}) then + if checkPresetsValidity({ auraType }) then previewCreature:getCreature():attachEffect(g_attachedEffects.getById(auraType)) lastSelectAura = auraType tempOutfit.auras = auraType @@ -1388,8 +1381,7 @@ function onWingsSelect(list, focusedChild, unfocusedChild, reason) if focusedChild then local wingsType = tonumber(focusedChild:getId()) - if checkPresetsValidity({wingsType}) then - + if checkPresetsValidity({ wingsType }) then previewCreature:getCreature():attachEffect(g_attachedEffects.getById(wingsType)) lastSelectWings = wingsType tempOutfit.wings = wingsType @@ -1419,7 +1411,7 @@ function onShaderSelect(list, focusedChild, unfocusedChild, reason) showShaderCheck:setChecked(true) showShaderCheck.onCheckChange = onShowShaderChange end - + lastSelectShader = shaderType tempOutfit.shaders = shaderType creature:setShader(shaderType) @@ -1467,7 +1459,7 @@ function onEffectBarSelect(list, focusedChild, unfocusedChild, reason) if focusedChild then local effect_id = tonumber(focusedChild:getId()) - if checkPresetsValidity({effect_id}) then + if checkPresetsValidity({ effect_id }) then previewCreature:getCreature():attachEffect(g_attachedEffects.getById(effect_id)) lastSelectEffects = effect_id tempOutfit.effects = effect_id @@ -1691,18 +1683,24 @@ function updatePreview() previewCreature:setOutfit(previewOutfit) previewCreature:getCreature():setDirection(direction) + for _, paperdoll in ipairs(g_game.getLocalPlayer():getPaperdolls()) do + if paperdoll:canDrawOnUI() then + local clone = paperdoll:clone() + previewCreature:getCreature():attachPaperdoll(clone) + end + end end function rotate(value) if not previewCreature then return end - + local creature = previewCreature:getCreature() if not creature then return end - + local direction = previewCreature:getDirection() direction = direction + value @@ -1739,7 +1737,6 @@ function onFilterOnlyMine(self, checked) end) end - function onFilterSearch() addEvent(function() local searchText = window.listSearch.search:getText():lower():trim() @@ -1768,7 +1765,7 @@ function saveSettings() local writeStatus, writeError = pcall(function() return g_resources.writeFileContents(settingsFile, "[]") end) - + if not writeStatus then g_logger.debug("Could not create outfit settings file during logout: " .. tostring(writeError)) return @@ -1803,7 +1800,7 @@ function saveSettings() local writeStatus, writeError = pcall(function() return g_resources.writeFileContents(settingsFile, json.encode(fullSettings)) end) - + if not writeStatus then g_logger.debug("Could not save outfit settings during logout: " .. tostring(writeError)) end diff --git a/modules/game_paperdolls/configs/demon.lua b/modules/game_paperdolls/configs/demon.lua new file mode 100644 index 0000000000..1812ced0dc --- /dev/null +++ b/modules/game_paperdolls/configs/demon.lua @@ -0,0 +1,36 @@ +--[[ + registerThingConfig(thingId, thingType) + set(paperdollId, config) +]] +-- +local c = PaperdollManager.registerThingConfig(35) + +c:set(1, { + sizeFactor = 1.7, + dirOffset = { + [North] = { 0, 0 }, + [East] = { 0, 5 }, + [South] = { 5, 0 }, + [West] = { 10, 5 } + } +}) + +c:set(2, { + sizeFactor = 1.7, + dirOffset = { + [North] = { 0, 20, false }, + [East] = { -15, 5 }, + [South] = { 5, -15 }, + [West] = { 5, -15 } + } +}) + +c:set(3, { + sizeFactor = 1.7, + dirOffset = { + [North] = { 0, -10 }, + [East] = { -15, -3 }, + [South] = { 0, -10 }, + [West] = { -10, -5 } + } +}) diff --git a/modules/game_paperdolls/lib.lua b/modules/game_paperdolls/lib.lua new file mode 100644 index 0000000000..8e1f38ab4c --- /dev/null +++ b/modules/game_paperdolls/lib.lua @@ -0,0 +1,201 @@ +local __OBJECTS = {} +local __THING_CONFIG = {} + +local executeConfig = function(paperdoll, config) + if not config then + return + end + + local x = 0 + local y = 0 + local onTop = true + if config.onTop ~= nil then + onTop = config.onTop + end + + if config.speed then + paperdoll:setSpeed(config.speed) + end + + paperdoll:reset() + + if config.drawOnUI == false then + paperdoll:setCanDrawOnUI(false) + end + + if config.shader then + paperdoll:setShader(config.shader) + end + + if config.priority then + paperdoll:setPriority(config.priority) + end + + if config.opacity ~= nil and config.opacity < 1.0 then + paperdoll:setOpacity(config.opacity) + end + + if config.onlyAddon then + paperdoll:setOnlyAddon(config.onlyAddon) + end + + if config.addon then + paperdoll:setAddon(config.addon) + end + + if config.sizeFactor then + paperdoll:setSizeFactor(config.sizeFactor) + end + + if config.color then + paperdoll:setColor(config.color) + end + + if config.headColor then + paperdoll:setHeadColor(config.headColor) + end + + if config.bodyColor then + paperdoll:setBodyColor(config.bodyColor) + end + + if config.legsColor then + paperdoll:setLegsColor(config.legsColor) + end + + if config.feetColor then + paperdoll:setFeetColor(config.feetColor) + end + + if config.useMountPattern ~= nil then + paperdoll:setUseMountPattern(config.useMountPattern) + end + + if config.showOnMount ~= nil then + paperdoll:setShowOnMount(config.showOnMount) + end + + if config.offset then + x = config.offset[1] or 0 + y = config.offset[2] or 0 + local _onTop = config.offset[3] + if _onTop == nil then _onTop = onTop end + + onTop = _onTop + + if x ~= 0 or y ~= 0 then + paperdoll:setOffset(x, y) + end + end + + if onTop ~= nil then + paperdoll:setOnTop(onTop) + end + + if config.dirOffset then + for dir, offset in pairs(config.dirOffset) do + local _x = offset[1] or x + local _y = offset[2] or y + local _onTop = offset[3] + if _onTop == nil then _onTop = onTop end + + if type(_x) == 'boolean' then -- onTop Config + paperdoll:setOnTopByDir(dir, _x) + else + paperdoll:setDirOffset(dir, _x, _y, _onTop) + end + end + end + + if config.mountOffset then + x = config.mountOffset[1] or 0 + y = config.mountOffset[2] or 0 + + if x ~= 0 or y ~= 0 then + paperdoll:setMountOffset(x, y) + end + end + + if config.mountDirOffset then + for dir, offset in pairs(config.mountDirOffset) do + local _x = offset[1] or x + local _y = offset[2] or y + local _onTop = offset[3] + if _onTop == nil then _onTop = onTop end + + if type(_x) == 'boolean' then -- onTop Config + paperdoll:setMountOnTopByDir(dir, _x) + else + paperdoll:setMountDirOffset(dir, _x, _y, _onTop) + end + end + end +end + +PaperdollManager = { + get = function(id) + return __OBJECTS[id] + end, + register = function(id, name, thingId, config) + local paperdoll = g_paperdolls.register(id, thingId) + if paperdoll == nil then + return + end + + executeConfig(paperdoll, config) + config.isThingConfig = false + + __OBJECTS[id] = { + id = id, + name = name, + thingId = thingId, + config = config + } + end, + registerThingConfig = function(thingId) + if __THING_CONFIG[thingId] == nil then + __THING_CONFIG[thingId] = {} + end + + local thingConfig = __THING_CONFIG[thingId] + + local methods = { + set = function(self, id, config) + local paperdoll = PaperdollManager.get(id) + if paperdoll == nil then + return + end + + local __config = table.recursivecopy(paperdoll.config) + table.merge(__config, config) + + thingConfig[id] = __config + + __config.isThingConfig = true + if config.onAttach then + __config.__onAttach = paperdoll.config.onAttach + end + + if config.onDetach then + __config.__onDetach = paperdoll.config.onDetach + end + end + } + + return methods + end, + getConfig = function(id, thingId) + local config = __THING_CONFIG[thingId] + if config then + config = config[id] + if config then + return config + end + end + + return __OBJECTS[id].config + end, + executeThingConfig = function(paperdoll, thingId) + executeConfig(paperdoll, PaperdollManager.getConfig(paperdoll:getId(), thingId)) + end +} diff --git a/modules/game_paperdolls/paperdolls.lua b/modules/game_paperdolls/paperdolls.lua new file mode 100644 index 0000000000..3c602ff907 --- /dev/null +++ b/modules/game_paperdolls/paperdolls.lua @@ -0,0 +1,50 @@ +--[[ + register(id, name, thingId, config) + config = { + drawOnUI, priority, onlyAddon, addon, + shader, sizeFactor, + color, headColor, bodyColor, legsColor, feetColor, + useMountPattern, showOnMount + offset{x, y, onTop}, dirOffset[dir]{x, y, onTop}, + mountOffset{x, y, onTop}, mountDirOffset[dir]{x, y, onTop}, + onAttach, onDetach + } +]] +-- + +PaperdollManager.register(1, 'Armadura', 512, { + priority = 1, + addon = 1, + onlyAddon = true, + onAttach = function(paperdoll, creature) + print('onAttach: ', paperdoll:getId(), creature:getName()) + end, + onDetach = function(paperdoll, creature) + print('onDetach: ', paperdoll:getId(), creature:getName()) + end +}) + +PaperdollManager.register(2, 'Weapons/shield', 512, { + priority = 2, + addon = 2, + onlyAddon = true +}) + +PaperdollManager.register(3, 'Peitoral', 367, { + priority = 3, + addon = 1, + onlyAddon = true +}) + +PaperdollManager.register(4, 'Akuma Aura', 664, { + priority = 4, + addon = 1, + onlyAddon = true +}) + +PaperdollManager.register(5, 'Mochila', 136, { + priority = 5, + addon = 1, + color = 77, + onlyAddon = true +}) diff --git a/modules/game_paperdolls/paperdolls.otmod b/modules/game_paperdolls/paperdolls.otmod new file mode 100644 index 0000000000..ba2e733b9a --- /dev/null +++ b/modules/game_paperdolls/paperdolls.otmod @@ -0,0 +1,9 @@ +Module + name: game_paperdolls + description: Attached Paperdolls System + author: Mehah + website: + scripts: [lib, setup, paperdolls, configs/demon] + sandboxed: true + @onLoad: controller:init() + @onUnload: controller:terminate() \ No newline at end of file diff --git a/modules/game_paperdolls/server/creature.cpp b/modules/game_paperdolls/server/creature.cpp new file mode 100644 index 0000000000..1f99fdc22b --- /dev/null +++ b/modules/game_paperdolls/server/creature.cpp @@ -0,0 +1,47 @@ +// paperdolls +void Creature::addPaperdoll(const Paperdoll_t& p) { + m_paperdolls.push_back(p); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, true, true); + + for (const auto spectator : spectators) { + spectator->getPlayer()->sendAttachedPaperdoll(this, p); + } +} + +bool Creature::removePaperdollById(uint16_t id) { + const auto it = std::find_if(m_paperdolls.begin(), m_paperdolls.end(), + [id](const Paperdoll_t& obj) { return obj.id == id; }); + + if (it == m_paperdolls.end()) + return false; + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, true, true); + + for (const auto spectator : spectators) { + spectator->getPlayer()->sendDetachPaperdoll(this, *it, false); + } + + m_paperdolls.erase(it); + return true; +} + +bool Creature::removePaperdollBySlot(uint8_t slot) { + const auto it = std::find_if(m_paperdolls.begin(), m_paperdolls.end(), + [slot](const Paperdoll_t& obj) { return obj.slot == slot; }); + + if (it == m_paperdolls.end()) + return false; + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, true, true); + + for (const auto spectator : spectators) { + spectator->getPlayer()->sendDetachPaperdoll(this, *it, true); + } + + m_paperdolls.erase(it); + return true; +} diff --git a/modules/game_paperdolls/server/creature.h b/modules/game_paperdolls/server/creature.h new file mode 100644 index 0000000000..6e3c49b229 --- /dev/null +++ b/modules/game_paperdolls/server/creature.h @@ -0,0 +1,50 @@ +public: + // paperdolls + void addPaperdoll(const Paperdoll_t& p); + const std::vector& getPaperdolls() const { + return m_paperdolls; + } + void setPaperdoll(const Paperdoll_t& p) { + removePaperdollBySlot(p.slot); + addPaperdoll(p); + } + bool hasPaperdollById(uint16_t id) const { + for (const auto& p : m_paperdolls) { + if (p.id == id) + return true; + } + return false; + } + bool hasPaperdollBySlot(uint8_t slot) const { + for (const auto& p : m_paperdolls) { + if (p.slot == slot) + return true; + } + return false; + } + + Paperdoll_t getPaperdollById(uint16_t id) const { + const auto it = std::find_if(m_paperdolls.begin(), m_paperdolls.end(), + [id](const Paperdoll_t& obj) { return obj.id == id; }); + + if (it == m_paperdolls.end()) + return { UINT16_MAX }; + + return *it; + } + Paperdoll_t getPaperdollBySlot(uint8_t slot) const { + const auto it = std::find_if(m_paperdolls.begin(), m_paperdolls.end(), + [slot](const Paperdoll_t& obj) { return obj.slot == slot; }); + + if (it == m_paperdolls.end()) + return { UINT16_MAX }; + + return *it; + } + + bool removePaperdollById(uint16_t id); + bool removePaperdollBySlot(uint8_t slot); + +private: + // paperdoll + std::vector m_paperdolls; \ No newline at end of file diff --git a/modules/game_paperdolls/server/enums.h b/modules/game_paperdolls/server/enums.h new file mode 100644 index 0000000000..d2f25e9006 --- /dev/null +++ b/modules/game_paperdolls/server/enums.h @@ -0,0 +1,12 @@ +// paperdoll +struct Paperdoll_t +{ + uint16_t id{ 0 }; + uint8_t slot{ 255 }; + uint8_t color{ 0 }; + uint8_t head{ 0 }; + uint8_t body{ 0 }; + uint8_t legs{ 0 }; + uint8_t feet{ 0 }; + std::string shader; +}; diff --git a/modules/game_paperdolls/server/luascript.cpp b/modules/game_paperdolls/server/luascript.cpp new file mode 100644 index 0000000000..e21ae3563d --- /dev/null +++ b/modules/game_paperdolls/server/luascript.cpp @@ -0,0 +1,189 @@ +void LuaScriptInterface::registerFunctions() { + // . + // . + // . + + // add at the end +// Paperdoll + registerMethod("Creature", "addPaperdoll", LuaScriptInterface::luaCreatureAddPaperdoll); + registerMethod("Creature", "getPaperdolls", LuaScriptInterface::luaCreatureGetPaperdolls); + registerMethod("Creature", "setPaperdoll", LuaScriptInterface::luaCreatureSetPaperdoll); + registerMethod("Creature", "hasPaperdollById", LuaScriptInterface::luaCreatureHasPaperdollById); + registerMethod("Creature", "hasPaperdollBySlot", LuaScriptInterface::luaCreatureHasPaperdollBySlot); + registerMethod("Creature", "getPaperdollById", LuaScriptInterface::luaCreatureGetPaperdollById); + registerMethod("Creature", "getPaperdollBySlot", LuaScriptInterface::luaCreatureGetPaperdollBySlot); + registerMethod("Creature", "removePaperdollById", LuaScriptInterface::luaCreatureRemovePaperdollById); + registerMethod("Creature", "removePaperdollBySlot", LuaScriptInterface::luaCreatureRemovePaperdollBySlot); +} + +// Paperdolls +Paperdoll_t LuaScriptInterface::getPaperdoll(lua_State* L, int32_t arg) +{ + Paperdoll_t o; + + o.id = getField(L, arg, "id"); + o.slot = getField(L, arg, "slot", 255); + o.color = getField(L, arg, "color", 0); + o.head = getField(L, arg, "head", 0); + o.body = getField(L, arg, "body", 0); + o.legs = getField(L, arg, "legs", 0); + o.feet = getField(L, arg, "feet", 0); + o.shader = getFieldString(L, arg, "shader"); + + lua_pop(L, 8); + return o; +} + +void LuaScriptInterface::pushPaperdoll(lua_State* L, const Paperdoll_t& paperdoll) +{ + lua_createtable(L, 0, 8); + setField(L, "id", paperdoll.id); + setField(L, "slot", paperdoll.slot); + setField(L, "color", paperdoll.color); + setField(L, "head", paperdoll.head); + setField(L, "body", paperdoll.body); + setField(L, "legs", paperdoll.legs); + setField(L, "feet", paperdoll.feet); + setField(L, "shader", paperdoll.shader); + setMetatable(L, -1, "Paperdoll"); +} + +int LuaScriptInterface::luaCreatureAddPaperdoll(lua_State* L) +{ + // creature:addPaperdoll(paperdoll) + Creature* creature = getUserdata(L, 1); + if (creature) { + creature->addPaperdoll(getPaperdoll(L, 2)); + pushBoolean(L, true); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetPaperdolls(lua_State* L) +{ + // creature:getPaperdolls() + const auto creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, creature->getPaperdolls().size(), 0); + + int index = 0; + for (const auto& paperdoll : creature->getPaperdolls()) { + pushPaperdoll(L, paperdoll); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetPaperdoll(lua_State* L) +{ + // creature:setPaperdoll(paperdoll) + Creature* creature = getUserdata(L, 1); + if (creature) { + creature->setPaperdoll(getPaperdoll(L, 2)); + pushBoolean(L, true); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureHasPaperdollById(lua_State* L) +{ + // creature:hasPaperdollById(id) + const Creature* creature = getUserdata(L, 1); + if (creature) { + uint16_t id = getNumber(L, 2); + pushBoolean(L, creature->hasPaperdollById(id)); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureHasPaperdollBySlot(lua_State* L) +{ + // creature:hasPaperdollBySlot(slot) + const Creature* creature = getUserdata(L, 1); + if (creature) { + uint8_t slot = getNumber(L, 2); + pushBoolean(L, creature->hasPaperdollBySlot(slot)); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetPaperdollById(lua_State* L) +{ + // creature:getPaperdollById() + const Creature* creature = getUserdata(L, 1); + if (creature) { + uint16_t id = getNumber(L, 2); + const auto& paperdoll = creature->getPaperdollById(id); + if (paperdoll.id < UINT16_MAX) + pushPaperdoll(L, creature->getPaperdollById(id)); + else + lua_pushnil(L); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetPaperdollBySlot(lua_State* L) +{ + // creature:getPaperdollBySlot() + const Creature* creature = getUserdata(L, 1); + if (creature) { + uint8_t slot = getNumber(L, 2); + const auto& paperdoll = creature->getPaperdollBySlot(slot); + if (paperdoll.id < UINT16_MAX) + pushPaperdoll(L, creature->getPaperdollBySlot(slot)); + else + lua_pushnil(L); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureRemovePaperdollById(lua_State* L) +{ + // creature:removePaperdollById(id) + Creature* creature = getUserdata(L, 1); + if (creature) { + uint16_t id = getNumber(L, 2); + pushBoolean(L, creature->removePaperdollById(id)); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureRemovePaperdollBySlot(lua_State* L) +{ + // creature:removePaperdollBySlot(slot) + Creature* creature = getUserdata(L, 1); + if (creature) { + uint8_t slot = getNumber(L, 2); + pushBoolean(L, creature->removePaperdollBySlot(slot)); + } + else { + lua_pushnil(L); + } + return 1; +} + diff --git a/modules/game_paperdolls/server/luascript.h b/modules/game_paperdolls/server/luascript.h new file mode 100644 index 0000000000..9fe88da454 --- /dev/null +++ b/modules/game_paperdolls/server/luascript.h @@ -0,0 +1,25 @@ +// put inside class LuaScriptInterface +public: + template + static T getField(lua_State* L, int32_t arg, const std::string& key, T defaultValue) + { + lua_getfield(L, arg, key.c_str()); + if (lua_isnil(L, -1) == 1) + return defaultValue; + + return getNumber(L, -1); + } + + // Paperdoll + static Paperdoll_t getPaperdoll(lua_State* L, int32_t arg); + static void pushPaperdoll(lua_State* L, const Paperdoll_t& paperdoll); + + static int luaCreatureAddPaperdoll(lua_State* L); + static int luaCreatureGetPaperdolls(lua_State* L); + static int luaCreatureSetPaperdoll(lua_State* L); + static int luaCreatureHasPaperdollById(lua_State* L); + static int luaCreatureHasPaperdollBySlot(lua_State* L); + static int luaCreatureGetPaperdollById(lua_State* L); + static int luaCreatureGetPaperdollBySlot(lua_State* L); + static int luaCreatureRemovePaperdollById(lua_State* L); + static int luaCreatureRemovePaperdollBySlot(lua_State* L); \ No newline at end of file diff --git a/modules/game_paperdolls/server/player.h b/modules/game_paperdolls/server/player.h new file mode 100644 index 0000000000..43e938bf3a --- /dev/null +++ b/modules/game_paperdolls/server/player.h @@ -0,0 +1,13 @@ +// paperdolls +public: + void sendAttachedPaperdoll(const Creature* creature, const Paperdoll_t& paperdoll) { + if (client) { + client->sendAttachedPaperdoll(creature, paperdoll); + } + } + + void sendDetachPaperdoll(const Creature* creature, const Paperdoll_t& paperdoll, bool bySlot) { + if (client) { + client->sendDetachPaperdoll(creature, paperdoll, bySlot); + } + } \ No newline at end of file diff --git a/modules/game_paperdolls/server/protocolgame.cpp b/modules/game_paperdolls/server/protocolgame.cpp new file mode 100644 index 0000000000..a030273327 --- /dev/null +++ b/modules/game_paperdolls/server/protocolgame.cpp @@ -0,0 +1,46 @@ + +// find this method +void ProtocolGame::AddCreature(NetworkMessage& msg, const Creature* creature, bool known, uint32_t remove) { + + // . + // . + // . + + // Add after msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01); + + // Paperdolls + { + msg.addByte(static_cast(creature->getPaperdolls().size())); + for (const auto& paperdoll : creature->getPaperdolls()) + addPaperdoll(msg, paperdoll); + } + +} + +void ProtocolGame::addPaperdoll(NetworkMessage& msg, const Paperdoll_t& paperdoll) { + msg.add(paperdoll.id); + msg.add(paperdoll.slot); + msg.add(paperdoll.color); + msg.add(paperdoll.head); + msg.add(paperdoll.body); + msg.add(paperdoll.legs); + msg.add(paperdoll.feet); + msg.addString(paperdoll.shader); +} + +void ProtocolGame::sendAttachedPaperdoll(const Creature* creature, const Paperdoll_t& paperdoll) { + NetworkMessage msg; + msg.addByte(0x3C); + msg.add(creature->getID()); + addPaperdoll(msg, paperdoll); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendDetachPaperdoll(const Creature* creature, const Paperdoll_t& paperdoll, bool bySlot) { + NetworkMessage msg; + msg.addByte(0x3D); + msg.add(creature->getID()); + msg.add(static_cast(bySlot)); + msg.add(bySlot ? paperdoll.slot : paperdoll.id); + writeToOutputBuffer(msg); +} \ No newline at end of file diff --git a/modules/game_paperdolls/server/protocolgame.h b/modules/game_paperdolls/server/protocolgame.h new file mode 100644 index 0000000000..ed1e0fec69 --- /dev/null +++ b/modules/game_paperdolls/server/protocolgame.h @@ -0,0 +1,5 @@ + // paperdoll +private: + void addPaperdoll(NetworkMessage& msg, const Paperdoll_t& paperdoll); + void sendAttachedPaperdoll(const Creature* creature, const Paperdoll_t& paperdoll); + void sendDetachPaperdoll(const Creature* creature, const Paperdoll_t& paperdoll, bool bySlot); \ No newline at end of file diff --git a/modules/game_paperdolls/setup.lua b/modules/game_paperdolls/setup.lua new file mode 100644 index 0000000000..528f85cc22 --- /dev/null +++ b/modules/game_paperdolls/setup.lua @@ -0,0 +1,54 @@ +controller = Controller:new() + +function controller:onGameStart() + -- g_game.getLocalPlayer():attachPaperdoll(g_paperdolls.getById(1)) + -- g_game.getLocalPlayer():attachPaperdoll(g_paperdolls.getById(2)) + -- g_game.getLocalPlayer():attachPaperdoll(g_paperdolls.getById(3)) + -- g_game.getLocalPlayer():attachPaperdoll(g_paperdolls.getById(4)) +end + +function controller:onGameEnd() + -- g_game.getLocalPlayer():clearPaperdolls() +end + +function controller:onTerminate() + g_paperdolls.clear() +end + +local function onAttach(paperdoll, owner) + local outfitType = owner:getOutfit().type + local config = PaperdollManager.getConfig(paperdoll:getId(), outfitType) + + if config.isThingConfig then + PaperdollManager.executeThingConfig(paperdoll, outfitType) + end + + if config.onAttach then + config.onAttach(paperdoll, owner, config.__onAttach) + end +end + +local function onDetach(paperdoll, oldOwner) + local config = PaperdollManager.getConfig(paperdoll:getId(), oldOwner:getOutfit().type) + + if config.onDetach then + config.onDetach(paperdoll, oldOwner, config.__onDetach) + end +end + +local function onOutfitChange(creature, outfit, oldOutfit) + for _i, paperdoll in pairs(creature:getPaperdolls()) do + PaperdollManager.executeThingConfig(paperdoll, outfit.type) + end +end + +controller:registerEvents(LocalPlayer, { + onOutfitChange = onOutfitChange +}) +controller:registerEvents(Creature, { + onOutfitChange = onOutfitChange +}) +controller:registerEvents(Paperdoll, { + onAttach = onAttach, + onDetach = onDetach +}) diff --git a/modules/gamelib/const.lua b/modules/gamelib/const.lua index e08cfa79c3..3943151ffa 100644 --- a/modules/gamelib/const.lua +++ b/modules/gamelib/const.lua @@ -220,6 +220,7 @@ GameTileAddThingWithStackpos = 124 GameMapCache = 125 GameForgeSkillStats = 126 GameCharacterSkillStats = 127 +GameCreaturePaperdoll = 128 TextColors = { red = '#f55e5e', -- '#c83200' @@ -475,7 +476,7 @@ ExperienceRate = { PriceTypeEnum = { Market = 0, - Leader = 1 + Leader = 1 } -- Analyzer constants diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 207e78cb75..6e14d51ceb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -288,6 +288,8 @@ set(SOURCE_FILES client/protocolgame.cpp client/protocolgameparse.cpp client/protocolgamesend.cpp + client/paperdoll.cpp + client/paperdollmanager.cpp client/spriteappearances.cpp client/spritemanager.cpp client/statictext.cpp diff --git a/src/client/client.cpp b/src/client/client.cpp index f8c8238f6d..af4682fc52 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -38,6 +38,7 @@ #ifdef FRAMEWORK_EDITOR #include "creatures.h" #endif +#include "paperdollmanager.h" Client g_client; @@ -70,6 +71,7 @@ void Client::terminate() g_sprites.terminate(); g_spriteAppearances.terminate(); g_shaders.terminate(); + g_paperdolls.clear(); g_gameConfig.terminate(); } diff --git a/src/client/const.h b/src/client/const.h index 7e62d06dac..472b7b19e2 100644 --- a/src/client/const.h +++ b/src/client/const.h @@ -566,6 +566,7 @@ namespace Otc GameMapCache = 125, GameForgeSkillStats = 126, GameCharacterSkillStats = 127, + GameCreaturePaperdoll = 128, LastGameFeature }; diff --git a/src/client/creature.cpp b/src/client/creature.cpp index c84d9ccbb3..55fa27812c 100644 --- a/src/client/creature.cpp +++ b/src/client/creature.cpp @@ -36,6 +36,7 @@ #include "thingtype.h" #include "thingtypemanager.h" #include "tile.h" +#include "paperdoll.h" #include "framework/core/clock.h" #include "framework/core/eventdispatcher.h" #include "framework/core/scheduledevent.h" @@ -119,6 +120,9 @@ void Creature::drawLight(const Point& dest, LightView* lightView) { } drawAttachedLightEffect(dest + m_walkOffset * g_drawPool.getScaleFactor(), lightView); + + for (const auto& paperdoll : m_paperdolls) + paperdoll->drawLight(dest, m_outfit.hasMount(), lightView); } void Creature::draw(const Rect& destRect, const uint8_t size, const bool center) @@ -313,6 +317,11 @@ void Creature::internalDraw(Point dest, const Color& color) drawAttachedEffect(dest, nullptr, false); // On Bottom if (!isHided()) { + const int animationPhase = getCurrentAnimationPhase(); + + for (const auto& paperdoll : m_paperdolls) + paperdoll->draw(dest, animationPhase, m_outfit.hasMount(), false, true, color); + // outfit is a real creature if (m_outfit.isCreature()) { if (m_outfit.hasMount()) { @@ -339,7 +348,6 @@ void Creature::internalDraw(Point dest, const Color& color) } const auto& datType = getThingType(); - const int animationPhase = getCurrentAnimationPhase(); const bool useFramebuffer = !replaceColorShader && hasShader() && g_shaders.getShaderById(m_shaderId)->useFramebuffer(); const auto& drawCreature = [&](const Point& dest) { @@ -379,6 +387,9 @@ void Creature::internalDraw(Point dest, const Color& color) g_drawPool.resetShaderProgram(); } else drawCreature(dest); + for (const auto& paperdoll : m_paperdolls) + paperdoll->draw(dest, animationPhase, m_outfit.hasMount(), true, true, color); + // outfit is a creature imitating an item or the invisible effect } else { int animationPhases = getThingType()->getAnimationPhases(); @@ -814,6 +825,7 @@ void Creature::setDirection(const Otc::Direction direction) m_numPatternX = direction; setAttachedEffectDirection(static_cast(m_numPatternX)); + setPaperdollsDirection(static_cast(m_numPatternX)); } void Creature::setOutfit(const Outfit& outfit, bool fireEvent) @@ -1310,4 +1322,87 @@ std::string Creature::getText() bool Creature::canShoot(int distance) { return getTile() ? getTile()->canShoot(distance) : false; +} + +bool Creature::hasPaperdoll(uint16_t id) { + for (const auto& pd : m_paperdolls) { + if (pd->m_id == id) + return true; + } + + return false; +} + +void Creature::attachPaperdoll(const PaperdollPtr& obj) { + if (!obj) return; + + obj->m_direction = getDirection(); + + uint_fast8_t i = 0; + for (const auto& pd : m_paperdolls) { + if (obj->m_priority < pd->m_priority) + break; + ++i; + } + + m_paperdolls.insert(m_paperdolls.begin() + i, obj); + + g_dispatcher.addEvent([paperdoll = obj, self = static_self_cast()] { + paperdoll->callLuaField("onAttach", self->asLuaObject()); + }); +} + +bool Creature::detachPaperdollById(uint16_t id) { + const auto it = std::find_if(m_paperdolls.begin(), m_paperdolls.end(), + [id](const PaperdollPtr& obj) { return obj->getId() == id; }); + + if (it == m_paperdolls.end()) + return false; + + onDetachPaperdoll(*it); + m_paperdolls.erase(it); + + return true; +} + +bool Creature::detachPaperdollByPriority(uint8_t priority) { + bool finded = false; + for (auto it = m_paperdolls.begin(); it != m_paperdolls.end();) { + const auto& obj = *it; + if (obj->getPriority() == priority) { + onDetachPaperdoll(obj); + it = m_paperdolls.erase(it); + finded = true; + } else ++it; + } + + return finded; +} + +void Creature::onDetachPaperdoll(const PaperdollPtr& paperdoll) { + paperdoll->callLuaField("onDetach", asLuaObject()); +} + +void Creature::clearPaperdolls() { + for (const auto& e : m_paperdolls) + onDetachPaperdoll(e); + m_paperdolls.clear(); +} + +PaperdollPtr Creature::getPaperdollById(uint16_t id) { + const auto it = std::find_if(m_paperdolls.begin(), m_paperdolls.end(), + [id](const PaperdollPtr& obj) { return obj->getId() == id; }); + + if (it == m_paperdolls.end()) + return nullptr; + + return *it; +} + +void Creature::setPaperdollsDirection(Otc::Direction dir) const +{ + for (const auto& paperdoll : m_paperdolls) { + if (paperdoll->m_thingType) + paperdoll->m_direction = dir; + } } \ No newline at end of file diff --git a/src/client/creature.h b/src/client/creature.h index 97741a0f1c..8a5339bf9d 100644 --- a/src/client/creature.h +++ b/src/client/creature.h @@ -202,6 +202,17 @@ minHeight, void setVocation(uint8_t vocation) { m_vocation = vocation; } uint8_t getVocation() { return m_vocation; } + void attachPaperdoll(const PaperdollPtr& obj); + void clearPaperdolls(); + bool hasPaperdoll(uint16_t id); + + bool detachPaperdollById(uint16_t id); + bool detachPaperdollByPriority(uint8_t priority); + + PaperdollPtr getPaperdollById(uint16_t id); + + const std::vector& getPaperdolls() { return m_paperdolls; }; + protected: virtual void terminateWalk(); virtual void onWalking() {}; @@ -211,6 +222,9 @@ minHeight, void setOldPositionSilently(const Position& pos) { m_oldPosition = pos; } void setRemovedSilently(const bool removed) { m_removed = removed; } + void onDetachPaperdoll(const PaperdollPtr& paperdoll); + void setPaperdollsDirection(Otc::Direction dir) const; + ThingType* getThingType() const override; ThingType* getMountThingType() const; @@ -261,6 +275,8 @@ minHeight, CachedText numberText; }; + std::vector m_paperdolls; + UIWidgetPtr m_widgetInformation; TilePtr m_walkingTile; diff --git a/src/client/declarations.h b/src/client/declarations.h index 0c39a73a1a..d7b17e1f29 100644 --- a/src/client/declarations.h +++ b/src/client/declarations.h @@ -58,6 +58,7 @@ class ItemType; class TileBlock; class AttachedEffect; class AttachableObject; +class Paperdoll; #ifdef FRAMEWORK_EDITOR class House; @@ -85,6 +86,7 @@ using ThingTypePtr = std::shared_ptr; using ItemTypePtr = std::shared_ptr; using AttachedEffectPtr = std::shared_ptr; using AttachableObjectPtr = std::shared_ptr; +using PaperdollPtr = std::shared_ptr; #ifdef FRAMEWORK_EDITOR using HousePtr = std::shared_ptr; diff --git a/src/client/luafunctions.cpp b/src/client/luafunctions.cpp index f88d0dadc8..58fbf056f3 100644 --- a/src/client/luafunctions.cpp +++ b/src/client/luafunctions.cpp @@ -57,6 +57,8 @@ #include "uiminimap.h" #include "uiprogressrect.h" #include "uisprite.h" +#include "paperdoll.h" +#include "paperdollmanager.h" #ifdef FRAMEWORK_EDITOR #include "houses.h" @@ -425,6 +427,12 @@ void Client::registerLuaFunctions() g_lua.bindSingletonFunction("g_attachedEffects", "remove", &AttachedEffectManager::remove, &g_attachedEffects); g_lua.bindSingletonFunction("g_attachedEffects", "clear", &AttachedEffectManager::clear, &g_attachedEffects); + g_lua.registerSingletonClass("g_paperdolls"); + g_lua.bindSingletonFunction("g_paperdolls", "getById", &PaperdollManager::getById, &g_paperdolls); + g_lua.bindSingletonFunction("g_paperdolls", "register", &PaperdollManager::set, &g_paperdolls); + g_lua.bindSingletonFunction("g_paperdolls", "remove", &PaperdollManager::remove, &g_paperdolls); + g_lua.bindSingletonFunction("g_paperdolls", "clear", &PaperdollManager::clear, &g_paperdolls); + g_lua.bindGlobalFunction("getOutfitColor", Outfit::getColor); g_lua.bindGlobalFunction("getAngleFromPos", Position::getAngleFromPositions); g_lua.bindGlobalFunction("getDirectionFromPos", Position::getDirectionFromPositions); @@ -636,6 +644,12 @@ void Client::registerLuaFunctions() g_lua.bindClassMemberFunction("isFullHealth", &Creature::isFullHealth); g_lua.bindClassMemberFunction("isCovered", &Creature::isCovered); + g_lua.bindClassMemberFunction("getPaperdolls", &Creature::getPaperdolls); + g_lua.bindClassMemberFunction("attachPaperdoll", &Creature::attachPaperdoll); + g_lua.bindClassMemberFunction("detachPaperdollById", &Creature::detachPaperdollById); + g_lua.bindClassMemberFunction("getPaperdollById", &Creature::getPaperdollById); + g_lua.bindClassMemberFunction("clearPaperdolls", &Creature::clearPaperdolls); + g_lua.bindClassMemberFunction("setText", &Creature::setText); g_lua.bindClassMemberFunction("getText", &Creature::getText); g_lua.bindClassMemberFunction("clearText", &Creature::clearText); @@ -830,6 +844,44 @@ void Client::registerLuaFunctions() g_lua.bindClassMemberFunction("getDirection", &AttachedEffect::getDirection); g_lua.bindClassMemberFunction("move", &AttachedEffect::move); + g_lua.registerClass(); + g_lua.bindClassMemberFunction("clone", &Paperdoll::clone); + g_lua.bindClassMemberFunction("getId", &Paperdoll::getId); + g_lua.bindClassMemberFunction("getSpeed", &Paperdoll::getSpeed); + g_lua.bindClassMemberFunction("setOnTop", &Paperdoll::setOnTop); + g_lua.bindClassMemberFunction("setSpeed", &Paperdoll::setSpeed); + g_lua.bindClassMemberFunction("setOpacity", &Paperdoll::setOpacity); + g_lua.bindClassMemberFunction("setOffset", &Paperdoll::setOffset); + g_lua.bindClassMemberFunction("setDirOffset", &Paperdoll::setDirOffset); + g_lua.bindClassMemberFunction("setOnTopByDir", &Paperdoll::setOnTopByDir); + g_lua.bindClassMemberFunction("setShader", &Paperdoll::setShader); + g_lua.bindClassMemberFunction("setSizeFactor", &Paperdoll::setSizeFactor); + g_lua.bindClassMemberFunction("setPriority", &Paperdoll::setPriority); + g_lua.bindClassMemberFunction("canDrawOnUI", &Paperdoll::canDrawOnUI); + g_lua.bindClassMemberFunction("setCanDrawOnUI", &Paperdoll::setCanDrawOnUI); + g_lua.bindClassMemberFunction("setOnlyAddon", &Paperdoll::setOnlyAddon); + g_lua.bindClassMemberFunction("setAddons", &Paperdoll::setAddons); + g_lua.bindClassMemberFunction("hasAddon", &Paperdoll::hasAddon); + g_lua.bindClassMemberFunction("setAddon", &Paperdoll::setAddon); + g_lua.bindClassMemberFunction("removeAddon", &Paperdoll::removeAddon); + g_lua.bindClassMemberFunction("reset", &Paperdoll::reset); + g_lua.bindClassMemberFunction("setColor", &Paperdoll::setColor); + g_lua.bindClassMemberFunction("setHeadColor", &Paperdoll::setHeadColor); + g_lua.bindClassMemberFunction("setBodyColor", &Paperdoll::setBodyColor); + g_lua.bindClassMemberFunction("setLegsColor", &Paperdoll::setLegsColor); + g_lua.bindClassMemberFunction("setFeetColor", &Paperdoll::setFeetColor); + g_lua.bindClassMemberFunction("getHeadColor", &Paperdoll::getHeadColor); + g_lua.bindClassMemberFunction("getBodyColor", &Paperdoll::getBodyColor); + g_lua.bindClassMemberFunction("getLegsColor", &Paperdoll::getLegsColor); + g_lua.bindClassMemberFunction("getFeetColor", &Paperdoll::getFeetColor); + g_lua.bindClassMemberFunction("setColorByOutfit", &Paperdoll::setColorByOutfit); + + g_lua.bindClassMemberFunction("setMountOffset", &Paperdoll::setMountOffset); + g_lua.bindClassMemberFunction("setMountOnTopByDir", &Paperdoll::setMountOnTopByDir); + g_lua.bindClassMemberFunction("setMountDirOffset", &Paperdoll::setMountDirOffset); + g_lua.bindClassMemberFunction("setUseMountPattern", &Paperdoll::setUseMountPattern); + g_lua.bindClassMemberFunction("setShowOnMount", &Paperdoll::setShowOnMount); + g_lua.registerClass(); g_lua.bindClassStaticFunction("create", [] { return std::make_shared(); }); g_lua.bindClassMemberFunction("addMessage", &StaticText::addMessage); diff --git a/src/client/paperdoll.cpp b/src/client/paperdoll.cpp new file mode 100644 index 0000000000..3a981bf329 --- /dev/null +++ b/src/client/paperdoll.cpp @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2010-2025 OTClient + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "paperdoll.h" +#include "animator.h" +#include "thingtype.h" + +#include +#include +#include +#include + +PaperdollPtr Paperdoll::clone() +{ + auto obj = std::make_shared(); + *(obj.get()) = *this; + + return obj; +} + +void Paperdoll::draw(const Point& dest, uint16_t animationPhase, bool mount, bool isOnTop, bool drawThings, const Color& color, LightView* lightView) { + if (!m_thingType) + return; + + if (mount && !m_showOnMount) + return; + + const auto& dirControl = m_offsetDirections[mount][m_direction]; + if (dirControl.onTop != isOnTop) + return; + + if (!m_useMountPattern) + mount = false; + + if (!m_canDrawOnUI && g_drawPool.getCurrentType() == DrawPoolType::FOREGROUND) + return; + + if (m_shader) g_drawPool.setShaderProgram(m_shader, true); + if (m_opacity < 100) g_drawPool.setOpacity(getOpacity(), true); + + const auto& point = dest - (dirControl.offset * g_drawPool.getScaleFactor()); + const int animation = animationPhase == 0 ? getCurrentAnimationPhase() : animationPhase; + + const auto oldScaleFactor = g_drawPool.getScaleFactor(); + + g_drawPool.setScaleFactor(m_sizeFactor); + + if (!drawThings) { + m_thingType->draw(point, 0, m_direction, 0, 0, animation, color, false, lightView); + } else { + if (!m_onlyAddon) + m_thingType->draw(point, 0, m_direction, 0, 0, animation, color, drawThings, lightView); + + for (int yPattern = 0; yPattern < m_thingType->getNumPatternY(); ++yPattern) { + if (yPattern == 0 && m_onlyAddon) + continue; + + // continue if we dont have this addon + if (yPattern > 0 && !(m_addons & (1 << (yPattern - 1)))) + continue; + + if (m_shader) + g_drawPool.setShaderProgram(m_shader, true); + + m_thingType->draw(point, 0, m_direction, yPattern, static_cast(mount), animation, color); + + if (m_thingType->getLayers() > 1) { + g_drawPool.setCompositionMode(CompositionMode::MULTIPLY); + m_thingType->draw(dest, SpriteMaskYellow, m_direction, yPattern, static_cast(mount), animationPhase, getHeadColor()); + m_thingType->draw(dest, SpriteMaskRed, m_direction, yPattern, static_cast(mount), animationPhase, getBodyColor()); + m_thingType->draw(dest, SpriteMaskGreen, m_direction, yPattern, static_cast(mount), animationPhase, getLegsColor()); + m_thingType->draw(dest, SpriteMaskBlue, m_direction, yPattern, static_cast(mount), animationPhase, getFeetColor()); + g_drawPool.resetCompositionMode(); + } + } + } + + g_drawPool.setScaleFactor(oldScaleFactor); +} + +void Paperdoll::drawLight(const Point& dest, bool mount, LightView* lightView) { + if (!lightView) return; + + const auto& dirControl = m_offsetDirections[mount][m_direction]; + draw(dest, 0, mount, dirControl.onTop, false, Color::white, lightView); +} + +int Paperdoll::getCurrentAnimationPhase() +{ + const auto* animator = m_thingType->getIdleAnimator(); + if (!animator && m_thingType->isAnimateAlways()) + animator = m_thingType->getAnimator(); + + if (animator) + return animator->getPhaseAt(m_animationTimer, getSpeed()); + + if (m_thingType->isCreature() && m_thingType->isAnimateAlways()) { + const int ticksPerFrame = std::round(1000 / m_thingType->getAnimationPhases()) / getSpeed(); + return (g_clock.millis() % (static_cast(ticksPerFrame) * m_thingType->getAnimationPhases())) / ticksPerFrame; + } + + return 0; +} + +void Paperdoll::setShader(const std::string_view name) { m_shader = g_shaders.getShader(name); } + +void Paperdoll::reset() { + m_onlyAddon = false; + m_canDrawOnUI = true; + + m_addons = 0; + m_sizeFactor = 1.f; + for (auto& pattern : m_offsetDirections) + pattern.fill(DirControl()); +} + +void Paperdoll::setOnTop(bool onTop) { + for (auto& pattern : m_offsetDirections) + for (auto& control : pattern) + control.onTop = onTop; +} + +void Paperdoll::setOffset(int16_t x, int16_t y) { + for (auto& control : m_offsetDirections[0]) + control.offset = { x, y }; +} + +void Paperdoll::setOnTopByDir(Otc::Direction direction, bool onTop) { + m_offsetDirections[0][direction].onTop = onTop; +} + +void Paperdoll::setMountOffset(int16_t x, int16_t y) { + for (auto& control : m_offsetDirections[1]) + control.offset = { x, y }; +} + +void Paperdoll::setMountOnTopByDir(Otc::Direction direction, bool onTop) { + m_offsetDirections[1][direction].onTop = onTop; +} \ No newline at end of file diff --git a/src/client/paperdoll.h b/src/client/paperdoll.h new file mode 100644 index 0000000000..861049887a --- /dev/null +++ b/src/client/paperdoll.h @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2010-2025 OTClient + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "outfit.h" +#include "declarations.h" +#include +#include +#include + +class Paperdoll : public LuaObject +{ +public: + void draw(const Point& /*dest*/, uint16_t animationPhase, bool mount, bool isOnTop, bool drawThings, const Color& color, LightView* = nullptr); + void drawLight(const Point& /*dest*/, bool mount, LightView*); + + uint16_t getId() { return m_id; } + + PaperdollPtr clone(); + + float getSpeed() { return m_speed / 100.f; } + void setSpeed(float speed) { m_speed = speed * 100u; } + + float getOpacity() { return m_opacity / 100.f; } + void setOpacity(float opacity) { m_opacity = opacity * 100u; } + + float getSizeFactor() { return m_sizeFactor; } + void setSizeFactor(const float s) { m_sizeFactor = s; } + + bool getOnlyAddon() { return m_onlyAddon; } + void setOnlyAddon(bool s) { m_onlyAddon = s; } + + uint32_t getAddons() { return m_addons; } + void setAddons(uint32_t addons) { m_addons = addons; } + + uint8_t getPriority() { return m_priority; } + void setPriority(uint8_t priority) { m_priority = priority; } + + uint32_t hasAddon(uint32_t addon) { return (m_addons & addon) == addon; } + void setAddon(uint32_t addon) { m_addons |= addon; } + void removeAddon(uint32_t addon) { m_addons &= ~addon; } + + void setOnTop(bool onTop); + void setOffset(int16_t x, int16_t y); + void setOnTopByDir(Otc::Direction direction, bool onTop); + + void setMountOffset(int16_t x, int16_t y); + void setMountOnTopByDir(Otc::Direction direction, bool onTop); + + void setUseMountPattern(bool b) { m_useMountPattern = b; } + bool isUsingMountPattern() { return m_useMountPattern; } + + void setShowOnMount(bool b) { m_showOnMount = b; } + bool isShowingOnMount() { return m_showOnMount; } + + void setDirOffset(Otc::Direction direction, int8_t x, int8_t y, bool onTop = true) { m_offsetDirections[0][direction] = { onTop, {x, y} }; } + void setMountDirOffset(Otc::Direction direction, int8_t x, int8_t y, bool onTop = true) { m_offsetDirections[1][direction] = { onTop, {x, y} }; } + + void setShader(const std::string_view name); + void setCanDrawOnUI(bool canDraw) { m_canDrawOnUI = canDraw; } + bool canDrawOnUI() { return m_canDrawOnUI; } + + void setColor(uint8_t c) { + m_head = c; + m_body = c; + m_legs = c; + m_feet = c; + } + + void setHeadColor(uint8_t c) { m_head = c; } + void setBodyColor(uint8_t c) { m_body = c; } + void setLegsColor(uint8_t c) { m_legs = c; } + void setFeetColor(uint8_t c) { m_feet = c; } + + uint8_t getHeadColor() { return m_head; } + uint8_t getBodyColor() { return m_body; } + uint8_t getLegsColor() { return m_legs; } + uint8_t getFeetColor() { return m_feet; } + + void setColorByOutfit(const Outfit& outfit) { + m_head = outfit.getHead(); + m_body = outfit.getBody(); + m_legs = outfit.getLegs(); + m_feet = outfit.getFeet(); + } + + void reset(); + +private: + int getCurrentAnimationPhase(); + + struct DirControl + { + bool onTop{ true }; + Point offset; + }; + + uint8_t m_priority{ 1 }; + uint8_t m_head{ 0 }, m_body{ 0 }, m_legs{ 0 }, m_feet{ 0 }; + + uint8_t m_speed{ 100 }; + uint8_t m_opacity{ 100 }; + uint16_t m_id{ 0 }; + uint16_t m_thingId{ 0 }; + uint32_t m_addons{ 0 }; + + Timer m_timer; + + bool m_onlyAddon{ false }; + bool m_canDrawOnUI{ true }; + bool m_useMountPattern{ false }; + bool m_showOnMount{ true }; + + ThingType* m_thingType{ nullptr }; + + float m_sizeFactor{ 1.0 }; + Timer m_animationTimer; + + Otc::Direction m_direction{ Otc::North }; + + std::array m_offsetDirections[2]; + + PainterShaderProgramPtr m_shader; + + friend class Creature; + friend class PaperdollManager; +}; diff --git a/src/client/paperdollmanager.cpp b/src/client/paperdollmanager.cpp new file mode 100644 index 0000000000..548363a585 --- /dev/null +++ b/src/client/paperdollmanager.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010-2025 OTClient + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "paperdollmanager.h" +#include "paperdoll.h" +#include "thingtypemanager.h" + +PaperdollManager g_paperdolls; + +PaperdollPtr PaperdollManager::getById(uint16_t id) { + const auto it = m_paperdolls.find(id); + if (it == m_paperdolls.end()) { + g_logger.error(std::format("PaperdollManager::getById(%d): not found.", id)); + return nullptr; + } + + const auto& obj = (*it).second; + if (obj->m_thingId > 0 && obj->m_thingType == nullptr) { + if (!g_things.isValidDatId(obj->m_thingId, ThingCategoryCreature)) { + g_logger.error(std::format("PaperdollManager::getById(%d): invalid thing with id %d.", id, obj->m_thingId)); + return nullptr; + } + + obj->m_thingType = g_things.getThingType(obj->m_thingId, ThingCategoryCreature).get(); + } + + return obj; +} + +PaperdollPtr PaperdollManager::set(uint16_t id, uint16_t thingId) { + const auto it = m_paperdolls.find(id); + if (it != m_paperdolls.end()) { + g_logger.error(std::format("PaperdollManager::register(%d, %d): has already been registered.", id, thingId)); + return nullptr; + } + + const auto& obj = std::make_shared(); + obj->m_id = id; + obj->m_thingId = thingId; + + m_paperdolls.emplace(id, obj); + return obj; +} \ No newline at end of file diff --git a/src/client/paperdollmanager.h b/src/client/paperdollmanager.h new file mode 100644 index 0000000000..4afefc3339 --- /dev/null +++ b/src/client/paperdollmanager.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2025 OTClient + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "declarations.h" + +class PaperdollManager +{ +public: + PaperdollPtr set(uint16_t id, uint16_t thingId); + PaperdollPtr getById(uint16_t id); + + void remove(uint16_t id) { m_paperdolls.erase(id); } + void clear() { m_paperdolls.clear(); } + +private: + stdext::map m_paperdolls; +}; + +extern PaperdollManager g_paperdolls; diff --git a/src/client/protocolcodes.h b/src/client/protocolcodes.h index a1ff5e381a..4a60b1e0ce 100644 --- a/src/client/protocolcodes.h +++ b/src/client/protocolcodes.h @@ -77,6 +77,8 @@ namespace Proto GameServerCreatureShader = 54, GameServerMapShader = 55, GameServerCreatureTyping = 56, + GameServerAttachedPaperdoll = 60, + GameServerDetachPaperdoll = 61, GameServerFeatures = 67, GameServerFloorDescription = 75, diff --git a/src/client/protocolgame.h b/src/client/protocolgame.h index 38d9cc7a6b..3ed8202462 100644 --- a/src/client/protocolgame.h +++ b/src/client/protocolgame.h @@ -366,6 +366,9 @@ class ProtocolGame final : public Protocol void parseCreatureShader(const InputMessagePtr& msg); void parseMapShader(const InputMessagePtr& msg); + void parseAttachedPaperdoll(const InputMessagePtr& msg); + void parseDetachPaperdoll(const InputMessagePtr& msg); + MarketOffer readMarketOffer(const InputMessagePtr& msg, uint8_t action, uint16_t var); Imbuement getImbuementInfo(const InputMessagePtr& msg); @@ -385,6 +388,8 @@ class ProtocolGame final : public Protocol Position getPosition(const InputMessagePtr& msg); private: + PaperdollPtr getPaperdoll(const InputMessagePtr& msg) const; + bool m_enableSendExtendedOpcode{ false }; bool m_gameInitialized{ false }; bool m_mapKnown{ false }; diff --git a/src/client/protocolgameparse.cpp b/src/client/protocolgameparse.cpp index 713c4baf48..759a221500 100644 --- a/src/client/protocolgameparse.cpp +++ b/src/client/protocolgameparse.cpp @@ -38,6 +38,8 @@ #include "thingtypemanager.h" #include "framework/core/eventdispatcher.h" #include "framework/net/inputmessage.h" +#include "paperdollmanager.h" +#include "paperdoll.h" void ProtocolGame::parseMessage(const InputMessagePtr& msg) { @@ -147,6 +149,12 @@ void ProtocolGame::parseMessage(const InputMessagePtr& msg) case Proto::GameServerCreatureTyping: parseCreatureTyping(msg); break; + case Proto::GameServerAttachedPaperdoll: + parseAttachedPaperdoll(msg); + break; + case Proto::GameServerDetachPaperdoll: + parseDetachPaperdoll(msg); + break; case Proto::GameServerFeatures: parseFeatures(msg); break; @@ -447,7 +455,7 @@ void ProtocolGame::parseMessage(const InputMessagePtr& msg) case Proto::GameServerLootContainers: parseLootContainers(msg); break; - case Proto::GameServerVirtue: + case Proto::GameServerVirtue: parseVirtue(msg); break; case Proto::GameServerCyclopediaHouseAuctionMessage: @@ -3885,6 +3893,15 @@ CreaturePtr ProtocolGame::getCreature(const InputMessagePtr& msg, int type) cons unpass = static_cast(msg->getU8()); } + if (g_game.getFeature(Otc::GameCreaturePaperdoll)) { + uint8_t size = msg->getU8(); + for (uint8_t i = 0; i < size; ++i) { + const auto& paperdoll = getPaperdoll(msg); + if (creature) + creature->attachPaperdoll(paperdoll); + } + } + std::string shader; if (g_game.getFeature(Otc::GameCreatureShader)) { shader = msg->getString(); @@ -5649,7 +5666,6 @@ void ProtocolGame::parseImbuementWindow(const InputMessagePtr& msg) const uint32_t unknown = msg->getU32(); g_lua.callGlobalField("g_game", "onOpenImbuementWindow", itemId, unknown); - } else if (windowType == Otc::IMBUEMENT_WINDOW_SELECT_ITEM) { const uint16_t itemId = msg->getU16(); const auto& item = Item::create(itemId); @@ -5697,7 +5713,6 @@ void ProtocolGame::parseImbuementWindow(const InputMessagePtr& msg) } g_lua.callGlobalField("g_game", "onImbuementItem", itemId, slot, activeSlots, imbuements, neededItemsList); - } else if (windowType == Otc::IMBUEMENT_WINDOW_SCROLL) { msg->getU8(); // unknown byte msg->getU8(); // unknown byte @@ -6250,7 +6265,77 @@ void ProtocolGame::parseWeaponProficiencyInfo(const InputMessagePtr& msg) const uint8_t size = msg->getU8(); for (auto j = 0; j < size; ++j) { - msg->getU8(); // proficiencyLevel - msg->getU8(); // perkPosition + msg->getU8(); // proficiencyLevel + msg->getU8(); // perkPosition + } +} + +void ProtocolGame::parseAttachedPaperdoll(const InputMessagePtr& msg) { + const uint32_t id = msg->getU32(); + const auto& paperdoll = getPaperdoll(msg); + + const auto& creature = g_map.getCreatureById(id); + if (!creature) { + g_logger.traceError(std::format("could not get creature with id %d", id)); + return; } + + if (creature->hasPaperdoll(paperdoll->getId())) + return; + + creature->attachPaperdoll(paperdoll); } +void ProtocolGame::parseDetachPaperdoll(const InputMessagePtr& msg) { + const uint32_t id = msg->getU32(); + const bool bySlot = msg->getU8(); + const uint16_t idOrSlot = msg->getU16(); + + const auto& creature = g_map.getCreatureById(id); + if (!creature) { + g_logger.traceError(std::format("could not get creature with id %d", id)); + return; + } + + if (bySlot) + creature->detachPaperdollByPriority(idOrSlot); + else + creature->detachPaperdollById(idOrSlot); +} + +PaperdollPtr ProtocolGame::getPaperdoll(const InputMessagePtr& msg) const { + uint16_t id = msg->getU16(); + uint8_t slot = msg->getU8(); + uint8_t color = msg->getU8(); + uint8_t head = msg->getU8(); + uint8_t body = msg->getU8(); + uint8_t legs = msg->getU8(); + uint8_t feet = msg->getU8(); + const auto& shader = msg->getString(); + + auto paperdoll = g_paperdolls.getById(id); + if (!paperdoll) return nullptr; + + paperdoll = paperdoll->clone(); + if (slot != UINT8_MAX) + paperdoll->setPriority(slot); + + if (color != 0) + paperdoll->setColor(color); + + if (head != 0) + paperdoll->setHeadColor(head); + + if (body != 0) + paperdoll->setBodyColor(body); + + if (legs != 0) + paperdoll->setLegsColor(legs); + + if (feet != 0) + paperdoll->setFeetColor(feet); + + if (!shader.empty()) + paperdoll->setShader(shader); + + return paperdoll; +} \ No newline at end of file diff --git a/vc17/otclient.vcxproj b/vc17/otclient.vcxproj index 8335eea36e..c263b49405 100644 --- a/vc17/otclient.vcxproj +++ b/vc17/otclient.vcxproj @@ -332,6 +332,8 @@ + + false Create @@ -511,6 +513,8 @@ + +