diff --git a/meta.lua b/meta.lua index 142d48167b..cad773f0e2 100644 --- a/meta.lua +++ b/meta.lua @@ -151,7 +151,7 @@ g_things = {} ---@param file string ---@return boolean -function g_things.loadAppearances(file) end +function g_things.loadAppearances(file, resourceId) end ---@param file string ---@return boolean @@ -159,11 +159,11 @@ function g_things.loadStaticData(file) end ---@param file string ---@return boolean -function g_things.loadDat(file) end +function g_things.loadDat(file, resourceId) end ---@param file string ---@return boolean -function g_things.loadOtml(file) end +function g_things.loadOtml(file, resourceId) end ---@return boolean function g_things.isDatLoaded() end @@ -181,7 +181,7 @@ function g_things.getThingType(id, category) end ---@param category integer ---@return ThingType[] -function g_things.getThingTypes(category) end +function g_things.getThingTypes(category, resourceId) end ---@param attr integer ---@param category integer @@ -308,47 +308,6 @@ function g_towns.getTowns() end function g_towns.sort() end --------------------------------- ----------- g_sprites ----------- --------------------------------- - ----@class g_sprites -g_sprites = {} - ----@param file string ----@return boolean -function g_sprites.loadSpr(file) end - -function g_sprites.unload() end - ----@return boolean -function g_sprites.isLoaded() end - ----@return number -function g_sprites.getSprSignature() end - ----@return integer -function g_sprites.getSpritesCount() end - ----* FRAMEWORK_EDITOR ----@param fileName string -function g_sprites.saveSpr(fileName) end - --------------------------------- ------- g_spriteAppearances ----- --------------------------------- - ----@class g_spriteAppearances -g_spriteAppearances = {} - ----@param id integer ----@param file string -function g_spriteAppearances.saveSpriteToFile(id, file) end - ----@param id integer ----@param file string -function g_spriteAppearances.saveSheetToFileBySprite(id, file) end - -------------------------------- ------------ g_map ------------- -------------------------------- diff --git a/modules/client_options/data_options.lua b/modules/client_options/data_options.lua index 0f15c6332e..f3d91f3795 100644 --- a/modules/client_options/data_options.lua +++ b/modules/client_options/data_options.lua @@ -397,7 +397,7 @@ return { asyncTxtLoading = { value = false, action = function(value, options, controller, panels, extraWidgets) - if g_game.isUsingProtobuf() then + if true then --g_game.isUsingProtobuf() then -- deprecated value = true elseif g_app.isEncrypted() then local asyncWidget = panels.graphicsPanel:recursiveGetChildById('asyncTxtLoading') diff --git a/modules/game_actionbar/logics/ActionButtonLogic.lua b/modules/game_actionbar/logics/ActionButtonLogic.lua index 29d882b622..c545d6dd50 100644 --- a/modules/game_actionbar/logics/ActionButtonLogic.lua +++ b/modules/game_actionbar/logics/ActionButtonLogic.lua @@ -682,7 +682,7 @@ function updateButton(button) button.cache = getButtonCache(button) if button.item.getItemId and not button.cache.actionType then - button.item:setItemId(0, true) + button.item:setItemId(0) button.item:setOn(false) end @@ -704,7 +704,7 @@ function updateButton(button) local passiveAbility = buttonData["actionsetting"]["passiveAbility"] if useAction then - button.item:setItemId(useAction, true) + button.item:setItemId(useAction, 0) -- to do: resourceId button.item:setOn(true) local cached = cachedItemWidget[useAction] if cached then diff --git a/modules/game_things/things.lua b/modules/game_things/things.lua index 91331f91eb..fd5270a6b8 100644 --- a/modules/game_things/things.lua +++ b/modules/game_things/things.lua @@ -1,12 +1,7 @@ ThingsLoaderController = Controller:new() -local filename = nil local loaded = false -function setFileName(name) - filename = name -end - function isLoaded() return loaded end @@ -41,55 +36,235 @@ local function tryLoadDatWithFallbacks(datPath) return false end -local function load(version) - local errorList = {} +local function findFileByExtension(path, ext) + -- find Tibia.ext + local fileName = "Tibia" .. ext + local resolvedPath = resolvepath(path .. fileName) + if g_resources.fileExists(resolvedPath) then + return resolvedPath + end - if version >= 1281 and not g_game.getFeature(GameLoadSprInsteadProtobuf) then - local filePath = resolvepath(string.format('/things/%d/', version)) - if not g_things.loadAppearances(filePath) then - errorList[#errorList + 1] = "Couldn't load assets" - end - if not g_things.loadStaticData(filePath) then - errorList[#errorList + 1] = "Couldn't load staticdata" - end - else - local datPath, sprPath - if filename then - datPath = resolvepath('/data/things/' .. filename) - sprPath = resolvepath('/data/things/' .. filename) - else - datPath = resolvepath('/data/things/' .. version .. '/Tibia') - sprPath = resolvepath('/data/things/' .. version .. '/Tibia') + -- find any filename.ext + resolvedPath = resolvepath(path) + local files = g_resources.listDirectoryFiles(resolvedPath) + for _, file in ipairs(files) do + -- match .otfi extension + if file:lower():sub(ext:len()) == ext then + resolvedPath = resolvepath(path .. "/" .. file) + return resolvedPath end + end - g_logger.setLevel(5) - if not tryLoadDatWithFallbacks(datPath) then - errorList[#errorList + 1] = tr('Unable to load dat file, please place a valid dat in \'%s.dat\'', datPath) - end - g_logger.setLevel(1) + -- no file found +end + +-- helper to get boolean from parsed otfi +local function toboolean(v) + if v == nil then + return false + end + + if type(v) == "boolean" then + return v + end - if not g_sprites.loadSpr(sprPath) then - errorList[#errorList + 1] = tr('Unable to load spr file, please place a valid spr in \'%s.spr\'', sprPath) + if type(v) ~= "string" then + return false + end + + v = string.lower(v) + return v == "true" or v == "1" or v == "yes" or v == "on" or v == "enabled" +end + +local function setFeature(feature, value) + if value == nil then + -- use version default if not defined in otfi + return + elseif toboolean(value) then + -- evaluated to true + g_game.enableFeature(feature) + else + -- evaluated to false + g_game.disableFeature(feature) + end +end + +local function addError(errorList, message, resourceId) + if resourceId > 0 then + errorList[#errorList + 1] = string.format("Resource %d: %s", resourceId, message) + else + errorList[#errorList + 1] = message + end +end + +local function loadResource(path, version, resourceId, errorList) + -- file loading fallback order: + -- 1. catalog-content.json - if found: load assets + -- 2. Tibia.otfi - if found: load dat specified in it + -- 3. any otfi - if found: load dat specified in it + -- 4. Tibia.dat + -- 5. any dat + + -- assets + if g_resources.fileExists(resolvepath(path .. 'catalog-content.json')) then + g_logger.info(string.format("Loading resource %d (assets) ...", resourceId)) + if not g_things.loadAppearances(resolvepath(path), resourceId) then + addError(errorList, "Couldn't load assets", resourceId) end - if g_game.getFeature(GameLoadSprInsteadProtobuf) and version >= 1281 then - local staticPath = resolvepath(string.format('/things/%d/appearances', version)) - if not g_things.loadAppearances(staticPath) then - g_logger.warning(string.format( - "[game_things.load()] Couldn't load /things/%d/appearances.dat, possible packets error.", version)) - end + if not g_things.loadStaticData(path) then + addError(errorList, "Couldn't load staticdata", resourceId) end - end + + return + end + + g_logger.info(string.format("Loading resource %d (spr/dat) from %s ...", resourceId, path)) + + -- otfi-defined spr/dat + local otfiPath = findFileByExtension(path, ".otfi") + if otfiPath then + -- read config from otfi + local otfiSettings = g_configs.create(otfiPath) + if not otfiSettings then + addError(errorList, "Failed to load OTFI", resourceId) + return + end + + local datSpr = otfiSettings:getNode("DatSpr") + if not datSpr then + addError(errorList, "Invalid OTFI structure", resourceId) + return + end + + -- nodes priority: + -- 1. otfi assets-name + -- 2. otfi "-file" nodes + -- 3. (if not defined by otfi) Tibia .spr/.dat + local sprName = "Tibia.spr" + local datName = "Tibia.dat" + + local assetsName = datSpr["assets-name"] + if assetsName then + sprName = assetsName .. ".spr" + datName = assetsName .. ".dat" + else + sprName = datSpr["sprites-file"] or sprName + datName = datSpr["metadata-file"] or datName + end + + -- set features according to otfi + setFeature(GameSpritesU32, datSpr["extended"]) + setFeature(GameSpritesAlphaChannel, datSpr["transparency"]) + setFeature(GameIdleAnimations, datSpr["frame-groups"]) + setFeature(GameEnhancedAnimations,datSpr["frame-durations"]) + + -- check if otfi-specified dat file exists + local datPath = resolvepath(path .. datName) + if not g_resources.fileExists(datPath) then + addError(errorList, string.format("Unable to load %s: file not found", datName), resourceId) + return + end + + -- try to load dat file + if not g_things.loadDat(datPath, resourceId) then + addError(errorList, string.format("Failed to read %s: file structure does not match the defined version or OTFI specification", datName), resourceId) + return + end + + -- check if otfi-specified spr file exists + local sprPath = resolvepath(path .. sprName) + if not g_resources.fileExists(sprPath) then + addError(errorList, string.format("Unable to load %s: file not found", datName), resourceId) + return + end + + -- try to load spr file + if not g_things.loadSpr(sprPath, resourceId) then + addError(errorList, string.format("Failed to read %s: file structure does not match the defined version or OTFI specification", datName), resourceId) + return + end + else + -- normal dat file + local datPath = findFileByExtension(path, ".dat") + if not datPath then + addError(errorList, "DAT file not found", resourceId) + return + end + + -- try to load dat + local datResult = tryLoadDatWithFallbacks(datPath, resourceId) + if not datResult then + addError(errorList, "Failed to read dat file: file structure does not match the defined version", resourceId) + end + + -- normal spr file + local sprPath = findFileByExtension(path, ".spr") + if not sprPath then + addError(errorList, "SPR file not found", resourceId) + return + end + + -- try to load spr + local sprResult = g_things.loadSpr(sprPath, resourceId) + if not sprResult then + addError(errorList, "Failed to read dat file: file structure does not match the defined version", resourceId) + end + end +end + +local function loadPackInfo(packPath, errorList) + g_logger.info("Found packinfo.xml") + local packInfo = g_things.decodePackInfo(resolvepath(packPath)) + if #packInfo == 0 then + addError(errorList, "Failed to decode packinfo.xml", 0) + return + end + + for _, resInfo in pairs(packInfo) do + loadResource(string.format("%s%s/", packPath, resInfo.dir), resInfo.version, resInfo.id, errorList) + + if #errorList > 0 then + break + end + end + + -- enable resource ids in packets + -- g_game.enableFeature(GameMultiSpr) +end + +local function load(version) + -- prevent calling again after a failed attempt + if version == 0 then + return + end + + local errorList = {} + local path = string.format('/data/things/%s/', version) + + g_logger.info("Loading game assets from " .. path) + + local packPath = resolvepath(path .. 'packinfo.xml') + if g_resources.fileExists(packPath) then + loadPackInfo(path, errorList) + else + loadResource(path, version, 0, errorList) + end loaded = #errorList == 0 if loaded then -- loading client files was successful, try to load sounds now -- sound files are optional, this means that failing to load them -- will not block logging into game - g_sounds.loadClientFiles(resolvepath(string.format('/sounds/%d/', version))) + if version > 1300 then + g_sounds.loadClientFiles(resolvepath(string.format('/sounds/%d/', version))) + end + + g_logger.info("Assets loading complete.") return end - local messageBox = displayErrorBox(tr('Error'), table.concat(errorList, "\n")) + local errors = table.concat(errorList, "\n") + local messageBox = displayErrorBox(tr('Error'), errors) addEvent(function() messageBox:raise() messageBox:focus() @@ -97,6 +272,7 @@ local function load(version) g_game.setClientVersion(0) g_game.setProtocolVersion(0) + g_logger.error(errors) end function ThingsLoaderController:onInit() diff --git a/modules/gamelib/const.lua b/modules/gamelib/const.lua index fc27673105..1f64389924 100644 --- a/modules/gamelib/const.lua +++ b/modules/gamelib/const.lua @@ -193,7 +193,7 @@ GameAnthem = 95 GameVipGroups = 96 GameBosstiary = 97 GameDoublePlayerGoodsMoney = 98 -GameLoadSprInsteadProtobuf = 100 +GameLoadSprInsteadProtobuf = 100 -- deprecated GameItemShader = 101 GameCreatureShader = 102 GameCreatureAttachedEffect = 103 @@ -222,6 +222,7 @@ GameMapCache = 125 GameForgeSkillStats = 126 GameCharacterSkillStats = 127 GameCreaturePaperdoll = 128 +GameMultiSpr = 129 TextColors = { red = '#f55e5e', -- '#c83200' diff --git a/modules/gamelib/protocollogin.lua b/modules/gamelib/protocollogin.lua index e18a2f1d0c..63c3425557 100644 --- a/modules/gamelib/protocollogin.lua +++ b/modules/gamelib/protocollogin.lua @@ -51,7 +51,7 @@ function ProtocolLogin:sendLoginPacket() else msg:addU32(g_things.getDatSignature()) end - msg:addU32(g_sprites.getSprSignature()) + msg:addU32(g_things.getSprSignature()) msg:addU32(PIC_SIGNATURE) if g_game.getFeature(GamePreviewState) then diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e3c6151b06..e392549865 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -292,7 +292,6 @@ set(SOURCE_FILES client/protocolgamesend.cpp client/paperdoll.cpp client/paperdollmanager.cpp - client/spriteappearances.cpp client/spritemanager.cpp client/statictext.cpp client/thing.cpp diff --git a/src/client/attachableobject.cpp b/src/client/attachableobject.cpp index 1a4a8e01f1..6d4e8f9c2a 100644 --- a/src/client/attachableobject.cpp +++ b/src/client/attachableobject.cpp @@ -48,7 +48,7 @@ void AttachableObject::attachEffect(const AttachedEffectPtr& obj) onStartAttachEffect(obj); - if (obj->isHidedOwner()) + if (obj->isOwnerHidden()) ++m_ownerHidden; if (obj->getDuration() > 0) { @@ -94,7 +94,7 @@ bool AttachableObject::detachEffectById(uint16_t id) void AttachableObject::onDetachEffect(const AttachedEffectPtr& effect, const bool callEvent) { - if (effect->isHidedOwner()) + if (effect->isOwnerHidden()) --m_ownerHidden; onStartDetachEffect(effect); diff --git a/src/client/attachableobject.h b/src/client/attachableobject.h index ad97811618..0ef0f3a3b9 100644 --- a/src/client/attachableobject.h +++ b/src/client/attachableobject.h @@ -73,7 +73,7 @@ class AttachableObject : public LuaObject UIWidgetPtr getAttachedWidgetById(const std::string& id); protected: - struct Data + struct AttachedObjects { std::vector attachedEffects; std::vector attachedParticles; @@ -88,11 +88,11 @@ class AttachableObject : public LuaObject auto getData() { if (!m_data) - m_data = std::make_shared(); + m_data = std::make_shared(); return m_data; } - std::shared_ptr m_data; + std::shared_ptr m_data; uint8_t m_ownerHidden{ 0 }; }; diff --git a/src/client/attachedeffect.cpp b/src/client/attachedeffect.cpp index e70fee822f..c9e05118e0 100644 --- a/src/client/attachedeffect.cpp +++ b/src/client/attachedeffect.cpp @@ -34,8 +34,8 @@ #include "framework/graphics/texture.h" #include "framework/graphics/texturemanager.h" -AttachedEffectPtr AttachedEffect::create(const uint16_t thingId, const ThingCategory category) { - if (!g_things.isValidDatId(thingId, category)) { +AttachedEffectPtr AttachedEffect::create(const uint16_t thingId, const ThingCategory category, const uint16_t resourceId) { + if (!g_things.isValidDatId(thingId, category, resourceId)) { g_logger.error("AttachedEffectManager::getInstance({}, {}): invalid thing with id or category.", thingId, static_cast(category)); return nullptr; } @@ -43,6 +43,7 @@ AttachedEffectPtr AttachedEffect::create(const uint16_t thingId, const ThingCate const auto& obj = std::make_shared(); obj->m_thingId = thingId; obj->m_thingCategory = category; + obj->m_resourceId = resourceId; return obj; } @@ -66,7 +67,6 @@ AttachedEffectPtr AttachedEffect::clone() } } } - return obj; } @@ -182,15 +182,15 @@ int AttachedEffect::getCurrentAnimationPhase() return animator->getPhaseAt(m_animationTimer, getSpeed()); if (thingTye->isEffect()) { - const int lastPhase = thingTye->getAnimationPhases() - 1; + const int lastPhase = thingTye->getAnimationPhase() - 1; const int phase = std::min(static_cast(m_animationTimer.ticksElapsed() / (g_gameConfig.getEffectTicksPerFrame() / getSpeed())), lastPhase); if (phase == lastPhase) m_animationTimer.restart(); return phase; } if (thingTye->isCreature() && thingTye->isAnimateAlways()) { - const int ticksPerFrame = std::round(1000 / thingTye->getAnimationPhases()) / getSpeed(); - return (g_clock.millis() % (static_cast(ticksPerFrame) * thingTye->getAnimationPhases())) / ticksPerFrame; + const int ticksPerFrame = std::round(1000 / thingTye->getAnimationPhase()) / getSpeed(); + return (g_clock.millis() % (static_cast(ticksPerFrame) * thingTye->getAnimationPhase())) / ticksPerFrame; } return 0; @@ -204,5 +204,5 @@ void AttachedEffect::move(const Position& fromPosition, const Position& toPositi } ThingType* AttachedEffect::getThingType() const { - return m_thingId > 0 ? g_things.getRawThingType(m_thingId, m_thingCategory) : nullptr; + return m_thingId > 0 ? g_things.getRawThingType(m_thingId, m_thingCategory, m_resourceId) : nullptr; } \ No newline at end of file diff --git a/src/client/attachedeffect.h b/src/client/attachedeffect.h index 4e8e5eebb3..48c8f6b599 100644 --- a/src/client/attachedeffect.h +++ b/src/client/attachedeffect.h @@ -29,7 +29,7 @@ class AttachedEffect final : public LuaObject { public: - static AttachedEffectPtr create(uint16_t thingId, ThingCategory category); + static AttachedEffectPtr create(const uint16_t thingId, const ThingCategory category, const uint16_t resourceId = 0); void draw(const Point& /*dest*/, bool /*isOnTop*/, LightView* = nullptr, bool drawThing = true); void drawLight(const Point& /*dest*/, LightView*); @@ -47,7 +47,7 @@ class AttachedEffect final : public LuaObject Size getSize() { return m_size; } void setSize(const Size& s) { m_size = s; } - bool isHidedOwner() { return m_hideOwner; } + bool isOwnerHidden() { return m_hideOwner; } void setHideOwner(const bool v) { m_hideOwner = v; } bool isTransform() { return m_transform; } @@ -122,7 +122,7 @@ class AttachedEffect final : public LuaObject uint8_t m_lastAnimation{ 0 }; DrawOrder m_drawOrder{ THIRD }; - uint16_t m_id{ 0 }; + uint16_t m_id{ 0 }; // slot number uint16_t m_duration{ 0 }; uint32_t m_frame{ 0 }; @@ -138,7 +138,8 @@ class AttachedEffect final : public LuaObject Outfit m_outfitOwner; Light m_light; - uint16_t m_thingId{ 0 }; + uint16_t m_thingId{ 0 }; // effect clientId + uint16_t m_resourceId{ 0 }; // effect resourceId ThingCategory m_thingCategory{ ThingInvalidCategory }; Size m_size; diff --git a/src/client/attachedeffectmanager.cpp b/src/client/attachedeffectmanager.cpp index 45527d87d5..9f51e01124 100644 --- a/src/client/attachedeffectmanager.cpp +++ b/src/client/attachedeffectmanager.cpp @@ -36,7 +36,7 @@ AttachedEffectPtr AttachedEffectManager::getById(const uint16_t id) { } const auto& obj = it->second; - if (obj->m_thingId > 0 && !g_things.isValidDatId(obj->m_thingId, obj->m_thingCategory)) { + if (obj->m_thingId > 0 && !g_things.isValidDatId(obj->m_thingId, obj->m_thingCategory, obj->m_resourceId)) { g_logger.error("AttachedEffectManager::getById({}): invalid thing with id {}.", id, obj->m_thingId); return nullptr; } @@ -44,7 +44,7 @@ AttachedEffectPtr AttachedEffectManager::getById(const uint16_t id) { return obj->clone(); } -AttachedEffectPtr AttachedEffectManager::registerByThing(uint16_t id, const std::string_view name, const uint16_t thingId, const ThingCategory category) { +AttachedEffectPtr AttachedEffectManager::registerByThing(uint16_t id, const std::string_view name, const uint16_t thingId, const ThingCategory category, const uint16_t resourceId) { const auto it = m_effects.find(id); if (it != m_effects.end()) { g_logger.error("AttachedEffectManager::registerByThing({}, {}): has already been registered.", id, name); @@ -54,6 +54,7 @@ AttachedEffectPtr AttachedEffectManager::registerByThing(uint16_t id, const std: const auto& obj = std::make_shared(); obj->m_id = id; obj->m_thingId = thingId; + obj->m_resourceId = resourceId; obj->m_thingCategory = category; obj->m_name = { name.data() }; diff --git a/src/client/attachedeffectmanager.h b/src/client/attachedeffectmanager.h index 45ac4d4fbb..80da8ce9a9 100644 --- a/src/client/attachedeffectmanager.h +++ b/src/client/attachedeffectmanager.h @@ -27,7 +27,7 @@ class AttachedEffectManager { public: - AttachedEffectPtr registerByThing(uint16_t id, std::string_view name, uint16_t thingId, ThingCategory category); + AttachedEffectPtr registerByThing(uint16_t id, std::string_view name, uint16_t thingId, ThingCategory category, uint16_t resourceId = 0); AttachedEffectPtr registerByImage(uint16_t id, std::string_view name, std::string_view path, bool smooth); AttachedEffectPtr getById(uint16_t id); diff --git a/src/client/client.cpp b/src/client/client.cpp index c9f41b4f1b..805d9ed81f 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -27,7 +27,6 @@ #include "map.h" #include "mapview.h" #include "minimap.h" -#include "spriteappearances.h" #include "spritemanager.h" #include "thingtypemanager.h" #include "uimap.h" @@ -52,8 +51,6 @@ void Client::init(std::vector& /*args*/) g_minimap.init(); g_game.init(); g_shaders.init(); - g_sprites.init(); - g_spriteAppearances.init(); g_things.init(); } @@ -68,8 +65,6 @@ void Client::terminate() g_map.terminate(); g_minimap.terminate(); g_things.terminate(); - g_sprites.terminate(); - g_spriteAppearances.terminate(); g_shaders.terminate(); g_paperdolls.clear(); g_gameConfig.terminate(); @@ -139,17 +134,17 @@ bool Client::canDraw(const DrawPoolType type) const bool Client::isLoadingAsyncTexture() { - return g_game.isUsingProtobuf(); + return true; // deprecated / to be discussed } bool Client::isUsingProtobuf() { - return g_game.isUsingProtobuf(); + return true; // deprecated / to be discussed } void Client::onLoadingAsyncTextureChanged(bool /*loadingAsync*/) { - g_sprites.reload(); + g_things.reloadSprites(); } void Client::doMapScreenshot(std::string file) diff --git a/src/client/const.h b/src/client/const.h index 4b95e8d23d..9fdf1e8f01 100644 --- a/src/client/const.h +++ b/src/client/const.h @@ -547,7 +547,7 @@ namespace Otc GameDoublePlayerGoodsMoney = 98, // others - GameLoadSprInsteadProtobuf = 100, + GameLoadSprInsteadProtobuf = 100, // deprecated GameItemShader = 101, GameCreatureShader = 102, GameCreatureAttachedEffect = 103, @@ -576,6 +576,7 @@ namespace Otc GameForgeSkillStats = 126, GameCharacterSkillStats = 127, GameCreaturePaperdoll = 128, + GameMultiSpr = 129, LastGameFeature }; @@ -877,7 +878,9 @@ namespace Otc { IMBUEMENT_WINDOW_CHOICE = 0, IMBUEMENT_WINDOW_SELECT_ITEM = 1, - IMBUEMENT_WINDOW_SCROLL = 2 + IMBUEMENT_WINDOW_SCROLL = 2, + + IMBUEMENT_WINDOW_LAST }; enum Vocations_t : uint8_t diff --git a/src/client/creature.cpp b/src/client/creature.cpp index ecbf0ab1e8..eaafeb0610 100644 --- a/src/client/creature.cpp +++ b/src/client/creature.cpp @@ -74,10 +74,13 @@ void Creature::draw(const Point& dest, const bool drawThings, LightView* /*light return; if (drawThings) { + // the black frame that shows when the creature attacks the player + // supports multiple colors if (m_showTimedSquare) { g_drawPool.addBoundingRect(Rect(dest + (m_walkOffset - getDisplacement() + 2) * g_drawPool.getScaleFactor(), Size(28 * g_drawPool.getScaleFactor())), m_timedSquareColor, std::max(static_cast(2 * g_drawPool.getScaleFactor()), 1)); } + // pvp aggression frame if (m_showStaticSquare) { g_drawPool.addBoundingRect(Rect(dest + (m_walkOffset - getDisplacement()) * g_drawPool.getScaleFactor(), Size(g_gameConfig.getSpriteSize() * g_drawPool.getScaleFactor())), m_staticSquareColor, std::max(static_cast(2 * g_drawPool.getScaleFactor()), 1)); } @@ -177,6 +180,11 @@ void Creature::drawInformation(const MapPosInfo& mapRect, const Point& dest, con const auto& creatureOffset = Point(16 - displacementX, -displacementY - 2) + getDrawOffset(); Point p = dest - mapRect.drawOffset; + + // name bouncing together with flying creature + // to do in the future: option to disable it if causes motion sickness? + p -= getBounceOffset(); + p += (creatureOffset - Point(std::round(m_jumpOffset.x), std::round(m_jumpOffset.y))) * mapRect.scaleFactor; p.x *= mapRect.horizontalStretchFactor; p.y *= mapRect.verticalStretchFactor; @@ -304,6 +312,181 @@ void Creature::drawInformation(const MapPosInfo& mapRect, const Point& dest, con g_drawPool.resetDrawOrder(); } +void Creature::drawOutfit(Point& dest, const Color& color, const bool replaceColorShader) +{ + // note: drawing mount before checking if the outfit is creature is correct + // the vanilla client also allows items to be drawn on top of mounts + // protocol 1320 example (server function "addcreature"): u16 looktype 0, u16 looktypeEx (itemId), u16 lookMount, 4x u8 mount color + + const float scale = g_drawPool.getScaleFactor(); + + // determines the frame group to be used + const bool flying = m_outfit.hasWings(); + const uint16_t animationPhase = getCurrentAnimationPhase(getThingType(), flying); + + // paperdoll (bottom) + const bool mounted = m_outfit.hasMount(); + for (const auto& paperdoll : m_paperdolls) + paperdoll->draw(dest, animationPhase, mounted, false, true, color); + + // mount + if (m_outfit.hasMount()) { + if (auto* thing = getMountThingType()) + drawCreatureMount(dest, color, getCurrentAnimationPhase(thing), replaceColorShader); + + dest += getDisplacement() * scale; + dest -= getMountThingType()->getDisplacement() * scale; + } + + // wings (direction south, east) + bool drawWings = flying; + if (drawWings && (m_direction == Otc::South || m_direction == Otc::East)) { + // draw wings thing type + if (auto* wings = getWingsThingType()) + wings->draw(dest, 0, m_numPatternX, 0, 0, getCurrentAnimationPhase(wings), color); + + // wings already drawn + drawWings = false; + } + + // base outfit + if (m_outfit.isCreature()) { + // outfit is a real creature + drawCreatureOutfit(dest, color, animationPhase, replaceColorShader); + + // paperdoll (top) + for (const auto& paperdoll : m_paperdolls) + paperdoll->draw(dest, animationPhase, mounted, true, true, color); + + } else { + // outfit is a creature imitating an item or the invisible effect + drawItemOutfit(dest, color, replaceColorShader); + } + + // wings (direction north, west) + if (drawWings) { + // draw wings thing type + if (auto* wings = getWingsThingType()) + wings->draw(dest, 0, m_numPatternX, 0, 0, getCurrentAnimationPhase(wings), color); + } + + // particles + if (m_outfit.hasParticles()) { + // draw particles + } +} + +void Creature::drawCreatureOutfit(Point& dest, const Color& color, const int animationPhase, const bool replaceColorShader) +{ + const auto& datType = getThingType(); + const bool useFramebuffer = !replaceColorShader && hasShader() && g_shaders.getShaderById(m_shaderId)->useFramebuffer(); + + const auto& drawCreature = [&](const Point& dest) { + // yPattern => creature addon + for (int yPattern = 0; yPattern < getNumPatternY(); ++yPattern) { + // continue if we dont have this addon + if (yPattern > 0 && !(m_outfit.getAddons() & (1 << (yPattern - 1)))) + continue; + + if (!replaceColorShader && hasShader() && !useFramebuffer) { + g_drawPool.setShaderProgram(g_shaders.getShaderById(m_shaderId), true/*, shaderAction*/); + } + + datType->draw(dest, 0, m_numPatternX, yPattern, m_numPatternZ, animationPhase, color); + + // mount colors + if (m_drawOutfitColor && !replaceColorShader && getLayers() > 1) { + auto colors = m_outfit.getBaseOutfit(); + g_drawPool.setCompositionMode(CompositionMode::MULTIPLY); + datType->draw(dest, SpriteMaskYellow, m_numPatternX, yPattern, m_numPatternZ, animationPhase, colors.headColor); + datType->draw(dest, SpriteMaskRed, m_numPatternX, yPattern, m_numPatternZ, animationPhase, colors.bodyColor); + datType->draw(dest, SpriteMaskGreen, m_numPatternX, yPattern, m_numPatternZ, animationPhase, colors.legsColor); + datType->draw(dest, SpriteMaskBlue, m_numPatternX, yPattern, m_numPatternZ, animationPhase, colors.feetColor); + g_drawPool.resetCompositionMode(); + } + } + }; + + if (useFramebuffer) { + const int size = static_cast(g_gameConfig.getSpriteSize() * std::max(datType->getSize().area(), 2) * g_drawPool.getScaleFactor()); + const auto& p = (Point(size) - Point(datType->getExactHeight())) / 2; + const auto& destFB = Rect(dest - p, Size{ size }); + + g_drawPool.setShaderProgram(g_shaders.getShaderById(m_shaderId), true/*, shaderAction*/); + + g_drawPool.bindFrameBuffer(destFB.size()); + drawCreature(p); + g_drawPool.releaseFrameBuffer(destFB); + g_drawPool.resetShaderProgram(); + } else drawCreature(dest); +} + +void Creature::drawCreatureMount(Point& dest, const Color& color, const int animationPhase, const bool replaceColorShader) +{ + const auto& datType = getMountThingType(); + const bool useFramebuffer = !replaceColorShader && hasMountShader() && g_shaders.getShaderById(m_mountShaderId)->useFramebuffer(); + const auto& drawCreature = [&](const Point& dest) { + if (!replaceColorShader && hasMountShader() && !useFramebuffer) { + g_drawPool.setShaderProgram(g_shaders.getShaderById(m_mountShaderId), true/*, [this]()-> void { + m_mountShader->bind(); + m_mountShader->setUniformValue(ShaderManager::MOUNT_ID_UNIFORM, m_outfit.getMount()); + }*/); + } + + datType->draw(dest, 0, m_numPatternX, 0, 0, animationPhase, color); + + if (m_drawOutfitColor && !replaceColorShader && getLayers() > 1) { + auto colors = m_outfit.getMount(); + g_drawPool.setCompositionMode(CompositionMode::MULTIPLY); + datType->draw(dest, SpriteMaskYellow, m_numPatternX, 0, 0, animationPhase, colors.headColor); + datType->draw(dest, SpriteMaskRed, m_numPatternX, 0, 0, animationPhase, colors.bodyColor); + datType->draw(dest, SpriteMaskGreen, m_numPatternX, 0, 0, animationPhase, colors.legsColor); + datType->draw(dest, SpriteMaskBlue, m_numPatternX, 0, 0, animationPhase, colors.feetColor); + g_drawPool.resetCompositionMode(); + } + }; + + if (useFramebuffer) { + const int size = static_cast(g_gameConfig.getSpriteSize() * std::max(datType->getSize().area(), 2) * g_drawPool.getScaleFactor()); + const auto& p = (Point(size) - Point(datType->getExactHeight())) / 2; + const auto& destFB = Rect(dest - p, Size{ size }); + + g_drawPool.setShaderProgram(g_shaders.getShaderById(m_mountShaderId), true/*, shaderAction*/); + + g_drawPool.bindFrameBuffer(destFB.size()); + drawCreature(p); + g_drawPool.releaseFrameBuffer(destFB); + g_drawPool.resetShaderProgram(); + } else drawCreature(dest); +} + +void Creature::drawItemOutfit(Point& dest, const Color& color, const bool replaceColorShader) +{ + int animationPhases = getThingType()->getAnimationPhase(); + int animateTicks = g_gameConfig.getItemTicksPerFrame(); + + // when creature is an effect we cant render the first and last animation phase, + // instead we should loop in the phases between + if (m_outfit.isEffect()) { + animationPhases = std::max(1, animationPhases - 2); + animateTicks = g_gameConfig.getInvisibleTicksPerFrame(); + } + + int animationPhase = 0; + if (auto* animator = getThingType()->getIdleAnimator(); animator && m_outfit.isItem()) { + animationPhase = animator->getPhase(); + } else if (animationPhases > 1) { + animationPhase = (g_clock.millis() % (static_cast(animateTicks) * animationPhases)) / animateTicks; + } + + if (m_outfit.isEffect()) + animationPhase = std::min(animationPhase + 1, animationPhases); + + if (!replaceColorShader && hasShader()) + g_drawPool.setShaderProgram(g_shaders.getShaderById(m_shaderId), true/*, shaderAction*/); + getThingType()->draw(dest - (getDisplacement() * g_drawPool.getScaleFactor()), 0, 0, 0, 0, animationPhase, color); +} + void Creature::internalDraw(Point dest, const Color& color) { // Example of how to send a UniformValue to shader @@ -314,14 +497,41 @@ void Creature::internalDraw(Point dest, const Color& color) };*/ Point originalDest = dest; + const float scale = g_drawPool.getScaleFactor(); + const bool visible = !isHidden(); + + // aura settings + const bool drawAura = m_outfit.hasAura(); + Point auraDest = dest; + Point topAuraDest = dest; + ThingType* aura = nullptr; + uint16_t auraPhase = m_walkAnimationPhase; + if (visible) { + // aura bottom layer (pattern_z = 0) + if (drawAura) { + const auto auraType = m_outfit.getAura(); + if (aura = g_things.getRawThingType(auraType.type, auraType.category, auraType.resourceId)) { + int auraHeight = aura->getHeight(); + int auraWidth = aura->getWidth(); + if (auraHeight > 1 || auraWidth > 1) { + Point offset = Point(auraWidth > 1 ? (auraWidth - 1) * 16 : 0, auraHeight > 1 ? (auraHeight - 1) * 16 : 0); + topAuraDest += offset * scale; + auraDest += offset * scale; + } + + // draw aura + auto anim = aura->getIdleAnimator(); + auraPhase = anim ? anim->getPhase() : auraPhase; + aura->draw(auraDest, 0, 0, 0, 0, auraPhase, color); + } + } + } if (!m_jumpOffset.isNull()) { - const auto& jumpOffset = m_jumpOffset * g_drawPool.getScaleFactor(); + const auto& jumpOffset = m_jumpOffset * scale; dest -= Point(std::round(jumpOffset.x), std::round(jumpOffset.y)); - } else if (m_bounce.height > 0 && m_bounce.speed > 0) { - const auto minHeight = m_bounce.minHeight * g_drawPool.getScaleFactor(); - const auto height = m_bounce.height * g_drawPool.getScaleFactor(); - dest -= minHeight + (height - std::abs(height - static_cast(m_bounce.timer.ticksElapsed() / (m_bounce.speed / 100.f)) % static_cast(height * 2))); + } else { + dest -= getBounceOffset(); } const bool replaceColorShader = color != Color::white; @@ -330,97 +540,14 @@ void Creature::internalDraw(Point dest, const Color& color) else drawAttachedEffect(originalDest, 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()) { - dest -= getMountThingType()->getDisplacement() * g_drawPool.getScaleFactor(); - - if (!replaceColorShader && hasMountShader()) { - g_drawPool.setShaderProgram(g_shaders.getShaderById(m_mountShaderId), true/*, [this]()-> void { - m_mountShader->bind(); - m_mountShader->setUniformValue(ShaderManager::MOUNT_ID_UNIFORM, m_outfit.getMount()); - }*/); - } - getMountThingType()->draw(dest, 0, m_numPatternX, 0, 0, getCurrentAnimationPhase(true), color); - - dest += getDisplacement() * g_drawPool.getScaleFactor(); - } - - const auto& datType = getThingType(); - const bool useFramebuffer = !replaceColorShader && hasShader() && g_shaders.getShaderById(m_shaderId)->useFramebuffer(); - - const auto& drawCreature = [&](const Point& dest) { - // yPattern => creature addon - for (int yPattern = 0; yPattern < getNumPatternY(); ++yPattern) { - // continue if we dont have this addon - if (yPattern > 0 && !(m_outfit.getAddons() & (1 << (yPattern - 1)))) - continue; - - if (!replaceColorShader && hasShader() && !useFramebuffer) { - g_drawPool.setShaderProgram(g_shaders.getShaderById(m_shaderId), true/*, shaderAction*/); - } - - datType->draw(dest, 0, m_numPatternX, yPattern, m_numPatternZ, animationPhase, color); - - if (m_drawOutfitColor && !replaceColorShader && getLayers() > 1) { - g_drawPool.setCompositionMode(CompositionMode::MULTIPLY); - datType->draw(dest, SpriteMaskYellow, m_numPatternX, yPattern, m_numPatternZ, animationPhase, m_outfit.getHeadColor()); - datType->draw(dest, SpriteMaskRed, m_numPatternX, yPattern, m_numPatternZ, animationPhase, m_outfit.getBodyColor()); - datType->draw(dest, SpriteMaskGreen, m_numPatternX, yPattern, m_numPatternZ, animationPhase, m_outfit.getLegsColor()); - datType->draw(dest, SpriteMaskBlue, m_numPatternX, yPattern, m_numPatternZ, animationPhase, m_outfit.getFeetColor()); - g_drawPool.resetCompositionMode(); - } - } - }; - - if (useFramebuffer) { - const int size = static_cast(g_gameConfig.getSpriteSize() * std::max(datType->getSize().area(), 2) * g_drawPool.getScaleFactor()); - const auto& p = (Point(size) - Point(datType->getExactHeight())) / 2; - const auto& destFB = Rect(dest - p, Size{ size }); - - g_drawPool.setShaderProgram(g_shaders.getShaderById(m_shaderId), true/*, shaderAction*/); - - g_drawPool.bindFrameBuffer(destFB.size()); - drawCreature(p); - g_drawPool.releaseFrameBuffer(destFB); - 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(); - int animateTicks = g_gameConfig.getItemTicksPerFrame(); - - // when creature is an effect we cant render the first and last animation phase, - // instead we should loop in the phases between - if (m_outfit.isEffect()) { - animationPhases = std::max(1, animationPhases - 2); - animateTicks = g_gameConfig.getInvisibleTicksPerFrame(); - } - - int animationPhase = 0; - if (auto* animator = getThingType()->getIdleAnimator(); animator && m_outfit.isItem()) { - animationPhase = animator->getPhase(); - } else if (animationPhases > 1) { - animationPhase = (g_clock.millis() % (static_cast(animateTicks) * animationPhases)) / animateTicks; - } - - if (m_outfit.isEffect()) - animationPhase = std::min(animationPhase + 1, animationPhases); + if (visible) { + drawOutfit(dest, color, replaceColorShader); + } - if (!replaceColorShader && hasShader()) - g_drawPool.setShaderProgram(g_shaders.getShaderById(m_shaderId), true/*, shaderAction*/); - getThingType()->draw(dest - (getDisplacement() * g_drawPool.getScaleFactor()), 0, 0, 0, 0, animationPhase, color); - } + // aura top layer (pattern_z = 1) + if (aura && aura->getNumPatternZ() > 0) { + // draw aura + aura->draw(topAuraDest, 0, 0, 0, 1, auraPhase, color); } if (replaceColorShader) @@ -431,6 +558,16 @@ void Creature::internalDraw(Point dest, const Color& color) } } +int Creature::getBounceOffset() const +{ + if (m_bounce.height <= 0 || m_bounce.speed <= 0) + return 0; + + const auto minHeight = m_bounce.minHeight * g_drawPool.getScaleFactor(); + const auto height = m_bounce.height * g_drawPool.getScaleFactor(); + return minHeight + (height - std::abs(height - static_cast(m_bounce.timer.ticksElapsed() / (m_bounce.speed / 100.f)) % static_cast(height * 2))); +} + void Creature::turn(const Otc::Direction direction) { // schedules to set the new direction when walk ends @@ -482,6 +619,15 @@ void Creature::stopWalk() terminateWalk(); } +void Creature::updateFlight() +{ + if (m_outfit.hasWings()) { + setBounce(5, 6, 10000); + } else { + setBounce(0, 0, 0); + } +} + void Creature::jump(const int height, const int duration) { if (!m_jumpOffset.isNull()) @@ -617,7 +763,7 @@ void Creature::updateWalkAnimation() if (!m_outfit.isCreature()) return; - int footAnimPhases = m_outfit.hasMount() ? getMountThingType()->getAnimationPhases() : getAnimationPhases(); + int footAnimPhases = m_outfit.hasMount() ? getMountThingType()->getAnimationPhase() : getAnimationPhases(); if (!g_game.getFeature(Otc::GameEnhancedAnimations) && footAnimPhases > 2) { --footAnimPhases; } @@ -868,10 +1014,12 @@ void Creature::setOutfit(const Outfit& outfit, bool fireEvent) m_numPatternZ = std::min(1, getNumPatternZ() - 1); } - if ((g_game.getFeature(Otc::GameWingsAurasEffectsShader))) { - m_outfit.setWing(0); - m_outfit.setAura(0); - m_outfit.setEffect(0); + if (g_game.getFeature(Otc::GameWingsAurasEffectsShader)) { + updateFlight(); + } else { + m_outfit.applyWings(SimpleOutfit()); + m_outfit.applyAura(EffectOutfit()); + m_outfit.applyParticles(EffectOutfit()); m_outfit.setShader("Outfit - Default"); } @@ -1128,27 +1276,42 @@ const Light& Creature::getLight() const } ThingType* Creature::getThingType() const { - return g_things.getRawThingType(m_outfit.isCreature() ? m_outfit.getId() : m_outfit.getAuxId(), m_outfit.getCategory()); + return g_things.getRawThingType(m_outfit.isCreature() ? m_outfit.getId() : m_outfit.getAuxId(), m_outfit.getCategory(), m_outfit.getResourceId()); } ThingType* Creature::getMountThingType() const { - return m_outfit.hasMount() ? g_things.getRawThingType(m_outfit.getMount(), ThingCategoryCreature) : nullptr; + if (!m_outfit.hasMount()) + return nullptr; + + const auto thing = m_outfit.getMount(); + return g_things.getRawThingType(thing.type, ThingCategoryCreature, thing.resourceId); } -uint16_t Creature::getCurrentAnimationPhase(const bool mount) +ThingType* Creature::getWingsThingType() const { - if (!canAnimate()) return 0; + if (!m_outfit.hasWings()) + return nullptr; - const auto thingType = mount ? getMountThingType() : getThingType(); + const auto thing = m_outfit.getWings(); + return g_things.getRawThingType(thing.type, ThingCategoryCreature, thing.resourceId); +} + +uint16_t Creature::getCurrentAnimationPhase(const ThingType* thingType, bool idle) +{ + if (!canAnimate() || !thingType) return 0; if (const auto idleAnimator = thingType->getIdleAnimator()) { - if (m_walkAnimationPhase == 0) return idleAnimator->getPhase(); + // idle animation + if (m_walkAnimationPhase == 0 || idle) + return idleAnimator->getPhase(); + + // movement animation return m_walkAnimationPhase + idleAnimator->getAnimationPhases() - 1; } if (thingType->isAnimateAlways()) { - const int ticksPerFrame = std::round(1000 / thingType->getAnimationPhases()); - return (g_clock.millis() % (static_cast(ticksPerFrame) * thingType->getAnimationPhases())) / ticksPerFrame; + const int ticksPerFrame = std::round(1000 / thingType->getAnimationPhase()); + return (g_clock.millis() % (static_cast(ticksPerFrame) * thingType->getAnimationPhase())) / ticksPerFrame; } return isDisabledWalkAnimation() ? 0 : m_walkAnimationPhase; @@ -1224,14 +1387,17 @@ void Creature::onDispatcherAttachEffect(const AttachedEffectPtr& effect) { effect->m_outfitOwner = outfit; + ThingType* effectThingType = effect->getThingType(); + Outfit newOutfit = outfit; newOutfit.setTemp(true); - newOutfit.setCategory(effect->getThingType()->getCategory()); + newOutfit.setCategory(effectThingType->getCategory()); if (newOutfit.isCreature()) - newOutfit.setId(effect->getThingType()->getId()); + newOutfit.setId(effectThingType->getId()); else - newOutfit.setAuxId(effect->getThingType()->getId()); + newOutfit.setAuxId(effectThingType->getId()); + newOutfit.setResourceId(effectThingType->getResourceId()); setOutfit(newOutfit); } } diff --git a/src/client/creature.h b/src/client/creature.h index 0c05dcf38b..a9c48a194b 100644 --- a/src/client/creature.h +++ b/src/client/creature.h @@ -51,10 +51,12 @@ class Creature : public Thing void draw(const Rect& destRect, uint8_t size, bool center = false); void drawLight(const Point& dest, LightView* lightView) override; - void internalDraw(Point dest, const Color& color = Color::white); void drawInformation(const MapPosInfo& mapRect, const Point& dest, int drawFlags); - void setId(const uint32_t id) override { m_id = id; } + // note: unlike other classes that derive from Thing, this one sets creatureId + // rather than clientId. The resourceId is managed in creature outfit. + void setId(const uint32_t id) { m_id = id; } + void setMasterId(const uint32_t id) { m_masterId = id; } void setName(std::string_view name); void setHealthPercent(uint8_t healthPercent); @@ -95,6 +97,7 @@ class Creature : public Thing void allowAppearWalk() { m_allowAppearWalk = true; } virtual void walk(const Position& oldPos, const Position& newPos); virtual void stopWalk(); + void updateFlight(); bool isDrawingOutfitColor() const { return m_drawOutfitColor; } void setDrawOutfitColor(const bool draw) { m_drawOutfitColor = draw; } @@ -171,9 +174,10 @@ class Creature : public Thing bool getTyping() { return m_typing; } void setTypingIconTexture(const std::string& filename); void setBounce(const uint8_t minHeight, const uint8_t height, const uint16_t speed) { - m_bounce = { .minHeight = -minHeight, -.height = height, .speed = speed + m_bounce = { + .minHeight = minHeight, + .height = height, + .speed = speed, }; } @@ -226,6 +230,7 @@ minHeight, ThingType* getThingType() const override; ThingType* getMountThingType() const; + ThingType* getWingsThingType() const; void onDeath(); void onPositionChange(const Position& newPos, const Position& oldPos) override; @@ -240,13 +245,21 @@ minHeight, int16_t m_lastMapDuration = -1; private: + // methods for drawing creature sprites specifically + void drawOutfit(Point& dest, const Color& color, const bool replaceColorShader); + void drawCreatureOutfit(Point& dest, const Color& color, const int animationPhase, const bool replaceColorShader); + void drawCreatureMount(Point& dest, const Color& color, const int animationPhase, const bool replaceColorShader); + void drawItemOutfit(Point& dest, const Color& color, const bool replaceColorShader); + void internalDraw(Point dest, const Color& color = Color::white); + int getBounceOffset() const; + void nextWalkUpdate(); void updateJump(); void updateShield(); void updateWalkingTile(); void updateWalkAnimation(); - uint16_t getCurrentAnimationPhase(bool mount = false); + uint16_t getCurrentAnimationPhase(const ThingType* thingType, bool idle = false); struct CachedStep { diff --git a/src/client/declarations.h b/src/client/declarations.h index cf8da95b54..3917ee083d 100644 --- a/src/client/declarations.h +++ b/src/client/declarations.h @@ -59,6 +59,10 @@ class TileBlock; class AttachedEffect; class AttachableObject; class Paperdoll; +class AssetResource; +class ISpriteManager; +class ProtobufSpriteManager; +struct AssetResourceInfo; #ifdef FRAMEWORK_EDITOR class House; @@ -87,6 +91,9 @@ using ItemTypePtr = std::shared_ptr; using AttachedEffectPtr = std::shared_ptr; using AttachableObjectPtr = std::shared_ptr; using PaperdollPtr = std::shared_ptr; +using AssetResourcePtr = std::shared_ptr; +using SpriteManagerPtr = std::shared_ptr; +using ProtobufSpriteManagerPtr = std::shared_ptr; #ifdef FRAMEWORK_EDITOR using HousePtr = std::shared_ptr; @@ -103,6 +110,9 @@ using SpawnMap = std::unordered_map; using ThingList = std::vector; using ThingTypeList = std::vector; using ItemTypeList = std::vector; +using AssetResourceList = std::vector; +using SpriteManagerList = std::vector; +using PackInfoResourceList = std::vector; using TileList = std::list; using ItemVector = std::vector; diff --git a/src/client/effect.cpp b/src/client/effect.cpp index f5f6c32a42..f6ea87a25c 100644 --- a/src/client/effect.cpp +++ b/src/client/effect.cpp @@ -35,7 +35,7 @@ void Effect::draw(const Point& dest, const bool drawThings, LightView* lightView) { - if (!canDraw() || isHided()) + if (!canDraw() || isHidden()) return; // It only starts to draw when the first effect as it is about to end. @@ -132,9 +132,9 @@ bool Effect::waitFor(const EffectPtr& effect) return true; } -void Effect::setId(const uint32_t id) +void Effect::setId(const uint32_t id, const uint16_t resourceId) { - if (!g_things.isValidDatId(id, ThingCategoryEffect)) + if (!g_things.isValidDatId(id, ThingCategoryEffect, resourceId)) return; m_clientId = id; @@ -158,5 +158,5 @@ void Effect::setPosition(const Position& position, const uint8_t stackPos) } ThingType* Effect::getThingType() const { - return g_things.getRawThingType(m_clientId, ThingCategoryEffect); + return g_things.getRawThingType(m_clientId, ThingCategoryEffect, m_resourceId); } \ No newline at end of file diff --git a/src/client/effect.h b/src/client/effect.h index adda68c95c..eb4cedb899 100644 --- a/src/client/effect.h +++ b/src/client/effect.h @@ -29,7 +29,7 @@ class Effect final : public Thing { public: void draw(const Point& /*dest*/, bool drawThings = true, LightView* = nullptr) override; - void setId(uint32_t id) override; + void setId(uint32_t id, uint16_t resourceId = 0); void setPosition(const Position& position, uint8_t stackPos = 0) override; bool isEffect() const override { return true; } diff --git a/src/client/game.cpp b/src/client/game.cpp index 0c69c615fd..83683f1d83 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -404,13 +404,19 @@ void Game::processRemoveAutomapFlag(const Position& pos, const uint8_t icon, con g_lua.callGlobalField("g_game", "onRemoveAutomapFlag", pos, icon, message); } -void Game::processOpenOutfitWindow(const Outfit& currentOutfit, const std::vector>& outfitList, - const std::vector>& mountList, - const std::vector>& familiarList, - const std::vector>& wingsList, - const std::vector>& aurasList, - const std::vector>& effectList, - const std::vector>& shaderList) +void Game::processOpenOutfitWindow( + const Outfit& currentOutfit, + const std::vector& outfitList, + const std::vector& mountList, + const std::vector& familiarList, + const std::vector& wingsList, + const std::vector& aurasList, + const std::vector& effectsList, + const std::vector& shaderList, + const uint8_t windowType, + const bool mounted, + const bool randomizeMount +) { // create virtual creature outfit const auto& virtualOutfitCreature = std::make_shared(); @@ -423,7 +429,7 @@ void Game::processOpenOutfitWindow(const Outfit& currentOutfit, const std::vecto CreaturePtr virtualMountCreature; if (getFeature(Otc::GamePlayerMounts)) { Outfit mountOutfit; - mountOutfit.setId(currentOutfit.getMount()); + mountOutfit.applyOutfit(currentOutfit.getMount()); mountOutfit.setCategory(ThingCategoryCreature); virtualMountCreature = std::make_shared(); @@ -431,13 +437,23 @@ void Game::processOpenOutfitWindow(const Outfit& currentOutfit, const std::vecto virtualMountCreature->setOutfit(mountOutfit); } + // create virtual familiar outfit + CreaturePtr virtualFamiliarCreature; if (getFeature(Otc::GamePlayerFamiliars)) { Outfit familiarOutfit; - familiarOutfit.setId(currentOutfit.getFamiliar()); + familiarOutfit.applySimpleOutfit(currentOutfit.getFamiliar()); familiarOutfit.setCategory(ThingCategoryCreature); + + virtualFamiliarCreature = std::make_shared(); + virtualFamiliarCreature->setDirection(Otc::South); + virtualFamiliarCreature->setOutfit(familiarOutfit); } - g_lua.callGlobalField("g_game", "onOpenOutfitWindow", virtualOutfitCreature, outfitList, virtualMountCreature, mountList, familiarList, wingsList, aurasList, effectList, shaderList); + g_lua.callGlobalField("g_game", "onOpenOutfitWindow", + virtualOutfitCreature, virtualMountCreature, virtualFamiliarCreature, + outfitList, mountList, familiarList, wingsList, aurasList, effectsList, shaderList, + windowType, mounted, randomizeMount + ); } void Game::processOpenNpcTrade(const std::vector>& items) @@ -470,9 +486,9 @@ void Game::processCloseTrade() g_lua.callGlobalField("g_game", "onCloseTrade"); } -void Game::processEditText(const uint32_t id, const uint32_t itemId, const uint16_t maxLength, const std::string_view text, const std::string_view writer, const std::string_view date) +void Game::processEditText(const uint32_t id, const uint32_t itemId, const uint16_t resourceId, const uint16_t maxLength, const std::string_view text, const std::string_view writer, const std::string_view date) { - g_lua.callGlobalField("g_game", "onEditText", id, itemId, maxLength, text, writer, date); + g_lua.callGlobalField("g_game", "onEditText", id, itemId, maxLength, text, writer, date, resourceId); } void Game::processEditList(const uint32_t id, const uint8_t doorId, const std::string_view text) @@ -497,9 +513,9 @@ void Game::processModalDialog(const uint32_t id, const std::string_view title, c g_lua.callGlobalField("g_game", "onModalDialog", id, title, message, buttonList, enterButton, escapeButton, choiceList, priority); } -void Game::processItemDetail(const uint32_t itemId, const std::vector>& descriptions) +void Game::processItemDetail(const uint32_t itemId, const std::vector>& descriptions, const uint16_t resourceId) { - g_lua.callGlobalField("g_game", "onParseItemDetail", itemId, descriptions); + g_lua.callGlobalField("g_game", "onParseItemDetail", itemId, descriptions, resourceId); } void Game::processCyclopediaCharacterGeneralStats(const CyclopediaCharacterGeneralStats& stats, const std::vector>& skills, @@ -510,7 +526,7 @@ void Game::processCyclopediaCharacterGeneralStats(const CyclopediaCharacterGener void Game::processCyclopediaCharacterCombatStats(const CyclopediaCharacterCombatStats& data, const double mitigation, const std::vector>& additionalSkillsArray, const std::vector>& forgeSkillsArray, const std::vector& perfectShotDamageRangesArray, - const std::vector>& combatsArray, const std::vector>& concoctionsArray) + const std::vector>& combatsArray, const std::vector>& concoctionsArray) { g_lua.callGlobalField("g_game", "onParseCyclopediaCharacterCombatStats", data, mitigation, additionalSkillsArray, forgeSkillsArray, perfectShotDamageRangesArray, combatsArray, concoctionsArray); } @@ -784,7 +800,7 @@ void Game::look(const ThingPtr& thing, const bool isBattleList) m_protocolGame->sendLookCreature(thing->getId()); else { const int thingId = thing->isCreature() ? static_cast(Proto::Creature) : thing->getId(); - m_protocolGame->sendLook(thing->getPosition(), thingId, thing->getStackPos()); + m_protocolGame->sendLook(thing->getPosition(), thingId, thing->getResourceId(), thing->getStackPos()); } } @@ -797,7 +813,7 @@ void Game::move(const ThingPtr& thing, const Position& toPos, int count) return; const auto thingId = thing->isCreature() ? static_cast(Proto::Creature) : thing->getId(); - m_protocolGame->sendMove(thing->getPosition(), thingId, thing->getStackPos(), toPos, count); + m_protocolGame->sendMove(thing->getPosition(), thingId, thing->getResourceId(), thing->getStackPos(), toPos, count); } void Game::moveToParentContainer(const ThingPtr& thing, const int count) @@ -841,9 +857,9 @@ void Game::use(const ThingPtr& thing) g_lua.callGlobalField("g_game", "onUse", pos, thing->getId(), thing->getStackPos(), 0); } -void Game::useInventoryItem(const uint16_t itemId) +void Game::useInventoryItem(const uint16_t itemId, const uint16_t resourceId) { - if (!canPerformGameAction() || !g_things.isValidDatId(itemId, ThingCategoryItem)) + if (!canPerformGameAction() || !g_things.isValidDatId(itemId, ThingCategoryItem, resourceId)) return; const auto& pos = Position(0xFFFF, 0, 0); // means that is a item in inventory @@ -1331,7 +1347,7 @@ void Game::inspectNpcTrade(const ItemPtr& item) if (!canPerformGameAction() || !item) return; - m_protocolGame->sendInspectNpcTrade(item->getId(), item->getCount()); + m_protocolGame->sendInspectNpcTrade(item->getId(), item->getResourceId(), item->getCount()); } void Game::buyItem(const ItemPtr& item, const uint16_t amount, const bool ignoreCapacity, const bool buyWithBackpack) @@ -1339,7 +1355,7 @@ void Game::buyItem(const ItemPtr& item, const uint16_t amount, const bool ignore if (!canPerformGameAction() || !item) return; - m_protocolGame->sendBuyItem(item->getId(), item->getCountOrSubType(), amount, ignoreCapacity, buyWithBackpack); + m_protocolGame->sendBuyItem(item->getId(), item->getResourceId(), item->getCountOrSubType(), amount, ignoreCapacity, buyWithBackpack); } void Game::sellItem(const ItemPtr& item, const uint16_t amount, const bool ignoreEquipped) @@ -1347,7 +1363,7 @@ void Game::sellItem(const ItemPtr& item, const uint16_t amount, const bool ignor if (!canPerformGameAction() || !item) return; - m_protocolGame->sendSellItem(item->getId(), item->getSubType(), amount, ignoreEquipped); + m_protocolGame->sendSellItem(item->getId(), item->getResourceId(), item->getSubType(), amount, ignoreEquipped); } void Game::closeNpcTrade() @@ -1363,7 +1379,7 @@ void Game::requestTrade(const ItemPtr& item, const CreaturePtr& creature) if (!canPerformGameAction() || !item || !creature) return; - m_protocolGame->sendRequestTrade(item->getPosition(), item->getId(), item->getStackPos(), creature->getId()); + m_protocolGame->sendRequestTrade(item->getPosition(), item->getId(), item->getResourceId(), item->getStackPos(), creature->getId()); } void Game::inspectTrade(const bool counterOffer, const uint8_t index) @@ -1473,25 +1489,25 @@ void Game::equipItem(const ItemPtr& item) return; if (g_game.getFeature(Otc::GameThingUpgradeClassification) && item->getClassification() > 0) { - m_protocolGame->sendEquipItemWithTier(item->getId(), item->getTier()); + m_protocolGame->sendEquipItemWithTier(item->getId(), item->getResourceId(), item->getTier()); } else { - m_protocolGame->sendEquipItemWithCountOrSubType(item->getId(), item->getCountOrSubType()); + m_protocolGame->sendEquipItemWithCountOrSubType(item->getId(), item->getResourceId(), item->getCountOrSubType()); } } -void Game::equipItemId(const uint16_t itemId, const uint8_t tier) +void Game::equipItemId(const uint16_t itemId, const uint8_t tier, const uint16_t resourceId) { if (!canPerformGameAction()) return; if (g_game.getFeature(Otc::GameThingUpgradeClassification)) { - const auto& thing = g_things.getThingType(itemId, ThingCategoryItem); + const auto& thing = g_things.getThingType(itemId, ThingCategoryItem, resourceId); if (thing && thing->getClassification() > 0) { - m_protocolGame->sendEquipItemWithTier(itemId, tier); + m_protocolGame->sendEquipItemWithTier(itemId, tier, resourceId); return; } } - m_protocolGame->sendEquipItemWithCountOrSubType(itemId, tier); + m_protocolGame->sendEquipItemWithCountOrSubType(itemId, tier, resourceId); } void Game::mount(const bool mount) @@ -1507,7 +1523,7 @@ void Game::requestItemInfo(const ItemPtr& item, const uint8_t index) if (!canPerformGameAction()) return; - m_protocolGame->sendRequestItemInfo(item->getId(), item->getSubType(), index); + m_protocolGame->sendRequestItemInfo(item->getId(), item->getResourceId(), item->getSubType(), index); } void Game::answerModalDialog(const uint32_t dialog, const uint8_t button, const uint8_t choice) @@ -1766,21 +1782,21 @@ void Game::leaveMarket() g_lua.callGlobalField("g_game", "onMarketLeave"); } -void Game::browseMarket(const uint8_t browseId, const uint16_t browseType, const uint8_t tier) +void Game::browseMarket(const uint8_t browseId, const uint16_t browseType, const uint8_t tier, const uint16_t resourceId) { if (!canPerformGameAction()) { return; } - m_protocolGame->sendMarketBrowse(browseId, browseType, tier); + m_protocolGame->sendMarketBrowse(browseId, browseType, tier, resourceId); } -void Game::createMarketOffer(const uint8_t type, const uint16_t itemId, const uint8_t itemTier, const uint16_t amount, const uint64_t price, const uint8_t anonymous) +void Game::createMarketOffer(const uint8_t type, const uint16_t itemId, const uint16_t resourceId, const uint8_t itemTier, const uint16_t amount, const uint64_t price, const uint8_t anonymous) { if (!canPerformGameAction()) return; - m_protocolGame->sendMarketCreateOffer(type, itemId, itemTier, amount, price, anonymous); + m_protocolGame->sendMarketCreateOffer(type, itemId, resourceId, itemTier, amount, price, anonymous); } void Game::cancelMarketOffer(const uint32_t timestamp, const uint16_t counter) @@ -1822,11 +1838,21 @@ void Game::openPortableForgeRequest() m_protocolGame->sendOpenPortableForge(); } -void Game::forgeRequest(Otc::ForgeAction_t actionType, bool convergence, uint16_t firstItemid, uint8_t firstItemTier, uint16_t secondItemId, bool improveChance, bool tierLoss) +void Game::forgeRequest( + Otc::ForgeAction_t actionType, + bool convergence, + uint16_t firstItemid, + uint8_t firstItemTier, + uint16_t secondItemId, + bool improveChance, + bool tierLoss, + uint16_t firstItemResourceId, + uint16_t secondItemResourceId +) { if (!canPerformGameAction()) return; - m_protocolGame->sendForgeRequest(actionType, convergence, firstItemid, firstItemTier, secondItemId, improveChance, tierLoss); + m_protocolGame->sendForgeRequest(actionType, convergence, firstItemid, firstItemResourceId, firstItemTier, secondItemId, secondItemResourceId, improveChance, tierLoss); } void Game::sendForgeBrowseHistoryRequest(uint16_t page) @@ -1868,20 +1894,20 @@ void Game::imbuementDurations(const bool isOpen) m_protocolGame->sendImbuementDurations(isOpen); } -void Game::stashWithdraw(const uint16_t itemId, const uint32_t count, const uint8_t stackpos) +void Game::stashWithdraw(const uint16_t itemId, const uint32_t count, const uint8_t stackpos, const uint16_t resourceId) { if (!canPerformGameAction()) return; - m_protocolGame->sendStashWithdraw(itemId, count, stackpos); + m_protocolGame->sendStashWithdraw(itemId, resourceId, count, stackpos); } -void Game::stashStowItem(const Position& position, const uint16_t itemId, const uint32_t count, const uint8_t stackpos, const uint8_t action) +void Game::stashStowItem(const Position& position, const uint16_t itemId, const uint32_t count, const uint8_t stackpos, const uint8_t action, const uint16_t resourceId) { if (!canPerformGameAction()) return; - m_protocolGame->sendStashStow(position, itemId, count, stackpos, action); + m_protocolGame->sendStashStow(position, itemId, resourceId, count, stackpos, action); } void Game::requestHighscore(const uint8_t action, const uint8_t category, const uint32_t vocation, const std::string_view world, const uint8_t worldType, const uint8_t battlEye, const uint16_t page, const uint8_t totalPages) @@ -1914,10 +1940,13 @@ void Game::sendQuickLoot(const uint8_t variant, const ItemPtr& item) if (!canPerformGameAction()) return; - Position pos = (item && item->getPosition().isValid()) ? item->getPosition() : Position(0, 0, 0); - uint16_t itemId = item ? item->getId() : 0; - uint8_t stackPos = item ? item->getStackPos() : 0; - m_protocolGame->sendQuickLoot(variant, pos, itemId, stackPos); + if (!item) { + m_protocolGame->sendQuickLoot(variant, Position(0, 0, 0), 0, 0, 0); + return; + } + + Position pos = item->getPosition().isValid() ? item->getPosition() : Position(0, 0, 0); + m_protocolGame->sendQuickLoot(variant, pos, item->getId(), item->getResourceId(), item->getStackPos()); } void Game::requestQuickLootBlackWhiteList(const uint8_t filter, const uint16_t size, const std::vector& listedItems) @@ -1928,11 +1957,11 @@ void Game::requestQuickLootBlackWhiteList(const uint8_t filter, const uint16_t s m_protocolGame->requestQuickLootBlackWhiteList(filter, size, listedItems); } -void Game::openContainerQuickLoot(const uint8_t action, const uint8_t category, const Position& pos, const uint16_t itemId, const uint8_t stackpos, const bool useMainAsFallback) +void Game::openContainerQuickLoot(const uint8_t action, const uint8_t category, const Position& pos, const uint16_t itemId, const uint8_t stackpos, const bool useMainAsFallback, const uint16_t resourceId) { if (!canPerformGameAction()) return; - m_protocolGame->openContainerQuickLoot(action, category, pos, itemId, stackpos, useMainAsFallback); + m_protocolGame->openContainerQuickLoot(action, category, pos, itemId, resourceId, stackpos, useMainAsFallback); } void Game::sendGmTeleport(const Position& pos) @@ -1951,12 +1980,12 @@ void Game::inspectionNormalObject(const Position& position) m_protocolGame->sendInspectionNormalObject(position); } -void Game::inspectionObject(const Otc::InspectObjectTypes inspectionType, const uint16_t itemId, const uint8_t itemCount) +void Game::inspectionObject(const Otc::InspectObjectTypes inspectionType, const uint16_t itemId, const uint8_t itemCount, const uint16_t resourceId) { if (!canPerformGameAction()) return; - m_protocolGame->sendInspectionObject(inspectionType, itemId, itemCount); + m_protocolGame->sendInspectionObject(inspectionType, itemId, resourceId, itemCount); } void Game::requestBestiary() diff --git a/src/client/game.h b/src/client/game.h index a6fc46c590..f988e35936 100644 --- a/src/client/game.h +++ b/src/client/game.h @@ -100,13 +100,19 @@ class Game static void processRemoveAutomapFlag(const Position& pos, uint8_t icon, std::string_view message); // outfit - void processOpenOutfitWindow(const Outfit& currentOutfit, const std::vector>& outfitList, - const std::vector>& mountList, - const std::vector>& familiarList, - const std::vector>& wingsList, - const std::vector>& aurasList, - const std::vector>& effectsList, - const std::vector>& shaderList); + void processOpenOutfitWindow( + const Outfit& currentOutfit, + const std::vector& outfitList, + const std::vector& mountList, + const std::vector& familiarList, + const std::vector& wingsList, + const std::vector& aurasList, + const std::vector& effectsList, + const std::vector& shaderList, + const uint8_t windowType, + const bool mounted, + const bool randomizeMount + ); // npc trade static void processOpenNpcTrade(const std::vector>& items); @@ -119,7 +125,7 @@ class Game static void processCloseTrade(); // edit text/list - static void processEditText(uint32_t id, uint32_t itemId, uint16_t maxLength, std::string_view text, std::string_view writer, std::string_view date); + static void processEditText(uint32_t id, uint32_t itemId, uint16_t resourceId, uint16_t maxLength, std::string_view text, std::string_view writer, std::string_view date); static void processEditList(uint32_t id, uint8_t doorId, std::string_view text); // questlog @@ -132,14 +138,14 @@ class Game & choiceList, bool priority); // cyclopedia - static void processItemDetail(uint32_t itemId, const std::vector>& descriptions); + static void processItemDetail(uint32_t itemId, const std::vector>& descriptions, uint16_t resourceId); static void processCyclopediaCharacterGeneralStats(const CyclopediaCharacterGeneralStats& stats, const std::vector>& skills, const std::vector>& combats); static void processCyclopediaCharacterCombatStats(const CyclopediaCharacterCombatStats& data, double mitigation, const std::vector>& additionalSkillsArray, const std::vector>& forgeSkillsArray, const std::vector& perfectShotDamageRangesArray, const std::vector>& combatsArray, - const std::vector>& concoctionsArray); + const std::vector>& concoctionsArray); static void processCyclopediaCharacterGeneralStatsBadge(uint8_t showAccountInformation, uint8_t playerOnline, uint8_t playerPremium, std::string_view loyaltyTitle, const std::vector>& badgesVector); @@ -181,7 +187,7 @@ class Game void wrap(const ThingPtr& thing); void use(const ThingPtr& thing); void useWith(const ItemPtr& item, const ThingPtr& toThing); - void useInventoryItem(uint16_t itemId); + void useInventoryItem(uint16_t itemId, uint16_t resourceId = 0); void useInventoryItemWith(uint16_t itemId, const ThingPtr& toThing); ItemPtr findItemInContainers(uint32_t itemId, int subType, uint8_t tier); @@ -284,7 +290,7 @@ class Game // 870 only void equipItem(const ItemPtr& item); - void equipItemId(const uint16_t itemId, const uint8_t tier); + void equipItemId(const uint16_t itemId, const uint8_t tier, const uint16_t resourceId = 0); void mount(bool mount); // 910 only @@ -326,8 +332,6 @@ class Game void setProtocolVersion(uint16_t version); int getProtocolVersion() { return m_protocolVersion; } - bool isUsingProtobuf() { return getProtocolVersion() >= 1281 && !getFeature(Otc::GameLoadSprInsteadProtobuf); } - void setClientVersion(uint16_t version); int getClientVersion() { return m_clientVersion; } @@ -371,8 +375,9 @@ class Game // market related void leaveMarket(); - void browseMarket(uint8_t browseId, uint16_t browseType, uint8_t tier = 0); - void createMarketOffer(uint8_t type, uint16_t itemId, uint8_t itemTier, uint16_t amount, uint64_t price, uint8_t anonymous); + + void browseMarket(uint8_t browseId, uint16_t browseType, uint8_t tier = 0, uint16_t resourceId = 0); + void createMarketOffer(uint8_t type, uint16_t itemId, uint16_t resourceId, uint8_t itemTier, uint16_t amount, uint64_t price, uint8_t anonymous); void cancelMarketOffer(uint32_t timestamp, uint16_t counter); void acceptMarketOffer(uint32_t timestamp, uint16_t counter, uint16_t amount); @@ -382,7 +387,17 @@ class Game // forge related void openPortableForgeRequest(); - void forgeRequest(Otc::ForgeAction_t actionType, bool convergence = false, uint16_t firstItemid = 0, uint8_t firstItemTier = 0, uint16_t secondItemId = 0, bool improveChance = false, bool tierLoss = false); + void forgeRequest( + Otc::ForgeAction_t actionType, + bool convergence = false, + uint16_t firstItemid = 0, + uint8_t firstItemTier = 0, + uint16_t secondItemId = 0, + bool improveChance = false, + bool tierLoss = false, + uint16_t firstItemResourceId = 0, + uint16_t secondItemResourceId = 0 + ); void sendForgeBrowseHistoryRequest(uint16_t page); // imbuing related @@ -394,9 +409,9 @@ class Game void enableTileThingLuaCallback(const bool value) { m_tileThingsLuaCallback = value; } bool isTileThingLuaCallbackEnabled() { return m_tileThingsLuaCallback; } - void stashWithdraw(uint16_t itemId, uint32_t count, uint8_t stackpos); + void stashWithdraw(uint16_t itemId, uint32_t count, uint8_t stackpos, uint16_t resourceId = 0); - void stashStowItem(const Position& position, const uint16_t itemId, const uint32_t count, const uint8_t stackpos, const uint8_t action); + void stashStowItem(const Position& position, const uint16_t itemId, const uint32_t count, const uint8_t stackpos, const uint8_t action, const uint16_t resourceId = 0); // highscore related void requestHighscore(uint8_t action, uint8_t category, uint32_t vocation, std::string_view world, uint8_t worldType, uint8_t battlEye, uint16_t page, uint8_t totalPages); @@ -411,13 +426,13 @@ class Game // quickLoot related void sendQuickLoot(const uint8_t variant, const ItemPtr& item); void requestQuickLootBlackWhiteList(uint8_t filter, uint16_t size, const std::vector& listedItems); - void openContainerQuickLoot(uint8_t action, uint8_t category, const Position& pos, uint16_t itemId, uint8_t stackpos, bool useMainAsFallback); + void openContainerQuickLoot(uint8_t action, uint8_t category, const Position& pos, uint16_t itemId, uint8_t stackpos, bool useMainAsFallback, uint16_t resourceId = 0); void sendGmTeleport(const Position& pos); // cyclopedia related void inspectionNormalObject(const Position& position); - void inspectionObject(Otc::InspectObjectTypes inspectionType, uint16_t itemId, uint8_t itemCount); + void inspectionObject(Otc::InspectObjectTypes inspectionType, uint16_t itemId, uint8_t itemCount, uint16_t resourceId = 0); void requestBestiary(); void requestBestiaryOverview(std::string_view catName, bool search = false, std::vector raceIds = {}); void requestBestiarySearch(uint16_t raceId); diff --git a/src/client/item.cpp b/src/client/item.cpp index ab6ef8036f..cd26d15698 100644 --- a/src/client/item.cpp +++ b/src/client/item.cpp @@ -39,17 +39,17 @@ #include "itemtype.h" #endif -ItemPtr Item::create(const int id) +ItemPtr Item::create(const int id, uint16_t resourceId) { const auto& item = std::make_shared(); - item->setId(id); + item->setId(id, resourceId); return item; } void Item::draw(const Point& dest, const bool drawThings, LightView* lightView) { - if (!canDraw(m_color) || isHided()) + if (!canDraw(m_color) || isHidden()) return; // determine animation phase @@ -90,7 +90,11 @@ void Item::internalDraw(const int animationPhase, const Point& dest, const Color void Item::drawLight(const Point& dest, LightView* lightView) { if (!lightView) return; - getThingType()->draw(dest, 0, m_numPatternX, m_numPatternY, m_numPatternZ, 0, Color::white, false, lightView); + + auto thingType = getThingType(); + if (!thingType) return; + + thingType->draw(dest, 0, m_numPatternX, m_numPatternY, m_numPatternZ, 0, Color::white, false, lightView); drawAttachedLightEffect(dest, lightView); } @@ -264,9 +268,9 @@ int Item::calculateAnimationPhase() return m_phase; } -void Item::setId(uint32_t id) +void Item::setId(uint32_t id, uint16_t resourceId) { - if (!g_things.isValidDatId(id, ThingCategoryItem)) + if (!g_things.isValidDatId(id, ThingCategoryItem, resourceId)) id = 0; #ifdef FRAMEWORK_EDITOR @@ -274,6 +278,7 @@ void Item::setId(uint32_t id) #endif m_clientId = id; + m_resourceId = resourceId; // Shader example on only items that can be marketed. /* @@ -284,7 +289,7 @@ void Item::setId(uint32_t id) } ThingType* Item::getThingType() const { - return g_things.getRawThingType(m_clientId, ThingCategoryItem); + return g_things.getRawThingType(m_clientId, ThingCategoryItem, m_resourceId); } #ifdef FRAMEWORK_EDITOR @@ -311,7 +316,7 @@ void Item::setOtbId(uint16_t id) m_serverId = id; id = itemType->getClientId(); - if (!g_things.isValidDatId(id, ThingCategoryItem)) + if (!g_things.isValidDatId(id, ThingCategoryItem, m_resourceId)) id = 0; m_clientId = id; diff --git a/src/client/item.h b/src/client/item.h index 3a14aa87e0..ec208802f2 100644 --- a/src/client/item.h +++ b/src/client/item.h @@ -73,12 +73,12 @@ enum ItemAttr : uint8_t class Item final : public Thing { public: - static ItemPtr create(int id); + static ItemPtr create(int id, uint16_t resourceId); void draw(const Point& dest, bool drawThings = true, LightView* lightView = nullptr) override; void drawLight(const Point& dest, LightView* lightView) override; - void setId(uint32_t id) override; + void setId(uint32_t id, uint16_t resourceId); void setCountOrSubType(const int value) { m_countOrSubType = value; updatePatterns(); } void setCount(const int count) { m_countOrSubType = count; updatePatterns(); } diff --git a/src/client/localplayer.cpp b/src/client/localplayer.cpp index c9a00ad97e..e55b36e8a7 100644 --- a/src/client/localplayer.cpp +++ b/src/client/localplayer.cpp @@ -457,7 +457,7 @@ void LocalPlayer::setInventoryItem(const Otc::InventorySlot inventory, const Ite callLuaField("onInventoryChange", inventory, item, oldItem); } -void LocalPlayer::setInventoryCountCache(std::map, uint32_t> counts) +void LocalPlayer::setInventoryCountCache(std::vector counts) { m_inventoryCountCache = std::move(counts); } @@ -483,41 +483,21 @@ bool LocalPlayer::hasEquippedItemId(const uint16_t itemId, const uint8_t tier) return false; } -uint32_t LocalPlayer::getInventoryCount(const uint16_t itemId, const uint8_t tier) +uint32_t LocalPlayer::getInventoryCount(const uint16_t itemId, const uint8_t tier, const uint16_t resourceId) { if (std::cmp_equal(itemId, 0)) return 0; - const auto key = std::make_pair(itemId, tier); - const auto it = m_inventoryCountCache.find(key); - if (it != m_inventoryCountCache.end()) { - return it->second; - } - - uint32_t total = 0; - - const auto accumulate = [&](const ItemPtr& item) { - if (item && std::cmp_equal(item->getId(), itemId) && item->getTier() == tier) { - total += item->getCount(); + for (const auto& item : m_inventoryCountCache) { + if (item.id == itemId && + item.resourceId == resourceId && + item.subType == tier) + { + return item.count; } - }; - - for (const auto& item : m_inventoryItems) - accumulate(item); - - for (const auto& [containerId, container] : g_game.getContainers()) { - if (!container) - continue; - - for (const auto& item : container->getItems()) - accumulate(item); - } - - if (const uint64_t maxUint32 = std::numeric_limits::max(); total > maxUint32) { - total = maxUint32; } - return total; + return 0; } void LocalPlayer::setPremium(const bool premium) diff --git a/src/client/localplayer.h b/src/client/localplayer.h index f3e798c94c..2b4d4f7aca 100644 --- a/src/client/localplayer.h +++ b/src/client/localplayer.h @@ -53,7 +53,7 @@ class LocalPlayer final : public Player void setKnown(const bool known) { m_known = known; } void setPendingGame(const bool pending) { m_pending = pending; } void setInventoryItem(Otc::InventorySlot inventory, const ItemPtr& item); - void setInventoryCountCache(std::map, uint32_t> counts); + void setInventoryCountCache(std::vector counts); void setPremium(bool premium); void setRegenerationTime(uint16_t regenerationTime); void setOfflineTrainingTime(uint16_t offlineTrainingTime); @@ -102,7 +102,7 @@ class LocalPlayer final : public Player const std::vector& getSpells() { return m_spells; } ItemPtr getInventoryItem(const Otc::InventorySlot inventory) { return m_inventoryItems[inventory]; } bool hasEquippedItemId(uint16_t itemId, uint8_t tier); - uint32_t getInventoryCount(uint16_t itemId, uint8_t tier); + uint32_t getInventoryCount(uint16_t itemId, uint8_t tier, uint16_t resourceId = 0); uint64_t getResourceBalance(const Otc::ResourceTypes_t type) { @@ -186,7 +186,7 @@ class LocalPlayer final : public Player stdext::map m_resourcesBalance; std::map m_combatAbsorbValues; std::map m_experienceRates; - std::map, uint32_t> m_inventoryCountCache; + std::vector m_inventoryCountCache; uint8_t m_autoWalkRetries{ 0 }; diff --git a/src/client/luafunctions.cpp b/src/client/luafunctions.cpp index 1414c8d9c8..53846f552c 100644 --- a/src/client/luafunctions.cpp +++ b/src/client/luafunctions.cpp @@ -38,7 +38,6 @@ #include "outfit.h" #include "player.h" #include "protocolgame.h" -#include "spriteappearances.h" #include "spritemanager.h" #include "statictext.h" #include "thingtypemanager.h" @@ -74,9 +73,12 @@ void Client::registerLuaFunctions() g_lua.registerSingletonClass("g_things"); g_lua.bindSingletonFunction("g_things", "loadAppearances", &ThingTypeManager::loadAppearances, &g_things); g_lua.bindSingletonFunction("g_things", "loadStaticData", &ThingTypeManager::loadStaticData, &g_things); + g_lua.bindSingletonFunction("g_things", "decodePackInfo", &ThingTypeManager::decodePackInfo, &g_things); g_lua.bindSingletonFunction("g_things", "loadDat", &ThingTypeManager::loadDat, &g_things); + g_lua.bindSingletonFunction("g_things", "loadSpr", &ThingTypeManager::loadSpr, &g_things); g_lua.bindSingletonFunction("g_things", "loadOtml", &ThingTypeManager::loadOtml, &g_things); g_lua.bindSingletonFunction("g_things", "isDatLoaded", &ThingTypeManager::isDatLoaded, &g_things); + g_lua.bindSingletonFunction("g_things", "getSprSignature", &ThingTypeManager::getSprSignature, &g_things); g_lua.bindSingletonFunction("g_things", "getDatSignature", &ThingTypeManager::getDatSignature, &g_things); g_lua.bindSingletonFunction("g_things", "getContentRevision", &ThingTypeManager::getContentRevision, &g_things); g_lua.bindSingletonFunction("g_things", "getThingType", &ThingTypeManager::getThingType, &g_things); @@ -84,6 +86,7 @@ void Client::registerLuaFunctions() g_lua.bindSingletonFunction("g_things", "findThingTypeByAttr", &ThingTypeManager::findThingTypeByAttr, &g_things); g_lua.bindSingletonFunction("g_things", "getRaceData", &ThingTypeManager::getRaceData, &g_things); g_lua.bindSingletonFunction("g_things", "getRacesByName", &ThingTypeManager::getRacesByName, &g_things); + g_lua.bindSingletonFunction("g_things", "isUsingProtobuf", &ThingTypeManager::isUsingProtobuf, &g_things); #ifdef FRAMEWORK_EDITOR g_lua.bindSingletonFunction("g_things", "getItemType", &ThingTypeManager::getItemType, &g_things); @@ -96,6 +99,7 @@ void Client::registerLuaFunctions() g_lua.bindSingletonFunction("g_things", "loadOtb", &ThingTypeManager::loadOtb, &g_things); g_lua.bindSingletonFunction("g_things", "loadXml", &ThingTypeManager::loadXml, &g_things); g_lua.bindSingletonFunction("g_things", "isOtbLoaded", &ThingTypeManager::isOtbLoaded, &g_things); + g_lua.bindSingletonFunction("g_things", "saveSpr", &ThingTypeManager::saveSpr, &g_things); g_lua.registerSingletonClass("g_houses"); g_lua.bindSingletonFunction("g_houses", "clear", &HouseManager::clear, &g_houses); @@ -118,22 +122,6 @@ void Client::registerLuaFunctions() g_lua.bindSingletonFunction("g_towns", "sort", &TownManager::sort, &g_towns); #endif - g_lua.registerSingletonClass("g_sprites"); - g_lua.bindSingletonFunction("g_sprites", "loadSpr", &SpriteManager::loadSpr, &g_sprites); - - g_lua.bindSingletonFunction("g_sprites", "unload", &SpriteManager::unload, &g_sprites); - g_lua.bindSingletonFunction("g_sprites", "isLoaded", &SpriteManager::isLoaded, &g_sprites); - g_lua.bindSingletonFunction("g_sprites", "getSprSignature", &SpriteManager::getSignature, &g_sprites); - g_lua.bindSingletonFunction("g_sprites", "getSpritesCount", &SpriteManager::getSpritesCount, &g_sprites); - -#ifdef FRAMEWORK_EDITOR - g_lua.bindSingletonFunction("g_sprites", "saveSpr", &SpriteManager::saveSpr, &g_sprites); -#endif - - g_lua.registerSingletonClass("g_spriteAppearances"); - g_lua.bindSingletonFunction("g_spriteAppearances", "saveSpriteToFile", &SpriteAppearances::saveSpriteToFile, &g_spriteAppearances); - g_lua.bindSingletonFunction("g_spriteAppearances", "saveSheetToFileBySprite", &SpriteAppearances::saveSheetToFileBySprite, &g_spriteAppearances); - g_lua.registerSingletonClass("g_map"); g_lua.bindSingletonFunction("g_map", "isLookPossible", &Map::isLookPossible, &g_map); g_lua.bindSingletonFunction("g_map", "addThing", &Map::addThing, &g_map); @@ -378,7 +366,6 @@ void Client::registerLuaFunctions() g_lua.bindSingletonFunction("g_game", "applyImbuement", &Game::applyImbuement, &g_game); g_lua.bindSingletonFunction("g_game", "clearImbuement", &Game::clearImbuement, &g_game); g_lua.bindSingletonFunction("g_game", "closeImbuingWindow", &Game::closeImbuingWindow, &g_game); - g_lua.bindSingletonFunction("g_game", "isUsingProtobuf", &Game::isUsingProtobuf, &g_game); g_lua.bindSingletonFunction("g_game", "enableTileThingLuaCallback", &Game::enableTileThingLuaCallback, &g_game); g_lua.bindSingletonFunction("g_game", "isTileThingLuaCallbackEnabled", &Game::isTileThingLuaCallbackEnabled, &g_game); g_lua.bindSingletonFunction("g_game", "stashWithdraw", &Game::stashWithdraw, &g_game); @@ -480,7 +467,6 @@ void Client::registerLuaFunctions() g_lua.bindClassMemberFunction("getAttachedWidgetById", &AttachableObject::getAttachedWidgetById); g_lua.registerClass(); - g_lua.bindClassMemberFunction("setId", &Thing::setId); g_lua.bindClassMemberFunction("setShader", &Thing::setShader); g_lua.bindClassMemberFunction("setPosition", &Thing::setPosition); g_lua.bindClassMemberFunction("setMarked", &Thing::lua_setMarked); @@ -687,7 +673,7 @@ void Client::registerLuaFunctions() g_lua.bindClassMemberFunction("getNumPatternX", &ThingType::getNumPatternX); g_lua.bindClassMemberFunction("getNumPatternY", &ThingType::getNumPatternY); g_lua.bindClassMemberFunction("getNumPatternZ", &ThingType::getNumPatternZ); - g_lua.bindClassMemberFunction("getAnimationPhases", &ThingType::getAnimationPhases); + g_lua.bindClassMemberFunction("getAnimationPhases", &ThingType::getAnimationPhase); g_lua.bindClassMemberFunction("getGroundSpeed", &ThingType::getGroundSpeed); g_lua.bindClassMemberFunction("getMaxTextLength", &ThingType::getMaxTextLength); g_lua.bindClassMemberFunction("getLight", &ThingType::getLight); diff --git a/src/client/luavaluecasts_client.cpp b/src/client/luavaluecasts_client.cpp index 375da7dc17..fd41dad33d 100644 --- a/src/client/luavaluecasts_client.cpp +++ b/src/client/luavaluecasts_client.cpp @@ -28,80 +28,190 @@ int push_luavalue(const Outfit& outfit) { - g_lua.createTable(0, 8); - g_lua.pushInteger(outfit.getId()); - g_lua.setField("type"); - g_lua.pushInteger(outfit.getAuxId()); - g_lua.setField("auxType"); + bool advanced = g_game.getFeature(Otc::GameWingsAurasEffectsShader); + + std::vector> outfitFields; + + // base outfit + const auto base = outfit.getBaseOutfit(); + outfitFields.push_back({ "type", base.type }); + outfitFields.push_back({ "auxType", base.typeEx }); + outfitFields.push_back({ "resourceId", base.resourceId }); + outfitFields.push_back({ "head", base.head }); + outfitFields.push_back({ "body", base.body }); + outfitFields.push_back({ "legs", base.legs }); + outfitFields.push_back({ "feet", base.feet }); + + // addons if (g_game.getFeature(Otc::GamePlayerAddons)) { - g_lua.pushInteger(outfit.getAddons()); - g_lua.setField("addons"); - } - g_lua.pushInteger(outfit.getHead()); - g_lua.setField("head"); - g_lua.pushInteger(outfit.getBody()); - g_lua.setField("body"); - g_lua.pushInteger(outfit.getLegs()); - g_lua.setField("legs"); - g_lua.pushInteger(outfit.getFeet()); - g_lua.setField("feet"); + outfitFields.push_back({ "addons", outfit.getAddons() }); + } + + // mount if (g_game.getFeature(Otc::GamePlayerMounts)) { - g_lua.pushInteger(outfit.getMount()); - g_lua.setField("mount"); + const auto mount = outfit.getMount(); + outfitFields.push_back({ "mount", mount.type }); + outfitFields.push_back({ "mountAux", mount.typeEx }); + outfitFields.push_back({ "mountResourceId", mount.resourceId }); + outfitFields.push_back({ "mountHead", mount.head }); + outfitFields.push_back({ "mountBody", mount.body }); + outfitFields.push_back({ "mountLegs", mount.legs }); + outfitFields.push_back({ "mountFeet", mount.feet }); } + + // familiar if (g_game.getFeature(Otc::GamePlayerFamiliars)) { - g_lua.pushInteger(outfit.getFamiliar()); - g_lua.setField("familiar"); + const auto familiar = outfit.getFamiliar(); + outfitFields.push_back({ "familiar", familiar.type }); + outfitFields.push_back({ "familiarAux", familiar.typeEx }); + outfitFields.push_back({ "familiarResourceId", familiar.resourceId }); } - if (g_game.getFeature(Otc::GameWingsAurasEffectsShader)) { - g_lua.pushInteger(outfit.getWing()); - g_lua.setField("wings"); - g_lua.pushInteger(outfit.getEffect()); - g_lua.setField("effects"); - g_lua.pushInteger(outfit.getAura()); - g_lua.setField("auras"); + + if (advanced) { + // wings + const auto wings = outfit.getWings(); + outfitFields.push_back({ "wings", wings.type }); + outfitFields.push_back({ "wingsAux", wings.typeEx }); + outfitFields.push_back({ "wingsResourceId", wings.resourceId }); + + // aura + const auto aura = outfit.getAura(); + outfitFields.push_back({ "aura", aura.type }); + outfitFields.push_back({ "auraResourceId", aura.resourceId }); + outfitFields.push_back({ "auraCategory", aura.category }); + + // effect + const auto effect = outfit.getEffect(); + outfitFields.push_back({ "effect", effect.type }); + outfitFields.push_back({ "effectResourceId", effect.resourceId }); + outfitFields.push_back({ "effectCategory", effect.category }); + } + + size_t tableSize = outfitFields.size(); + if (advanced) { + ++tableSize; + } + + g_lua.createTable(0, tableSize); + + for (auto& pair : outfitFields) { + g_lua.pushInteger(pair.second); + g_lua.setField(pair.first); + } + + if (advanced) { g_lua.pushString(outfit.getShader()); g_lua.setField("shaders"); } + return 1; } +// helper for outfit cast +SimpleOutfit lua_unserialize_simple_outfit(const int index, std::string_view type, std::string_view auxType, std::string_view resourceId) +{ + SimpleOutfit out; + g_lua.getField(type, index); + out.type = g_lua.popInteger(); + g_lua.getField(auxType, index); + out.typeEx = g_lua.popInteger(); + g_lua.getField(resourceId, index); + out.resourceId = g_lua.popInteger(); + return out; +} + +// helper for outfit cast +ColorOutfit lua_unserialize_color_outfit( + const int index, + std::string_view type, + std::string_view auxType, + std::string_view resourceId, + std::string_view head, + std::string_view body, + std::string_view legs, + std::string_view feet +) +{ + ColorOutfit out; + g_lua.getField(type, index); + out.type = g_lua.popInteger(); + g_lua.getField(auxType, index); + out.typeEx = g_lua.popInteger(); + g_lua.getField(resourceId, index); + out.resourceId = g_lua.popInteger(); + g_lua.getField(head, index); + out.head = g_lua.popInteger(); + g_lua.getField(body, index); + out.body = g_lua.popInteger(); + g_lua.getField(legs, index); + out.legs = g_lua.popInteger(); + g_lua.getField(feet, index); + out.feet = g_lua.popInteger(); + out.applyColors(); + return out; +} + +// helper for aura/particles +EffectOutfit lua_unserialize_effect_outfit(const int index, std::string_view type, std::string_view resourceId, std::string_view category) +{ + EffectOutfit out; + g_lua.getField(type, index); + out.type = g_lua.popInteger(); + g_lua.getField(resourceId, index); + out.resourceId = g_lua.popInteger(); + g_lua.getField(category, index); + out.category = static_cast(g_lua.popInteger()); + return out; +} + bool luavalue_cast(const int index, Outfit& outfit) { if (!g_lua.isTable(index)) return false; - g_lua.getField("type", index); - outfit.setId(g_lua.popInteger()); - g_lua.getField("auxType", index); - outfit.setAuxId(g_lua.popInteger()); + // outfit + outfit.applyOutfit( + lua_unserialize_color_outfit(index, "type", "auxType", "resourceId", "head", "body", "legs", "feet") + ); + + // outfit addons if (g_game.getFeature(Otc::GamePlayerAddons)) { g_lua.getField("addons", index); outfit.setAddons(g_lua.popInteger()); } - g_lua.getField("head", index); - outfit.setHead(g_lua.popInteger()); - g_lua.getField("body", index); - outfit.setBody(g_lua.popInteger()); - g_lua.getField("legs", index); - outfit.setLegs(g_lua.popInteger()); - g_lua.getField("feet", index); - outfit.setFeet(g_lua.popInteger()); + + // mount if (g_game.getFeature(Otc::GamePlayerMounts)) { - g_lua.getField("mount", index); - outfit.setMount(g_lua.popInteger()); + outfit.applyMount( + lua_unserialize_color_outfit(index, "mount", "mountAux", "mountResourceId", "mountHead", "mountBody", "mountLegs", "mountFeet") + ); } + + // familiar if (g_game.getFeature(Otc::GamePlayerFamiliars)) { - g_lua.getField("familiar", index); - outfit.setFamiliar(g_lua.popInteger()); + outfit.applyFamiliar( + lua_unserialize_simple_outfit(index, "familiar", "familiarAux", "familiarResourceId") + ); } + + // advanced cosmetics if (g_game.getFeature(Otc::GameWingsAurasEffectsShader)) { - g_lua.getField("wings", index); - outfit.setWing(g_lua.popInteger()); - g_lua.getField("effects", index); - outfit.setEffect(g_lua.popInteger()); - g_lua.getField("auras", index); - outfit.setAura(g_lua.popInteger()); + // outfit wings + outfit.applyWings( + lua_unserialize_simple_outfit(index, "wings", "wingsAux", "wingsResourceId") + ); + + // effect below outfit + outfit.applyAura( + lua_unserialize_effect_outfit(index, "aura", "auraResourceId", "auraCategory") + ); + + // effect above outfit + outfit.applyParticles( + lua_unserialize_effect_outfit(index, "effect", "effectResourceId", "effectCategory") + ); + + // shader g_lua.getField("shaders", index); outfit.setShader(g_lua.popString()); } @@ -109,6 +219,83 @@ bool luavalue_cast(const int index, Outfit& outfit) return true; } +int push_luavalue(const OutfitWindowThing& item) +{ + g_lua.createTable(0, 7); + g_lua.pushInteger(item.id); + g_lua.setField("id"); + g_lua.pushInteger(item.resourceId); + g_lua.setField("resourceId"); + g_lua.pushString(item.name); + g_lua.setField("name"); + g_lua.pushInteger(item.addons); + g_lua.setField("addons"); + g_lua.pushInteger(item.lockReason); + g_lua.setField("lockReason"); + g_lua.pushInteger(item.offerId); + g_lua.setField("offerId"); + g_lua.pushInteger(static_cast(item.category)); + g_lua.setField("category"); + return 1; +} + +bool luavalue_cast(const int index, OutfitWindowThing& item) +{ + if (!g_lua.isTable(index)) + return false; + + g_lua.getField("id", index); + item.id = g_lua.popInteger(); + g_lua.getField("resourceId", index); + item.resourceId = g_lua.popInteger(); + g_lua.getField("name", index); + item.name = g_lua.popString(); + g_lua.getField("addons", index); + item.addons = g_lua.popInteger(); + g_lua.getField("lockReason", index); + item.lockReason = g_lua.popInteger(); + g_lua.getField("offerId", index); + item.offerId = g_lua.popInteger(); + g_lua.getField("category", index); + item.category = static_cast(g_lua.popInteger()); + + return true; +} + +int push_luavalue(const LootContainerConf& conf) +{ + g_lua.createTable(0, 5); + g_lua.pushInteger(conf.categoryType); + g_lua.setField("categoryType"); + g_lua.pushInteger(conf.lootId); + g_lua.setField("lootId"); + g_lua.pushInteger(conf.lootResourceId); + g_lua.setField("lootResourceId"); + g_lua.pushInteger(conf.retrieveId); + g_lua.setField("retrieveId"); + g_lua.pushInteger(conf.retrieveResourceId); + g_lua.setField("retrieveResourceId"); + return 1; +} + +bool luavalue_cast(int index, LootContainerConf& conf) +{ + if (!g_lua.isTable(index)) + return false; + + g_lua.getField("categoryType", index); + conf.categoryType = g_lua.popInteger(); + g_lua.getField("lootId", index); + conf.lootId = g_lua.popInteger(); + g_lua.getField("lootResourceId", index); + conf.lootResourceId = g_lua.popInteger(); + g_lua.getField("retrieveId", index); + conf.retrieveId = g_lua.popInteger(); + g_lua.getField("retrieveResourceId", index); + conf.retrieveResourceId = g_lua.popInteger(); + return true; +} + int push_luavalue(const Position& pos) { if (pos.isValid()) { @@ -547,12 +734,16 @@ int push_luavalue(const StoreOffer& offer) { } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_MOUNT) { g_lua.pushInteger(offer.mountId); g_lua.setField("mountId"); + g_lua.pushInteger(offer.resourceId); + g_lua.setField("resourceId"); } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_ITEM) { g_lua.pushInteger(offer.itemId); g_lua.setField("itemId"); } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_OUTFIT) { g_lua.pushInteger(offer.outfitId); g_lua.setField("outfitId"); + g_lua.pushInteger(offer.resourceId); + g_lua.setField("resourceId"); g_lua.pushInteger(offer.outfitHead); g_lua.setField("outfitHead"); g_lua.pushInteger(offer.outfitBody); @@ -568,6 +759,8 @@ int push_luavalue(const StoreOffer& offer) { g_lua.setField("maleOutfitId"); g_lua.pushInteger(offer.femaleOutfitId); g_lua.setField("femaleOutfitId"); + g_lua.pushInteger(offer.resourceId); + g_lua.setField("resourceId"); g_lua.pushInteger(offer.outfitHead); g_lua.setField("outfitHead"); g_lua.pushInteger(offer.outfitBody); @@ -592,67 +785,6 @@ int push_luavalue(const StoreOffer& offer) { return 1; } -int push_luavalue(const HomeOffer& homeOffer) { - g_lua.createTable(0, 16); - g_lua.pushString(homeOffer.name); - g_lua.setField("name"); - g_lua.pushInteger(homeOffer.unknownByte); - g_lua.setField("unknownByte"); - g_lua.pushInteger(homeOffer.id); - g_lua.setField("id"); - g_lua.pushInteger(homeOffer.unknownU16); - g_lua.setField("unknownU16"); - g_lua.pushInteger(homeOffer.price); - g_lua.setField("price"); - g_lua.pushInteger(homeOffer.coinType); - g_lua.setField("coinType"); - g_lua.pushInteger(homeOffer.disabledReasonIndex); - g_lua.setField("disabledReasonIndex"); - g_lua.pushInteger(homeOffer.unknownByte2); - g_lua.setField("unknownByte2"); - g_lua.pushInteger(homeOffer.type); - g_lua.setField("type"); - - if (homeOffer.type == Otc::GameStoreInfoType_t::SHOW_NONE) { - g_lua.pushString(homeOffer.icon); - g_lua.setField("icon"); - } else if (homeOffer.type == Otc::GameStoreInfoType_t::SHOW_MOUNT) { - g_lua.pushInteger(homeOffer.mountClientId); - g_lua.setField("mountClientId"); - } else if (homeOffer.type == Otc::GameStoreInfoType_t::SHOW_ITEM) { - g_lua.pushInteger(homeOffer.itemType); - g_lua.setField("itemType"); - } else if (homeOffer.type == Otc::GameStoreInfoType_t::SHOW_OUTFIT) { - g_lua.pushInteger(homeOffer.sexId); - g_lua.setField("sexId"); - g_lua.createTable(0, 4); - g_lua.pushInteger(homeOffer.outfit.lookHead); - g_lua.setField("lookHead"); - g_lua.pushInteger(homeOffer.outfit.lookBody); - g_lua.setField("lookBody"); - g_lua.pushInteger(homeOffer.outfit.lookLegs); - g_lua.setField("lookLegs"); - g_lua.pushInteger(homeOffer.outfit.lookFeet); - g_lua.setField("lookFeet"); - g_lua.setField("outfit"); - } - - g_lua.pushInteger(homeOffer.tryOnType); - g_lua.setField("tryOnType"); - g_lua.pushInteger(homeOffer.collection); - g_lua.setField("collection"); - g_lua.pushInteger(homeOffer.popularityScore); - g_lua.setField("popularityScore"); - g_lua.pushInteger(homeOffer.stateNewUntil); - g_lua.setField("stateNewUntil"); - g_lua.pushInteger(homeOffer.userConfiguration); - g_lua.setField("userConfiguration"); - g_lua.pushInteger(homeOffer.productsCapacity); - g_lua.setField("productsCapacity"); - - return 1; -} - int push_luavalue(const Banner& banner) { g_lua.createTable(0, 5); g_lua.pushString(banner.image); @@ -1549,9 +1681,11 @@ int push_luavalue(const ForgeHistory& item) { } int push_luavalue(const ForgeItemInfo& item) { - g_lua.createTable(0, 3); + g_lua.createTable(0, 4); g_lua.pushInteger(item.id); g_lua.setField("id"); + g_lua.pushInteger(item.resourceId); + g_lua.setField("resourceId"); g_lua.pushInteger(item.tier); g_lua.setField("tier"); g_lua.pushInteger(item.count); @@ -1650,7 +1784,7 @@ int push_luavalue(const ForgeOpenData& data) { } int push_luavalue(const ForgeResultData& data) { - g_lua.createTable(0, 8); + g_lua.createTable(0, 13); g_lua.pushInteger(data.actionType); g_lua.setField("actionType"); g_lua.pushBoolean(data.convergence); @@ -1660,10 +1794,18 @@ int push_luavalue(const ForgeResultData& data) { g_lua.pushInteger(data.leftItemId); g_lua.setField("leftItemId"); g_lua.pushInteger(data.leftTier); + g_lua.setField("leftItemResourceId"); + g_lua.pushInteger(data.leftItemResourceId); g_lua.setField("leftTier"); g_lua.pushInteger(data.rightItemId); g_lua.setField("rightItemId"); g_lua.pushInteger(data.rightTier); + g_lua.setField("rightItemResourceId"); + g_lua.pushInteger(data.rightItemResourceId); + g_lua.setField("outcomeItemId"); + g_lua.pushInteger(data.outcomeItemId); + g_lua.setField("outcomeItemResourceId"); + g_lua.pushInteger(data.outcomeResourceId); g_lua.setField("rightTier"); g_lua.pushInteger(data.bonus); g_lua.setField("bonus"); @@ -1785,4 +1927,16 @@ int push_luavalue(const PartyMemberName& data) { g_lua.pushString(data.memberName); g_lua.setField("memberName"); return 1; -} \ No newline at end of file +} + +int push_luavalue(const AssetResourceInfo& data) +{ + g_lua.createTable(0, 3); + g_lua.pushInteger(data.resourceId); + g_lua.setField("id"); + g_lua.pushInteger(data.clientVersionId); + g_lua.setField("version"); + g_lua.pushString(data.dir); + g_lua.setField("dir"); + return 1; +} diff --git a/src/client/luavaluecasts_client.h b/src/client/luavaluecasts_client.h index 7c76a6e0fd..b7ee54bcf3 100644 --- a/src/client/luavaluecasts_client.h +++ b/src/client/luavaluecasts_client.h @@ -23,11 +23,20 @@ #pragma once #include "staticdata.h" +#include "thingtypemanager.h" // outfit int push_luavalue(const Outfit& outfit); bool luavalue_cast(int index, Outfit& outfit); +// outfit window cosmetic +int push_luavalue(const OutfitWindowThing& item); +bool luavalue_cast(int index, OutfitWindowThing& item); + +// loot containers +int push_luavalue(const LootContainerConf& conf); +bool luavalue_cast(int index, LootContainerConf& conf); + // position int push_luavalue(const Position& pos); bool luavalue_cast(int index, Position& pos); @@ -61,7 +70,6 @@ int push_luavalue(const BlessDialogData& data); int push_luavalue(const StoreCategory& category); int push_luavalue(const SubOffer& subOffer); int push_luavalue(const StoreOffer& offer); -int push_luavalue(const HomeOffer& homeOffer); int push_luavalue(const Banner& banner); int push_luavalue(const StoreData& storeData); @@ -116,3 +124,6 @@ int push_luavalue(const ForgeConfigData& data); int push_luavalue(const BossCooldownData& data); int push_luavalue(const PartyMemberData& data); int push_luavalue(const PartyMemberName& data); + +// packinfo.xml +int push_luavalue(const AssetResourceInfo& data); diff --git a/src/client/map.h b/src/client/map.h index 3ffe75aa6e..09086a4b7b 100644 --- a/src/client/map.h +++ b/src/client/map.h @@ -125,7 +125,7 @@ class Map // thing related ThingPtr getThing(const Position& pos, int16_t stackPos); - void addThing(const ThingPtr& thing, const Position& pos, int16_t stackPos = -1); + void addThing(const ThingPtr& thing, const Position& pos, const int16_t stackPos = -1); bool removeThing(const ThingPtr& thing); bool removeThingByPos(const Position& pos, int16_t stackPos); diff --git a/src/client/mapio.cpp b/src/client/mapio.cpp index 4ed4bfc53a..4bc3973591 100644 --- a/src/client/mapio.cpp +++ b/src/client/mapio.cpp @@ -34,8 +34,11 @@ #include #include "houses.h" +#include "item.h" #include "towns.h" +#include "thingtypemanager.h" + void Map::loadOtbm(const std::string& fileName) { try { @@ -403,6 +406,9 @@ void Map::saveOtbm(const std::string& fileName) bool Map::loadOtcm(const std::string& fileName) { + // otcm does not support resource ids + static constexpr uint16_t resourceId = 0; + try { const FileStreamPtr fin = g_resources.openFile(fileName); if (!fin) @@ -425,7 +431,7 @@ bool Map::loadOtcm(const std::string& fileName) fin->getU16(); // protocol version fin->getString(); // world name - if (datSignature != g_things.getDatSignature()) + if (datSignature != g_things.getDatSignature(resourceId)) g_logger.warning("otcm map loaded was created with a different dat signature"); break; @@ -459,7 +465,7 @@ bool Map::loadOtcm(const std::string& fileName) const int countOrSubType = fin->getU8(); - ItemPtr item = Item::create(id); + ItemPtr item = Item::create(id, resourceId); item->setCountOrSubType(countOrSubType); if (item->isValid()) diff --git a/src/client/missile.cpp b/src/client/missile.cpp index a70212f28a..390e27d792 100644 --- a/src/client/missile.cpp +++ b/src/client/missile.cpp @@ -33,7 +33,7 @@ void Missile::draw(const Point& dest, const bool drawThings, LightView* lightView) { - if (!canDraw() || isHided()) + if (!canDraw() || isHidden()) return; const float fraction = m_duration > 0 ? m_animationTimer.ticksElapsed() / m_duration : 1; @@ -108,14 +108,15 @@ void Missile::setDirection(const Otc::Direction dir) { } } -void Missile::setId(uint32_t id) +void Missile::setId(uint32_t id, uint16_t resourceId) { - if (!g_things.isValidDatId(id, ThingCategoryMissile)) + if (!g_things.isValidDatId(id, ThingCategoryMissile, resourceId)) id = 0; m_clientId = id; + m_resourceId = resourceId; } ThingType* Missile::getThingType() const { - return g_things.getRawThingType(m_clientId, ThingCategoryMissile); + return g_things.getRawThingType(m_clientId, ThingCategoryMissile, m_resourceId); } \ No newline at end of file diff --git a/src/client/missile.h b/src/client/missile.h index 359cbd15df..849c19c6ea 100644 --- a/src/client/missile.h +++ b/src/client/missile.h @@ -30,7 +30,7 @@ class Missile final : public Thing public: void draw(const Point& dest, bool drawThings = true, LightView* lightView = nullptr) override; - void setId(uint32_t id) override; + void setId(uint32_t id, uint16_t resourceId); void setPath(const Position& fromPosition, const Position& toPosition); bool isMissile() const override { return true; } diff --git a/src/client/outfit.cpp b/src/client/outfit.cpp index 1f9ba86359..023a106a7d 100644 --- a/src/client/outfit.cpp +++ b/src/client/outfit.cpp @@ -113,43 +113,31 @@ Color Outfit::getColor(int color) void Outfit::resetClothes() { - setHead(0); - setBody(0); - setLegs(0); - setFeet(0); - setMount(0); - setFamiliar(0); - setWing(0); - setAura(0); - setEffect(0); + m_outfit.resetColors(); + applyMount(ColorOutfit()); + applyFamiliar(SimpleOutfit()); + applyWings(SimpleOutfit()); + applyAura(EffectOutfit()); + applyParticles(EffectOutfit()); setShader("Outfit - Default"); } -void Outfit::setHead(const uint8_t head) { - if (m_head == head) - return; - - m_head = head; - m_headColor = getColor(head); -} -void Outfit::setBody(const uint8_t body) { - if (m_body == body) - return; - - m_body = body; - m_bodyColor = getColor(body); +void ColorOutfit::applyColors() +{ + headColor = Outfit::getColor(head); + bodyColor = Outfit::getColor(body); + legsColor = Outfit::getColor(legs); + feetColor = Outfit::getColor(feet); } -void Outfit::setLegs(const uint8_t legs) { - if (m_legs == legs) - return; - m_legs = legs; - m_legsColor = getColor(legs); +void ColorOutfit::resetColors() +{ + head = 0; + body = 0; + legs = 0; + feet = 0; + headColor = Color::white; + bodyColor = Color::white; + legsColor = Color::white; + feetColor = Color::white; } -void Outfit::setFeet(const uint8_t feet) { - if (m_feet == feet) - return; - - m_feet = feet; - m_feetColor = getColor(feet); -} \ No newline at end of file diff --git a/src/client/outfit.h b/src/client/outfit.h index 1bfaa1d261..01d09d473b 100644 --- a/src/client/outfit.h +++ b/src/client/outfit.h @@ -24,6 +24,45 @@ #include "declarations.h" +struct SimpleOutfit +{ + uint16_t type = 0; + uint16_t typeEx = 0; + uint16_t resourceId = 0; + + bool operator==(const SimpleOutfit&) const = default; +}; + +struct ColorOutfit +{ + uint16_t type = 0; + uint16_t typeEx = 0; + uint16_t resourceId = 0; + + uint8_t head = 0; + uint8_t body = 0; + uint8_t legs = 0; + uint8_t feet = 0; + + Color headColor{ Color::white }; + Color bodyColor{ Color::white }; + Color legsColor{ Color::white }; + Color feetColor{ Color::white }; + + void applyColors(); + void resetColors(); + + bool operator==(const ColorOutfit&) const = default; +}; + +struct EffectOutfit +{ + uint16_t type = 0; + uint16_t resourceId = 0; + ThingCategory category = ThingCategoryEffect; + bool operator==(const EffectOutfit&) const = default; +}; + class Outfit { enum @@ -35,19 +74,37 @@ class Outfit public: static Color getColor(int color); - void setId(const uint16_t id) { m_id = id; } - void setAuxId(const uint16_t id) { m_auxId = id; } - void setMount(const uint16_t mount) { m_mount = mount; } - void setFamiliar(const uint16_t familiar) { m_familiar = familiar; } - void setWing(const uint16_t Wing) { m_wing = Wing; } - void setAura(const uint16_t Aura) { m_aura = Aura; } - void setEffect(const uint16_t Effect) { m_effect = Effect; } + // bulk apply fields + void applyOutfit(ColorOutfit outfit) { m_outfit = std::move(outfit); }; + void applySimpleOutfit(SimpleOutfit outfit) { + m_outfit = ColorOutfit(); + m_outfit.type = outfit.type; + m_outfit.typeEx = outfit.typeEx; + m_outfit.resourceId = outfit.resourceId; + }; + void applyMount(ColorOutfit outfit) { m_mount = std::move(outfit); }; + void applyFamiliar(SimpleOutfit outfit) { m_familiar = std::move(outfit); }; + void applyWings(SimpleOutfit outfit) { m_wings = std::move(outfit); }; + void applyAura(EffectOutfit effect) { m_aura = std::move(effect); }; + void applyParticles(EffectOutfit effect) { m_effect = std::move(effect); }; + + // bulk get fields + ColorOutfit getBaseOutfit() const { return m_outfit; }; + ColorOutfit getMount() const { return m_mount; }; + SimpleOutfit getFamiliar() const { return m_familiar; }; + SimpleOutfit getWings() const { return m_wings; }; + EffectOutfit getAura() const { return m_aura; }; + EffectOutfit getEffect() const { return m_effect; }; + + // these fields are in use more than the rest + // so it's best to have get/set for them + void setId(const uint16_t id) { m_outfit.type = id; } + void setAuxId(const uint16_t id) { m_outfit.typeEx = id; } + void setResourceId(const uint16_t resourceId) { m_outfit.resourceId = resourceId; } + + void setMount(const uint16_t mount) { m_mount.type = mount; } void setShader(const std::string& shader) { m_shader = shader; } - void setHead(uint8_t head); - void setBody(uint8_t body); - void setLegs(uint8_t legs); - void setFeet(uint8_t feet); void setAddons(const uint8_t addons) { m_addons = addons; } void setTemp(const bool temp) { m_temp = temp; } @@ -55,22 +112,18 @@ class Outfit void resetClothes(); - uint16_t getId() const { return m_id; } - uint16_t getAuxId() const { return m_auxId; } - uint16_t getMount() const { return m_mount; } - uint16_t getFamiliar() const { return m_familiar; } - uint16_t getWing() const { return m_wing; } - uint16_t getAura() const { return m_aura; } - uint16_t getEffect() const { return m_effect; } + uint16_t getId() const { return m_outfit.type; } + uint16_t getAuxId() const { return m_outfit.typeEx; } std::string getShader() const { return m_shader; } - uint8_t getHead() const { return m_head; } - uint8_t getBody() const { return m_body; } - uint8_t getLegs() const { return m_legs; } - uint8_t getFeet() const { return m_feet; } + uint16_t getResourceId() const { return m_outfit.resourceId; } + uint8_t getAddons() const { return m_addons; } - bool hasMount() const { return m_mount > 0; } + bool hasMount() const { return m_mount.type > 0; } + bool hasWings() const { return m_wings.type > 0; } + bool hasAura() const { return m_aura.type > 0; } + bool hasParticles() const { return m_effect.type > 0; } ThingCategory getCategory() const { return m_category; } bool isCreature() const { return m_category == ThingCategoryCreature; } @@ -79,24 +132,14 @@ class Outfit bool isItem() const { return m_category == ThingCategoryItem; } bool isTemp() const { return m_temp; } - Color getHeadColor() const { return m_headColor; } - Color getBodyColor() const { return m_bodyColor; } - Color getLegsColor() const { return m_legsColor; } - Color getFeetColor() const { return m_feetColor; } - bool operator==(const Outfit& other) const { return m_category == other.m_category && - m_id == other.m_id && - m_auxId == other.m_auxId && - m_head == other.m_head && - m_body == other.m_body && - m_legs == other.m_legs && - m_feet == other.m_feet && + m_outfit == other.m_outfit && m_addons == other.m_addons && m_mount == other.m_mount && m_familiar == other.m_familiar && - m_wing == other.m_wing && + m_wings == other.m_wings && m_aura == other.m_aura && m_effect == other.m_effect && m_shader == other.m_shader; @@ -107,24 +150,27 @@ class Outfit ThingCategory m_category{ ThingInvalidCategory }; bool m_temp{ false }; + + // base outfit fields + ColorOutfit m_outfit{}; + uint8_t m_addons{ 0 }; - uint16_t m_id{ 0 }; - uint16_t m_auxId{ 0 }; - uint16_t m_mount{ 0 }; - uint16_t m_familiar{ 0 }; - uint16_t m_wing{ 0 }; - uint16_t m_aura{ 0 }; - uint16_t m_effect{ 0 }; - std::string m_shader; + // mount fields + ColorOutfit m_mount{}; - uint8_t m_head{ 0 }; - uint8_t m_body{ 0 }; - uint8_t m_legs{ 0 }; - uint8_t m_feet{ 0 }; - uint8_t m_addons{ 0 }; + // familiar fields + SimpleOutfit m_familiar{}; + + // wings fields + SimpleOutfit m_wings{}; - Color m_headColor{ Color::white }; - Color m_bodyColor{ Color::white }; - Color m_legsColor{ Color::white }; - Color m_feetColor{ Color::white }; + // aura fields + EffectOutfit m_aura{}; + + // particles fields + EffectOutfit m_effect{}; + + // shaders are indexed by string + // and they do not use resource ids + std::string m_shader; }; diff --git a/src/client/paperdoll.cpp b/src/client/paperdoll.cpp index 3da60e53fe..cfce86c0f1 100644 --- a/src/client/paperdoll.cpp +++ b/src/client/paperdoll.cpp @@ -114,8 +114,8 @@ int Paperdoll::getCurrentAnimationPhase() 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; + const int ticksPerFrame = std::round(1000 / m_thingType->getAnimationPhase()) / getSpeed(); + return (g_clock.millis() % (static_cast(ticksPerFrame) * m_thingType->getAnimationPhase())) / ticksPerFrame; } return 0; diff --git a/src/client/paperdoll.h b/src/client/paperdoll.h index 36900f6129..927894e332 100644 --- a/src/client/paperdoll.h +++ b/src/client/paperdoll.h @@ -98,10 +98,11 @@ class Paperdoll : public LuaObject 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(); + const auto colors = outfit.getBaseOutfit(); + m_head = colors.head; + m_body = colors.body; + m_legs = colors.legs; + m_feet = colors.feet; } void reset(); @@ -122,6 +123,7 @@ class Paperdoll : public LuaObject uint8_t m_opacity{ 100 }; uint16_t m_id{ 0 }; uint16_t m_thingId{ 0 }; + uint16_t m_thingResourceId{ 0 }; uint32_t m_addons{ 0 }; Timer m_timer; diff --git a/src/client/paperdollmanager.cpp b/src/client/paperdollmanager.cpp index aa638ee6b2..6822b6c55e 100644 --- a/src/client/paperdollmanager.cpp +++ b/src/client/paperdollmanager.cpp @@ -35,18 +35,18 @@ PaperdollPtr PaperdollManager::getById(uint16_t id) { const auto& obj = (*it).second; if (obj->m_thingId > 0 && obj->m_thingType == nullptr) { - if (!g_things.isValidDatId(obj->m_thingId, ThingCategoryCreature)) { + if (!g_things.isValidDatId(obj->m_thingId, ThingCategoryCreature, obj->m_thingResourceId)) { 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(); + obj->m_thingType = g_things.getThingType(obj->m_thingId, ThingCategoryCreature, obj->m_thingResourceId).get(); } return obj; } -PaperdollPtr PaperdollManager::set(uint16_t id, uint16_t thingId) { +PaperdollPtr PaperdollManager::set(uint16_t id, uint16_t thingId, uint16_t thingResourceId) { 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)); @@ -56,6 +56,7 @@ PaperdollPtr PaperdollManager::set(uint16_t id, uint16_t thingId) { const auto& obj = std::make_shared(); obj->m_id = id; obj->m_thingId = thingId; + obj->m_thingResourceId = thingResourceId; m_paperdolls.emplace(id, obj); return obj; diff --git a/src/client/paperdollmanager.h b/src/client/paperdollmanager.h index e49e570061..db7cb1645e 100644 --- a/src/client/paperdollmanager.h +++ b/src/client/paperdollmanager.h @@ -27,7 +27,7 @@ class PaperdollManager { public: - PaperdollPtr set(uint16_t id, uint16_t thingId); + PaperdollPtr set(uint16_t id, uint16_t thingId, uint16_t thingResourceId); PaperdollPtr getById(uint16_t id); void remove(uint16_t id) { m_paperdolls.erase(id); } diff --git a/src/client/position.cpp b/src/client/position.cpp index 482efc7930..d0e7853957 100644 --- a/src/client/position.cpp +++ b/src/client/position.cpp @@ -22,6 +22,24 @@ #include "gameconfig.h" #include "position.h" +#include "map.h" + +void Position::offsetByDelta(const Position& origin, const uint8_t delta) +{ + // this is for MAGIC_EFFECT_DELTA, for packed area effects + + // horizontal viewport size (tiles) + const uint8_t tileCount = g_map.getAwareRange().horizontal(); + for (uint8_t i = 1; i <= delta; ++i) { + x++; + if ((x - origin.x) == tileCount) { // wrap row + y++; + x = origin.x; + } else { + x += delta; // continue within the same row + } + } +} bool Position::isMapPosition() const { return ((x >= 0) && (y >= 0) && (x < UINT16_MAX) && (y < UINT16_MAX) && (z <= g_gameConfig.getMapMaxZ())); } diff --git a/src/client/position.h b/src/client/position.h index 7227914e80..31fc5f009c 100644 --- a/src/client/position.h +++ b/src/client/position.h @@ -127,6 +127,8 @@ class Position return positions; } + void offsetByDelta(const Position& origin, const uint8_t delta); + static bool isDiagonal(const Otc::Direction dir) { return dir == Otc::NorthWest || dir == Otc::NorthEast || dir == Otc::SouthWest || dir == Otc::SouthEast; }; static double getAngleFromPositions(const Position& fromPos, const Position& toPos) diff --git a/src/client/protocolgame.h b/src/client/protocolgame.h index 99db9423e5..ecb58ae739 100644 --- a/src/client/protocolgame.h +++ b/src/client/protocolgame.h @@ -52,14 +52,14 @@ class ProtocolGame final : public Protocol void sendTurnSouth(); void sendTurnWest(); void sendGmTeleport(const Position& pos); - void sendEquipItemWithTier(uint16_t itemId, uint8_t tierOrFluid); - void sendEquipItemWithCountOrSubType(uint16_t itemId, uint16_t tierOrFluid); - void sendMove(const Position& fromPos, uint16_t thingId, uint8_t stackpos, const Position& toPos, uint16_t count); - void sendInspectNpcTrade(uint16_t itemId, uint16_t count); - void sendBuyItem(uint16_t itemId, uint8_t subType, uint16_t amount, bool ignoreCapacity, bool buyWithBackpack); - void sendSellItem(uint16_t itemId, uint8_t subType, uint16_t amount, bool ignoreEquipped); + void sendEquipItemWithTier(uint16_t itemId, uint16_t resourceId, uint8_t tierOrFluid); + void sendEquipItemWithCountOrSubType(uint16_t itemId, uint16_t resourceId, uint16_t tierOrFluid); + void sendMove(const Position& fromPos, uint16_t thingId, uint16_t resourceId, uint8_t stackpos, const Position& toPos, uint16_t count); + void sendInspectNpcTrade(uint16_t itemId, uint16_t resourceId, uint16_t count); + void sendBuyItem(uint16_t itemId, uint16_t resourceId, uint8_t subType, uint16_t amount, bool ignoreCapacity, bool buyWithBackpack); + void sendSellItem(uint16_t itemId, uint16_t resourceId, uint8_t subType, uint16_t amount, bool ignoreEquipped); void sendCloseNpcTrade(); - void sendRequestTrade(const Position& pos, uint16_t thingId, uint8_t stackpos, uint32_t creatureId); + void sendRequestTrade(const Position& pos, uint16_t thingId, uint16_t resourceId, uint8_t stackpos, uint32_t creatureId); void sendInspectTrade(bool counterOffer, uint8_t index); void sendAcceptTrade(); void sendRejectTrade(); @@ -72,7 +72,7 @@ class ProtocolGame final : public Protocol void sendUpContainer(uint8_t containerId); void sendEditText(uint32_t id, std::string_view text); void sendEditList(uint32_t id, uint8_t doorId, std::string_view text); - void sendLook(const Position& position, uint16_t itemId, uint8_t stackpos); + void sendLook(const Position& position, uint16_t itemId, uint16_t resourceId, uint8_t stackpos); void sendLookCreature(uint32_t creatureId); void sendTalk(Otc::MessageMode mode, uint16_t channelId, std::string_view receiver, std::string_view message); void sendRequestChannels(); @@ -114,7 +114,7 @@ class ProtocolGame final : public Protocol void sendRequestQuestLog(); void sendRequestQuestLine(uint16_t questId); void sendNewNewRuleViolation(uint8_t reason, uint8_t action, std::string_view characterName, std::string_view comment, std::string_view translation); - void sendRequestItemInfo(uint16_t itemId, uint8_t subType, uint8_t index); + void sendRequestItemInfo(uint16_t itemId, uint16_t resourceId, uint8_t subType, uint8_t index); void sendAnswerModalDialog(uint32_t dialog, uint8_t button, uint8_t choice); void sendBrowseField(const Position& position); void sendSeekInContainer(uint8_t containerId, uint16_t index); @@ -130,14 +130,17 @@ class ProtocolGame final : public Protocol void sendTransferCoins(std::string_view recipient, uint16_t amount); void sendOpenTransactionHistory(uint8_t entriesPerPage); void sendMarketLeave(); - void sendMarketBrowse(uint8_t browseId, uint16_t browseType, uint8_t tier = 0); - void sendMarketCreateOffer(uint8_t type, uint16_t itemId, uint8_t itemTier, uint16_t amount, uint64_t price, uint8_t anonymous); + void sendMarketBrowse(uint8_t browseId, uint16_t browseType, uint8_t tier = 0, uint16_t resourceId = 0); + void sendMarketCreateOffer(uint8_t type, uint16_t itemId, uint16_t resourceId, uint8_t itemTier, uint16_t amount, uint64_t price, uint8_t anonymous); void sendMarketCancelOffer(uint32_t timestamp, uint16_t counter); void sendMarketAcceptOffer(uint32_t timestamp, uint16_t counter, uint16_t amount); void sendPreyAction(uint8_t slot, uint8_t actionType, uint16_t index); void sendPreyRequest(); void sendOpenPortableForge(); - void sendForgeRequest(Otc::ForgeAction_t actionType, bool convergence = false, uint16_t firstItemid = 0, uint8_t firstItemTier = 0, uint16_t secondItemId = 0, bool improveChance = false, bool tierLoss = false); + void sendForgeRequest( + Otc::ForgeAction_t actionType, bool convergence = false, uint16_t firstItemid = 0, uint16_t firstItemResourceId = 0, uint8_t firstItemTier = 0, + uint16_t secondItemId = 0, uint16_t secondItemResourceId = 0, bool improveChance = false, bool tierLoss = false + ); void sendForgeBrowseHistoryRequest(uint16_t page); void sendApplyImbuement(uint8_t slot, uint32_t imbuementId, bool protectionCharm); void sendClearImbuement(uint8_t slot); @@ -145,8 +148,8 @@ class ProtocolGame final : public Protocol void sendOpenRewardWall(); void sendOpenRewardHistory(); void sendGetRewardDaily(const uint8_t bonusShrine, const std::map& items); - void sendStashWithdraw(uint16_t itemId, uint32_t count, uint8_t stackpos); - void sendStashStow(const Position& position, const uint16_t itemId, const uint32_t count, const uint8_t stackpos, const uint8_t action); + void sendStashWithdraw(uint16_t itemId, uint16_t resourceId, uint32_t count, uint8_t stackpos); + void sendStashStow(const Position& position, const uint16_t itemId, const uint16_t resourceId, const uint32_t count, const uint8_t stackpos, const uint8_t action); void sendHighscoreInfo(uint8_t action, uint8_t category, uint32_t vocation, std::string_view world, uint8_t worldType, uint8_t battlEye, uint16_t page, uint8_t totalPages); void sendImbuementDurations(bool isOpen = false); void sendRequestBestiary(); @@ -159,11 +162,11 @@ class ProtocolGame final : public Protocol void sendRequestBossSlootInfo(); void sendRequestBossSlotAction(uint8_t action, uint32_t raceId); void sendStatusTrackerBestiary(uint16_t raceId, bool status); - void sendQuickLoot(const uint8_t variant, const Position& pos, const uint16_t itemId, const uint8_t stackpos); + void sendQuickLoot(const uint8_t variant, const Position& pos, const uint16_t itemId, const uint16_t resourceId, const uint8_t stackpos); void requestQuickLootBlackWhiteList(uint8_t filter, uint16_t size, const std::vector& listedItems); - void openContainerQuickLoot(uint8_t action, uint8_t category, const Position& pos, uint16_t itemId, uint8_t stackpos, bool useMainAsFallback); + void openContainerQuickLoot(uint8_t action, uint8_t category, const Position& pos, uint16_t itemId, uint16_t resourceId, uint8_t stackpos, bool useMainAsFallback); void sendInspectionNormalObject(const Position& position); - void sendInspectionObject(Otc::InspectObjectTypes inspectionType, uint16_t itemId, uint8_t itemCount); + void sendInspectionObject(Otc::InspectObjectTypes inspectionType, uint16_t itemId, uint16_t resourceId, uint8_t itemCount); // otclient only void sendChangeMapAwareRange(uint8_t xrange, uint8_t yrange); @@ -215,7 +218,7 @@ class ProtocolGame final : public Protocol void parsePing(const InputMessagePtr& msg); void parsePingBack(const InputMessagePtr& msg); void parseLoginChallenge(const InputMessagePtr& msg); - void parseDeath(const InputMessagePtr& msg); + void parseDeathScreen(const InputMessagePtr& msg); void parseFloorDescription(const InputMessagePtr& msg); void parseMapDescription(const InputMessagePtr& msg); void parseCreatureTyping(const InputMessagePtr& msg); @@ -388,8 +391,9 @@ class ProtocolGame final : public Protocol void setMapDescription(const InputMessagePtr& msg, int x, int y, int z, int width, int height); int setFloorDescription(const InputMessagePtr& msg, int x, int y, int z, int width, int height, int offset, int skip); int setTileDescription(const InputMessagePtr& msg, Position position); + bool setMagicEffect(const InputMessagePtr& msg, Position& pos, uint8_t effectType, uint8_t& delay); - Outfit getOutfit(const InputMessagePtr& msg, bool parseMount = true) const; + Outfit getOutfit(const InputMessagePtr& msg, bool parseMount = true, bool forceReadMountColors = false) const; ThingPtr getThing(const InputMessagePtr& msg); ThingPtr getMappedThing(const InputMessagePtr& msg) const; CreaturePtr getCreature(const InputMessagePtr& msg, int type = 0) const; @@ -398,6 +402,29 @@ class ProtocolGame final : public Protocol private: PaperdollPtr getPaperdoll(const InputMessagePtr& msg) const; + void internalGetCreature(const InputMessagePtr& msg, CreaturePtr& creature, bool known) const; + void creatureFromPacket(const InputMessagePtr& msg, CreaturePtr& creature, uint32_t& creatureId, const bool known) const; + void makeCreature(CreaturePtr& creature, uint8_t creatureType) const; + void getImbuingIngredients(const InputMessagePtr& msg, std::vector& imbuements, std::vector& neededItemsList); + void setExtendedCosmetics(const InputMessagePtr& msg, const CreaturePtr& creature) const; + void setCreatureIcons(const InputMessagePtr& msg, const CreaturePtr& creature, const uint32_t creatureId, const bool known) const; + ForgeItemInfo getForgeItem(const InputMessagePtr& msg, const bool multiSpr, const bool skipTier = false); + void getForgeTransfers(const InputMessagePtr& msg, std::vector& transfers, const bool multiSpr); + void getBosstiarySlot(const InputMessagePtr& msg, bool& unlocked, uint32_t& bossId, std::optional& slot); + StoreOffer getStoreOffer(const InputMessagePtr& msg); + void getStoreOfferImage(const InputMessagePtr& msg, StoreOffer& offer); + SubOffer getStoreSubOffer(const InputMessagePtr& msg); + void getStorePackageItem(const InputMessagePtr& msg); + OutfitWindowThing getOutfitWindowThing(const InputMessagePtr& msg, const bool addons, const bool multiSpr, const bool thingCategories = false) const; + void getOutfitWindowCosmeticsList( + const InputMessagePtr& msg, + std::vector& thingList, + const bool listInU16, + const bool addons, + const bool multiSpr, + const bool thingCategories = false + ) const; + void simpleEvent1520(uint8_t eventId); bool m_enableSendExtendedOpcode{ false }; bool m_gameInitialized{ false }; diff --git a/src/client/protocolgameparse.cpp b/src/client/protocolgameparse.cpp index 2235499ae3..6a7e115e17 100644 --- a/src/client/protocolgameparse.cpp +++ b/src/client/protocolgameparse.cpp @@ -120,7 +120,7 @@ void ProtocolGame::parseMessage(const InputMessagePtr& msg) parseLoginChallenge(msg); break; case Proto::GameServerDeath: - parseDeath(msg); + parseDeathScreen(msg); break; case Proto::GameServerSupplyStash: parseSupplyStash(msg); @@ -711,17 +711,20 @@ void ProtocolGame::parseLogin(const InputMessagePtr& msg) const canReportBugs = msg->getU8() > 0; } + // bool: can change pvp frame option if (g_game.getClientVersion() >= 1054) { - msg->getU8(); // can change pvp frame option + msg->getU8(); } + // bool: enable advanced pvp modes if (g_game.getClientVersion() >= 1058) { const uint8_t expertModeEnabled = msg->getU8(); g_game.setExpertPvpMode(expertModeEnabled); } + // store meta if (g_game.getFeature(Otc::GameIngameStore)) { - // URL to ingame store images + // store images url std::string url = msg->getString(); // premium coin package size @@ -731,9 +734,11 @@ void ProtocolGame::parseLogin(const InputMessagePtr& msg) const } if (g_game.getClientVersion() >= 1281) { - msg->getU8(); // exiva button enabled (bool) + // bool: exiva options button enabled + msg->getU8(); if (g_game.getFeature(Otc::GameTournamentPackets)) { - msg->getU8(); // Tournament button (bool) + // bool: tournament button enabled + msg->getU8(); } } @@ -769,17 +774,72 @@ void ProtocolGame::parseEnterGame(const InputMessagePtr&) void ProtocolGame::parseStoreButtonIndicators(const InputMessagePtr& msg) { - msg->getU8(); // (bool) IsSaleBannerVisible - msg->getU8(); // (bool) IsNewBannerVisible + // store button style: "sale" + // server sends this when there is a sale happening in the store + msg->getU8(); + + // store button style "new" + // server sends this when there are new items in the store + msg->getU8(); } void ProtocolGame::parseSetStoreDeepLink(const InputMessagePtr& msg) { - msg->getU8(); // currentlyFeaturedServiceType + // if action type 1-5: creates a frame around store button + // sets the arguments for 0xFB packet that generates when you click the "store" button + // after clicking the button, it resets to default action + + // based on protocol 1320 + // may differ in other versions + const uint8_t actionType = msg->getU8(); + switch (actionType) { + case 1: + // service request + // 0 - premium + // 1 - xp boost + msg->getU8(); + break; + case 2: + // category request + msg->getString(); // primaryText + msg->getString(); // secondaryText + break; + case 3: + // service request + // services hardcoded in the vanilla client: + // 0 - prey slots + // 1 - prey cards + // 2 - instant reward access tokens + // 3 - charm expansion + // 11 - death redemption + msg->getU8(); + break; + case 4: + // offerId + msg->getU32(); + break; + default: + break; + } + + msg->getU8(); // enum: sort order + msg->getU8(); // secondary value } void ProtocolGame::parseBlessings(const InputMessagePtr& msg) const { + // client blessings (flags - 2^n): + /* + 1 - adventurer (glowing slots + info in bless panel) + 2 - twist of fate + 3 - wisdom + 4 - spark + 5 - fire + 6 - spirit + 7 - embrace + 8 - heart + 9 - blood + */ const uint16_t blessings = msg->getU16(); // glowing effect indicator uint8_t blessVisualState = 0; if (g_game.getClientVersion() >= 1200) { @@ -837,23 +897,29 @@ void ProtocolGame::parseStore(const InputMessagePtr& msg) const for (auto i = 0; i < categoryCount; ++i) { StoreCategory category; + + // category name category.name = msg->getString(); if (g_game.getClientVersion() < 1291) { msg->getString(); } + // category state (present but unused in 13.20) + // 0 - normal, 1 - new, 2 - sale, 3 - limited time offer if (g_game.getFeature(Otc::GameIngameStoreHighlights)) { category.state = msg->getU8(); } else { category.state = 0; } + // category images const uint8_t iconCount = msg->getU8(); for (auto j = 0; j < iconCount; ++j) { category.icons.push_back(msg->getString()); } + // category parent category.parent = msg->getString(); categories.push_back(category); } @@ -891,20 +957,22 @@ void ProtocolGame::parseCoinBalance(const InputMessagePtr& msg) const { const bool update = static_cast(msg->getU8()); if (update) { - // amount of coins that can be used to buy prodcuts - // in the ingame store + // TOTAL STORE COINS + // transferable + non-transferable amount (+ reserved?) const uint32_t coins = msg->getU32(); // coins m_localPlayer->setResourceBalance(Otc::RESOURE_COIN_NORMAL, coins); - // amount of coins that can be sold in market - // or be transfered to another player - const uint32_t transferrableCoins = msg->getU32(); // transferableCoins + // TRANSFERABLE STORE COINS + // coins that can be passed to another player + const uint32_t transferrableCoins = msg->getU32(); m_localPlayer->setResourceBalance(Otc::RESOURE_COIN_TRANSFERRABLE, transferrableCoins); if (g_game.getClientVersion() >= 1281) { + // STORE COINS RESERVED FOR CHARACTER AUCTIONS const uint32_t auctionCoins = msg->getU32(); m_localPlayer->setResourceBalance(Otc::RESOURE_COIN_AUCTION, auctionCoins); if (g_game.getFeature(Otc::GameTournamentPackets)) { + // TOURNAMENT COINS const uint32_t tournamentCoins = msg->getU32(); m_localPlayer->setResourceBalance(Otc::RESOURE_COIN_TOURNAMENT, tournamentCoins); } @@ -939,13 +1007,12 @@ void ProtocolGame::parseCoinBalanceUpdating(const InputMessagePtr& msg) void ProtocolGame::parseCompleteStorePurchase(const InputMessagePtr& msg) const { + msg->getU8(); // 0, 1 - success, other values - packet ignored + if (g_game.getClientVersion() >= 1291) { - msg->getU8(); const auto& purchaseStatus = msg->getString(); g_lua.callGlobalField("g_game", "onParseStoreGetPurchaseStatus", purchaseStatus); } else { - msg->getU8(); // not used - const auto& message = msg->getString(); const uint32_t coins = msg->getU32(); const uint32_t transferableCoins = msg->getU32(); @@ -1000,22 +1067,33 @@ void ProtocolGame::parseStoreOffers(const InputMessagePtr& msg) { if (g_game.getClientVersion() >= 1291) { StoreData storeData; + + // current category storeData.categoryName = msg->getString(); + + // current offer id storeData.redirectId = msg->getU32(); - msg->getU8(); // -- sort by 0 - most popular, 1 - alphabetically, 2 - newest + // sort mode + // 0 - popular, 1 - alphabetically, 2 - newest + msg->getU8(); + + // middle section - dropdown menu available options + // (eg. bronze mounts, silver mounts, etc.) const uint8_t dropMenuShowAll = msg->getU8(); for (auto i = 0; i < dropMenuShowAll; ++i) { const auto& menu = msg->getString(); storeData.menuFilter.push_back(menu); } - uint16_t stringLength = msg->getU16(); - msg->skipBytes(stringLength); // tfs send string , canary send u16 + // middle section - dropdown menu current choice + // (eg. bronze mounts, silver mounts, etc) + msg->getString(); + // full list of possible reasons why offers may be unavailable + // when the buy button is inactive, individual offers provide selected errors from this list if (g_game.getClientVersion() >= 1310) { const uint16_t disableReasonsSize = msg->getU16(); - for (auto i = 0; i < disableReasonsSize; ++i) { const auto& reason = msg->getString(); storeData.disableReasons.push_back(reason); @@ -1025,49 +1103,7 @@ void ProtocolGame::parseStoreOffers(const InputMessagePtr& msg) const uint16_t offersCount = msg->getU16(); if (storeData.categoryName == "Home") { for (auto i = 0; i < offersCount; ++i) { - HomeOffer offer; - offer.name = msg->getString(); - offer.unknownByte = msg->getU8(); - offer.id = msg->getU32(); - offer.unknownU16 = msg->getU16(); - offer.price = msg->getU32(); - offer.coinType = msg->getU8(); - - const uint8_t hasDisabledReason = msg->getU8(); - if (hasDisabledReason == 1) { - msg->skipBytes(1); - if (g_game.getClientVersion() >= 1300) { - offer.disabledReasonIndex = msg->getU16(); - } else { - msg->getString(); - } - } - - offer.unknownByte2 = msg->getU8(); - offer.type = msg->getU8(); - - if (offer.type == Otc::GameStoreInfoType_t::SHOW_NONE) { - offer.icon = msg->getString(); - } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_MOUNT) { - offer.mountClientId = msg->getU16(); - } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_ITEM) { - offer.itemType = msg->getU16(); - } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_OUTFIT) { - offer.sexId = msg->getU16(); - offer.outfit.lookHead = msg->getU8(); - offer.outfit.lookBody = msg->getU8(); - offer.outfit.lookLegs = msg->getU8(); - offer.outfit.lookFeet = msg->getU8(); - } - - offer.tryOnType = msg->getU8(); - offer.collection = msg->getU16(); - offer.popularityScore = msg->getU16(); - offer.stateNewUntil = msg->getU32(); - offer.userConfiguration = msg->getU8(); - offer.productsCapacity = msg->getU16(); - - storeData.homeOffers.push_back(offer); + storeData.homeOffers.push_back(getStoreOffer(msg)); } const uint8_t bannerCount = msg->getU8(); @@ -1086,80 +1122,10 @@ void ProtocolGame::parseStoreOffers(const InputMessagePtr& msg) g_lua.callGlobalField("g_game", "onParseStoreCreateHome", storeData); return; - } - - for (auto i = 0; i < offersCount; ++i) { - StoreOffer offer; - offer.name = msg->getString(); - - const uint8_t subOffersCount = msg->getU8(); - for (auto j = 0; j < subOffersCount; ++j) { - SubOffer subOffer{}; - subOffer.id = msg->getU32(); - subOffer.count = msg->getU16(); - subOffer.price = msg->getU32(); - subOffer.coinType = msg->getU8(); - subOffer.disabled = msg->getU8() == 1; - if (subOffer.disabled) { - const uint8_t reason = msg->getU8(); - for (auto k = 0; k < reason; ++k) { - if (g_game.getClientVersion() >= 1300) { - subOffer.reasonIdDisable = msg->getU16(); - } else { - msg->getString(); - } - } - } - subOffer.state = msg->getU8(); - - if (subOffer.state == Otc::GameStoreInfoStatesType_t::STATE_SALE) { - subOffer.validUntil = msg->getU32(); - subOffer.basePrice = msg->getU32(); - } - offer.subOffers.push_back(subOffer); - } - - offer.type = msg->getU8(); - if (offer.type == Otc::GameStoreInfoType_t::SHOW_NONE) { - offer.icon = msg->getString(); - } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_MOUNT) { - offer.mountId = msg->getU16(); - } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_ITEM) { - offer.itemId = msg->getU16(); - } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_OUTFIT) { - offer.outfitId = msg->getU16(); - offer.outfitHead = msg->getU8(); - offer.outfitBody = msg->getU8(); - offer.outfitLegs = msg->getU8(); - offer.outfitFeet = msg->getU8(); - } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_HIRELING) { - offer.sex = msg->getU8(); - offer.maleOutfitId = msg->getU16(); - offer.femaleOutfitId = msg->getU16(); - offer.outfitHead = msg->getU8(); - offer.outfitBody = msg->getU8(); - offer.outfitLegs = msg->getU8(); - offer.outfitFeet = msg->getU8(); - } - - offer.tryOnType = msg->getU8(); - - if (g_game.getClientVersion() <= 1310) { - auto test = msg->getString(); - } else { - offer.collection = msg->getU16(); - } - - offer.popularityScore = msg->getU16(); - offer.stateNewUntil = msg->getU32(); - offer.configurable = msg->getU8() == 1; - offer.productsCapacity = msg->getU16(); - for (auto j = 0; j < offer.productsCapacity; ++j) { - msg->getString(); - msg->getU8(); // info in description? - msg->getU16(); + } else { + for (auto i = 0; i < offersCount; ++i) { + storeData.storeOffers.push_back(getStoreOffer(msg)); } - storeData.storeOffers.push_back(offer); } if (storeData.categoryName == "Search") { @@ -1168,6 +1134,7 @@ void ProtocolGame::parseStoreOffers(const InputMessagePtr& msg) g_lua.callGlobalField("g_game", "onParseStoreCreateProducts", storeData); } else { + // old protocol StoreData storeData; storeData.categoryName = msg->getString(); // categoryName @@ -1253,7 +1220,7 @@ void ProtocolGame::parsePvpSituations(const InputMessagePtr& msg) void ProtocolGame::parsePlayerHelpers(const InputMessagePtr& msg) const { const uint32_t creatureId = msg->getU32(); - const uint16_t helpers = msg->getU16(); + const uint16_t helpers = msg->getU16(); // guild / party members online const auto& creature = g_map.getCreatureById(creatureId); if (!creature) { @@ -1331,9 +1298,11 @@ void ProtocolGame::parseLoginChallenge(const InputMessagePtr& msg) sendLoginPacket(timestamp, random); } -void ProtocolGame::parseDeath(const InputMessagePtr& msg) +void ProtocolGame::parseDeathScreen(const InputMessagePtr& msg) { - uint8_t penality = 100; + // "You are dead" window ("Alas, brave adventurer! ...") + + uint8_t penalty = 100; uint8_t deathType = Otc::DeathRegular; if (g_game.getFeature(Otc::GameDeathType)) { @@ -1341,14 +1310,14 @@ void ProtocolGame::parseDeath(const InputMessagePtr& msg) } if (g_game.getFeature(Otc::GamePenalityOnDeath) && deathType == Otc::DeathRegular) { - penality = msg->getU8(); + penalty = msg->getU8(); } if (g_game.getClientVersion() >= 1281) { msg->getU8(); // (bool) can use death redemption } - g_game.processDeath(deathType, penality); + g_game.processDeath(deathType, penalty); } void ProtocolGame::parseFloorDescription(const InputMessagePtr& msg) @@ -1646,7 +1615,7 @@ void ProtocolGame::parseCyclopediaItemDetail(const InputMessagePtr& msg) descriptions.emplace_back(firstDescription, secondDescription); } - g_game.processItemDetail(item->getId(), descriptions); + g_game.processItemDetail(item->getId(), descriptions, item->getResourceId()); } void ProtocolGame::parseAddInventoryItem(const InputMessagePtr& msg) @@ -1665,12 +1634,17 @@ void ProtocolGame::parseRemoveInventoryItem(const InputMessagePtr& msg) void ProtocolGame::parseOpenNpcTrade(const InputMessagePtr& msg) { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + if (g_game.getFeature(Otc::GameNameOnNpcTrade)) { msg->getString(); // npcName } if (g_game.getClientVersion() >= 1281) { - msg->getU16(); // currency + msg->getU16(); // currency item id + if (multiSpr) { + msg->getU16(); // currency resource id + } msg->getString(); // currency name } @@ -1679,9 +1653,10 @@ void ProtocolGame::parseOpenNpcTrade(const InputMessagePtr& msg) for (auto i = 0; i < listCount; ++i) { const uint16_t itemId = msg->getU16(); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; const uint8_t itemCount = msg->getU8(); - const auto item = Item::create(itemId); + const auto item = Item::create(itemId, resourceId); item->setCountOrSubType(itemCount); const auto& itemName = msg->getString(); @@ -1712,11 +1687,15 @@ void ProtocolGame::parsePlayerGoods(const InputMessagePtr& msg) const const uint8_t itemsListSize = g_game.getClientVersion() >= 1334 ? msg->getU16() : msg->getU8(); std::vector> goods; + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + const bool shopU16 = g_game.getFeature(Otc::GameDoubleShopSellAmount); + for (auto i = 0; i < itemsListSize; ++i) { const uint16_t itemId = msg->getU16(); - const uint16_t itemAmount = g_game.getFeature(Otc::GameDoubleShopSellAmount) ? msg->getU16() : msg->getU8(); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const uint16_t itemAmount = shopU16 ? msg->getU16() : msg->getU8(); - goods.emplace_back(Item::create(itemId), itemAmount); + goods.emplace_back(Item::create(itemId, resourceId), itemAmount); } g_game.processPlayerGoods(money, goods); @@ -1772,88 +1751,28 @@ void ProtocolGame::parseWorldLight(const InputMessagePtr& msg) void ProtocolGame::parseMagicEffect(const InputMessagePtr& msg) { - const auto& pos = getPosition(msg); - if (g_game.getProtocolVersion() >= 1203) { - uint8_t effectType = msg->getU8(); - while (effectType != Otc::MAGIC_EFFECTS_END_LOOP) { - switch (effectType) { - case Otc::MAGIC_EFFECTS_DELAY: - case Otc::MAGIC_EFFECTS_DELTA: { - msg->getU8(); // ? - break; - } - - case Otc::MAGIC_EFFECTS_CREATE_DISTANCEEFFECT: - case Otc::MAGIC_EFFECTS_CREATE_DISTANCEEFFECT_REVERSED: { - const uint16_t shotId = g_game.getFeature(Otc::GameEffectU16) ? msg->getU16() : msg->getU8(); - const auto offsetX = static_cast(msg->getU8()); - const auto offsetY = static_cast(msg->getU8()); - if (!g_things.isValidDatId(shotId, ThingCategoryMissile)) { - g_logger.traceError("invalid missile id {}", shotId); - return; - } - - const auto& missile = std::make_shared(); - missile->setId(shotId); - - if (effectType == Otc::MAGIC_EFFECTS_CREATE_DISTANCEEFFECT) { - missile->setPath(pos, Position(pos.x + offsetX, pos.y + offsetY, pos.z)); - } else { - missile->setPath(Position(pos.x + offsetX, pos.y + offsetY, pos.z), pos); - } - - g_map.addThing(missile, pos); - break; - } - - case Otc::MAGIC_EFFECTS_CREATE_EFFECT: { - const uint16_t effectId = g_game.getFeature(Otc::GameEffectU16) ? msg->getU16() : msg->getU8(); - if (!g_things.isValidDatId(effectId, ThingCategoryEffect)) { - g_logger.traceError("invalid effect id {}", effectId); - continue; - } - - const auto& effect = std::make_shared(); - effect->setId(effectId); - g_map.addThing(effect, pos); - break; - } - - case Otc::MAGIC_EFFECTS_CREATE_SOUND_MAIN_EFFECT: { - msg->getU8(); // Source - msg->getU16(); // Sound ID - break; - } - - case Otc::MAGIC_EFFECTS_CREATE_SOUND_SECONDARY_EFFECT: { - msg->getU8(); // ENUM - msg->getU8(); // Source - msg->getU16(); // Sound ID - break; - } - default: - break; - } - - effectType = msg->getU8(); - } - + auto pos = getPosition(msg); + if (g_game.getProtocolVersion() >= 1203) { + // read all magic/sound effects + uint8_t delay = 0; + while (setMagicEffect(msg, pos, msg->getU8(), delay)); return; } uint16_t effectId = g_game.getFeature(Otc::GameMagicEffectU16) ? msg->getU16() : msg->getU8(); + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; if (g_game.getClientVersion() <= 750) { effectId += 1; //hack to fix effects in earlier clients } - if (!g_things.isValidDatId(effectId, ThingCategoryEffect)) { + if (!g_things.isValidDatId(effectId, ThingCategoryEffect, resourceId)) { g_logger.traceError("invalid effect id {}", effectId); return; } const auto& effect = std::make_shared(); - effect->setId(effectId); + effect->setId(effectId, resourceId); g_map.addThing(effect, pos); } @@ -1862,7 +1781,12 @@ void ProtocolGame::parseRemoveMagicEffect(const InputMessagePtr& msg) { getPosition(msg); uint16_t effectId = g_game.getFeature(Otc::GameEffectU16) ? msg->getU16() : msg->getU8(); - if (!g_things.isValidDatId(effectId, ThingCategoryEffect)) { + uint16_t resourceId = 0; + if (g_game.getFeature(Otc::GameMultiSpr)) { + resourceId = msg->getU16(); + } + + if (!g_things.isValidDatId(effectId, ThingCategoryEffect, resourceId)) { g_logger.warning("[ProtocolGame::parseRemoveMagicEffect] - Invalid effectId type {}", effectId); return; } @@ -1892,13 +1816,14 @@ void ProtocolGame::parseDistanceMissile(const InputMessagePtr& msg) const auto& toPos = getPosition(msg); const uint16_t shotId = g_game.getFeature(Otc::GameDistanceEffectU16) ? msg->getU16() : msg->getU8(); - if (!g_things.isValidDatId(shotId, ThingCategoryMissile)) { + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + if (!g_things.isValidDatId(shotId, ThingCategoryMissile, resourceId)) { g_logger.traceError("invalid missile id {}", shotId); return; } const auto& missile = std::make_shared(); - missile->setId(shotId); + missile->setId(shotId, resourceId); missile->setPath(fromPos, toPos); g_map.addThing(missile, fromPos); @@ -1906,28 +1831,48 @@ void ProtocolGame::parseDistanceMissile(const InputMessagePtr& msg) void ProtocolGame::parseForgeResult(const InputMessagePtr& msg) { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + ForgeResultData forgeResult; forgeResult.actionType = msg->getU8(); forgeResult.convergence = msg->getU8() == 1; forgeResult.success = msg->getU8() == 1; forgeResult.leftItemId = msg->getU16(); + if (multiSpr) { + forgeResult.leftItemResourceId = msg->getU16(); + } forgeResult.leftTier = msg->getU8(); forgeResult.rightItemId = msg->getU16(); + if (multiSpr) { + forgeResult.rightItemResourceId = msg->getU16(); + } forgeResult.rightTier = msg->getU8(); forgeResult.bonus = 0; forgeResult.coreCount = 0; - if (forgeResult.actionType == 1) { - msg->getU8(); // Bonus type always none for transfer - } else { - forgeResult.bonus = msg->getU8();// Roll fusion bonus - // Core kept - if (forgeResult.bonus == 2) { - forgeResult.coreCount = msg->getU8(); - } else if (forgeResult.bonus >= 4 && forgeResult.bonus <= 8) { - forgeResult.leftItemId = msg->getU16(); - forgeResult.leftTier = msg->getU8(); - } + /* + random event that can trigger during fusion: + 0 - nothing (normal fusion result) + 1 - dust not consumed + 2 - cores not consumed (u8 how many) + 3 - gold not consumed + 4 - item not consumed, lost 1 tier only (u16 item id to display) + 5 - second item kept, no tier loss (u16 item id to display) + 6 - both items upgraded (u16 item id to display) + 7 - item gained two tiers (u16 item id to display) + 8 - second item did not lose a tier (u16 item id to display) + */ + forgeResult.bonus = msg->getU8(); + if (forgeResult.bonus == 2) { + // cores not consumed + forgeResult.coreCount = msg->getU8(); + } else if (forgeResult.bonus >= 4 && forgeResult.bonus <= 8) { + // item related events (4-8) + forgeResult.outcomeItemId = msg->getU16(); + if (multiSpr) { + forgeResult.outcomeResourceId = msg->getU16(); + } + forgeResult.leftTier = msg->getU8(); } g_lua.callGlobalField("g_game", "forgeResultData", forgeResult); @@ -2049,6 +1994,9 @@ void ProtocolGame::parseCreatureMark(const InputMessagePtr& msg) void ProtocolGame::parseTrappers(const InputMessagePtr& msg) { + // deprecated open pvp feature + // possibly related to swapping places with allies in pvp situation + const uint8_t numTrappers = msg->getU8(); if (numTrappers > 8) { @@ -2068,17 +2016,22 @@ void ProtocolGame::parseTrappers(const InputMessagePtr& msg) void ProtocolGame::parseOpenForge(const InputMessagePtr& msg) { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + ForgeOpenData data; const uint16_t fusionCount = msg->getU16(); data.fusionItems.reserve(fusionCount); for (auto i = 0; i < fusionCount; ++i) { - ForgeItemInfo item; - msg->getU8(); // unknown count of friend items - item.id = msg->getU16(); - item.tier = msg->getU8(); - item.count = msg->getU16(); - data.fusionItems.emplace_back(item); + // unused list of items (follows the same structure as convergance) + // read without doing anything + const uint8_t items = msg->getU8(); + for (auto j = 0; j < items; ++j) { + getForgeItem(msg, multiSpr); + } + + // items for fusion + data.fusionItems.emplace_back(getForgeItem(msg, multiSpr)); } const uint16_t convergenceFusionCount = msg->getU16(); @@ -2087,65 +2040,16 @@ void ProtocolGame::parseOpenForge(const InputMessagePtr& msg) const uint8_t items = msg->getU8(); std::vector slotItems; slotItems.reserve(items); + for (auto j = 0; j < items; ++j) { - ForgeItemInfo item; - item.id = msg->getU16(); - item.tier = msg->getU8(); - item.count = msg->getU16(); - slotItems.emplace_back(item); + slotItems.emplace_back(getForgeItem(msg, multiSpr)); } data.convergenceFusion.emplace_back(slotItems); } - const uint8_t transferTotalCount = msg->getU8(); - data.transfers.reserve(transferTotalCount); - for (auto i = 0; i < transferTotalCount; ++i) { - ForgeTransferData transfer; - const uint16_t donorCount = msg->getU16(); - transfer.donors.reserve(donorCount); - for (auto j = 0; j < donorCount; ++j) { - ForgeItemInfo donor; - donor.id = msg->getU16(); - donor.tier = msg->getU8(); - donor.count = msg->getU16(); - transfer.donors.emplace_back(donor); - } - const uint16_t receiverCount = msg->getU16(); - transfer.receivers.reserve(receiverCount); - for (auto j = 0; j < receiverCount; ++j) { - ForgeItemInfo receiver; - receiver.id = msg->getU16(); - receiver.count = msg->getU16(); - receiver.tier = 0; - transfer.receivers.emplace_back(receiver); - } - data.transfers.emplace_back(transfer); - } + getForgeTransfers(msg, data.transfers, multiSpr); + getForgeTransfers(msg, data.convergenceTransfers, multiSpr); - const uint8_t convergenceTransferCount = msg->getU8(); - data.convergenceTransfers.reserve(convergenceTransferCount); - for (auto i = 0; i < convergenceTransferCount; ++i) { - ForgeTransferData transfer; - const uint16_t donorCount = msg->getU16(); - transfer.donors.reserve(donorCount); - for (auto j = 0; j < donorCount; ++j) { - ForgeItemInfo donor; - donor.id = msg->getU16(); - donor.tier = msg->getU8(); - donor.count = msg->getU16(); - transfer.donors.emplace_back(donor); - } - const uint16_t receiverCount = msg->getU16(); - transfer.receivers.reserve(receiverCount); - for (auto j = 0; j < receiverCount; ++j) { - ForgeItemInfo receiver; - receiver.id = msg->getU16(); - receiver.count = msg->getU16(); - receiver.tier = 0; - transfer.receivers.emplace_back(receiver); - } - data.convergenceTransfers.emplace_back(transfer); - } data.dustLevel = msg->getU16(); return g_lua.callGlobalField("g_game", "onOpenForge", data); @@ -2164,20 +2068,29 @@ void ProtocolGame::setCreatureVocation(const InputMessagePtr& msg, const uint32_ void ProtocolGame::addCreatureIcon(const InputMessagePtr& msg, const uint32_t creatureId) const { - const auto& creature = g_map.getCreatureById(creatureId); - if (!creature) { - g_logger.traceDebug("ProtocolGame::addCreatureIcon: could not get creature with id {}", creatureId); - return; - } - + // read the packet const uint8_t sizeIcons = msg->getU8(); std::vector> icons; // icon, category, count for (auto i = 0; i < sizeIcons; ++i) { - const uint8_t icon = msg->getU8(); // icon.serialize() - const uint8_t category = msg->getU8(); // icon.category -- 0x00 = monster // 0x01 = player? - const uint16_t count = msg->getU16(); // icon.count + // icon id + const uint8_t icon = msg->getU8(); + + // icon category + // 0 - monster icons (fiendish, weakened, etc.) + // 1 - quest icons (heat level, rascoohan score, etc.) + const uint8_t category = msg->getU8(); + + // number next to the icon + const uint16_t count = msg->getU16(); icons.emplace_back(icon, category, count); } + + // update the icons if creature found + const auto& creature = g_map.getCreatureById(creatureId); + if (!creature) { + g_logger.traceDebug("ProtocolGame::addCreatureIcon: could not get creature with id {}", creatureId); + return; + } creature->setIcons(icons); } @@ -2337,15 +2250,19 @@ void ProtocolGame::parseCreatureUnpass(const InputMessagePtr& msg) void ProtocolGame::parseEditText(const InputMessagePtr& msg) { - const uint32_t id = msg->getU32(); + const uint32_t id = msg->getU32(); // window unique id uint32_t itemId; + uint16_t resourceId = 0; if (g_game.getClientVersion() >= 1010 || g_game.getFeature(Otc::GameItemShader)) { // TODO: processEditText with ItemPtr as parameter const auto& item = getItem(msg); itemId = item->getId(); } else { itemId = msg->getU16(); + if (g_game.getFeature(Otc::GameMultiSpr)) { + resourceId = msg->getU16(); + } } const uint16_t maxLength = msg->getU16(); @@ -2354,7 +2271,7 @@ void ProtocolGame::parseEditText(const InputMessagePtr& msg) const auto& writer = msg->getString(); if (g_game.getClientVersion() >= 1281) { - msg->getU8(); // suffix + msg->getU8(); // bool: writer is "traded" character } std::string date; @@ -2362,13 +2279,13 @@ void ProtocolGame::parseEditText(const InputMessagePtr& msg) date = msg->getString(); } - g_game.processEditText(id, itemId, maxLength, text, writer, date); + g_game.processEditText(id, itemId, resourceId, maxLength, text, writer, date); } void ProtocolGame::parseEditList(const InputMessagePtr& msg) { const uint8_t doorId = msg->getU8(); - const uint32_t id = msg->getU32(); + const uint32_t id = msg->getU32(); // window unique id const auto& text = msg->getString(); g_game.processEditList(id, doorId, text); @@ -2424,16 +2341,18 @@ void ProtocolGame::parsePlayerInfo(const InputMessagePtr& msg) const void ProtocolGame::parsePlayerStats(const InputMessagePtr& msg) const { + const int version = g_game.getClientVersion(); + const uint32_t health = g_game.getFeature(Otc::GameDoubleHealth) ? msg->getU32() : msg->getU16(); const uint32_t maxHealth = g_game.getFeature(Otc::GameDoubleHealth) ? msg->getU32() : msg->getU16(); uint32_t freeCapacity = g_game.getFeature(Otc::GameDoubleFreeCapacity) ? msg->getU32() : msg->getU16(); - if (g_game.getClientVersion() > 772) { + if (version > 772) { // todo: We only know scaling started some time after 7.72; the 772 cutoff is a placeholder until we find the exact version. freeCapacity /= 100; } uint32_t totalCapacity = 0; - if (g_game.getClientVersion() < 1281 && g_game.getFeature(Otc::GameTotalCapacity)) { + if (version < 1281 && g_game.getFeature(Otc::GameTotalCapacity)) { totalCapacity = msg->getU32() / 100.f; } @@ -2442,13 +2361,13 @@ void ProtocolGame::parsePlayerStats(const InputMessagePtr& msg) const const uint8_t levelPercent = msg->getU8(); if (g_game.getFeature(Otc::GameExperienceBonus)) { - if (g_game.getClientVersion() <= 1096) { + if (version <= 1096) { const double experienceBonus = msg->getDouble(); m_localPlayer->setExperienceRate(Otc::EXP_BASE, experienceBonus * 100); } else { const uint16_t baseXpGain = msg->getU16(); m_localPlayer->setExperienceRate(Otc::EXP_BASE, baseXpGain); - if (g_game.getClientVersion() < 1281) { + if (version < 1281) { const uint16_t voucherAddend = msg->getU16(); m_localPlayer->setExperienceRate(Otc::EXP_VOUCHER, voucherAddend); } @@ -2466,7 +2385,7 @@ void ProtocolGame::parsePlayerStats(const InputMessagePtr& msg) const uint32_t manaShield = 0; uint32_t maxManaShield = 0; - if (g_game.getClientVersion() < 1281) { + if (version < 1281) { const uint8_t magicLevel = msg->getU8(); const uint8_t baseMagicLevel = g_game.getFeature(Otc::GameSkillsBase) ? msg->getU8() : magicLevel; const uint8_t magicLevelPercent = msg->getU8(); @@ -2481,12 +2400,12 @@ void ProtocolGame::parsePlayerStats(const InputMessagePtr& msg) const const uint16_t regeneration = g_game.getFeature(Otc::GamePlayerRegenerationTime) ? msg->getU16() : 0; const uint16_t training = g_game.getFeature(Otc::GameOfflineTrainingTime) ? msg->getU16() : 0; - if (g_game.getClientVersion() >= 1097) { + if (version >= 1097) { m_localPlayer->setStoreExpBoostTime(msg->getU16()); // xp boost time (seconds) msg->getU8(); // enables exp boost in the store } - if (g_game.getClientVersion() >= 1281) { + if (version >= 1281) { if (g_game.getFeature(Otc::GameDoubleHealth)) { manaShield = msg->getU32(); // remaining mana shield maxManaShield = msg->getU32(); // total mana shield @@ -2502,7 +2421,7 @@ void ProtocolGame::parsePlayerStats(const InputMessagePtr& msg) const m_localPlayer->setExperience(experience); m_localPlayer->setLevel(level, levelPercent); m_localPlayer->setMana(mana, maxMana); - if (g_game.getClientVersion() >= 1281) + if (version >= 1281) m_localPlayer->setManaShield(manaShield, maxManaShield); else m_localPlayer->setManaShield(0, 0); @@ -2515,7 +2434,8 @@ void ProtocolGame::parsePlayerStats(const InputMessagePtr& msg) const void ProtocolGame::parsePlayerSkills(const InputMessagePtr& msg) const { - if (g_game.getClientVersion() >= 1281) { + const int version = g_game.getClientVersion(); + if (version >= 1281) { // magic level const uint16_t magicLevel = msg->getU16(); const uint16_t baseMagicLevel = msg->getU16(); @@ -2538,7 +2458,7 @@ void ProtocolGame::parsePlayerSkills(const InputMessagePtr& msg) const uint16_t levelPercent = 0; - if (g_game.getClientVersion() >= 1281) { + if (version >= 1281) { msg->getU16(); // base + loyalty bonus(?) levelPercent = msg->getU16() / 100; } else { @@ -2570,7 +2490,7 @@ void ProtocolGame::parsePlayerSkills(const InputMessagePtr& msg) const } if (g_game.getFeature(Otc::GameForgeSkillStats)) { - const uint8_t lastSkill = g_game.getClientVersion() >= 1332 ? Otc::LastSkill : Otc::Momentum + 1; + const uint8_t lastSkill = version >= 1332 ? Otc::LastSkill : Otc::Momentum + 1; for (int_fast32_t skill = Otc::Fatal; skill < lastSkill; ++skill) { const uint16_t level = msg->getU16(); const uint16_t baseLevel = msg->getU16(); @@ -2615,7 +2535,7 @@ void ProtocolGame::parsePlayerSkills(const InputMessagePtr& msg) const // Defense info const uint16_t defense = msg->getU16(); const uint16_t armor = msg->getU16(); - if (g_game.getClientVersion() >= 1500) { + if (version >= 1500) { msg->getU16(); // getMantraTotal } const double mitigation = msg->getDouble(); @@ -2704,7 +2624,7 @@ void ProtocolGame::parseTalk(const InputMessagePtr& msg) const auto& name = g_game.formatCreatureName(msg->getString()); if (statement > 0 && g_game.getClientVersion() >= 1281) { - msg->getU8(); // suffix + msg->getU8(); // "traded" suffix } const uint16_t level = g_game.getFeature(Otc::GameMessageLevel) ? msg->getU16() : 0; @@ -2975,39 +2895,63 @@ void ProtocolGame::parseFloorChangeDown(const InputMessagePtr& msg) void ProtocolGame::parseOpenOutfitWindow(const InputMessagePtr& msg) const { - const auto& currentOutfit = getOutfit(msg); + /* + packet structure: + PART 1: CURRENT OUTFIT + 1. outfit, addons, colors + 2. mount, colors (always present) + 3. wings, auras, etc. + 4. familiar - // mount color bytes are required here regardless of having one - if (g_game.getClientVersion() >= 1281) { - if (currentOutfit.getMount() == 0) { - msg->getU8(); //head - msg->getU8(); //body - msg->getU8(); //legs - msg->getU8(); //feet + PART 2: AVAILABLE COSMETICS + 1. available outfits + 2. available mounts + 3. available familiars + 4. flags: try on, mounted, randomize mount + 5. available other features (wings, auras, etc) + + this window requires reworking to accommodate resource ids for multiSpr feature + */ + + // multi resource system + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + + // at some point the amount of obtainable mounts in game exceeded 256 + // this was when the list size got increased to u16 + const bool cosmeticsU16 = g_game.getClientVersion() >= 1281; + + // outfit addons + const bool addons = g_game.getFeature(Otc::GamePlayerAddons); + + // currenrly selected cosmetics (except for familiar) + auto currentOutfit = getOutfit(msg, true, true); + + // currently selected familiar + if (g_game.getFeature(Otc::GamePlayerFamiliars)) { + SimpleOutfit familiar; + familiar.type = msg->getU16(); // current familiar looktype + if (multiSpr) { + familiar.resourceId = msg->getU16(); // resourceId } - msg->getU16(); // current familiar looktype + currentOutfit.applyFamiliar(familiar); } - std::vector> outfitList; + // lists + std::vector outfitList; + std::vector mountList; + std::vector familiarList; + std::vector wingList; + std::vector auraList; + std::vector effectList; + std::vector shaderList; + // outfits if (g_game.getFeature(Otc::GameNewOutfitProtocol)) { - const uint16_t outfitCount = g_game.getClientVersion() >= 1281 ? msg->getU16() : msg->getU8(); - for (auto i = 0; i < outfitCount; ++i) { - const uint16_t outfitId = msg->getU16(); - const auto& outfitName = msg->getString(); - const uint8_t outfitAddons = msg->getU8(); - uint8_t outfitMode = 0; - if (g_game.getClientVersion() >= 1281) { - outfitMode = msg->getU8(); // mode: 0x00 - available, 0x01 store (requires U32 store offerId), 0x02 golden outfit tooltip (hardcoded) - if (outfitMode == 1) { - msg->getU32(); - } - } - - outfitList.emplace_back(outfitId, outfitName, outfitAddons, outfitMode); - } + // 10.98+ outfit window + getOutfitWindowCosmeticsList(msg, outfitList, cosmeticsU16, addons, multiSpr); } else { + // 7.x outfit window uint16_t outfitStart; uint16_t outfitEnd; if (g_game.getFeature(Otc::GameLooktypeU16)) { @@ -3019,85 +2963,55 @@ void ProtocolGame::parseOpenOutfitWindow(const InputMessagePtr& msg) const } for (auto i = outfitStart; i <= outfitEnd; ++i) { - outfitList.emplace_back(i, "", 0, 0); + OutfitWindowThing o; + o.id = i; + outfitList.emplace_back(o); } } - std::vector> mountList; - + // mounts if (g_game.getFeature(Otc::GamePlayerMounts)) { - const uint16_t mountCount = g_game.getClientVersion() >= 1281 ? msg->getU16() : msg->getU8(); - for (auto i = 0; i < mountCount; ++i) { - const uint16_t mountId = msg->getU16(); // mount type - const auto& mountName = msg->getString(); // mount name - uint8_t mountMode = 0; - if (g_game.getClientVersion() >= 1281) { - mountMode = msg->getU8(); // mode: 0x00 - available, 0x01 store (requires U32 store offerId) - if (mountMode == 1) { - msg->getU32(); - } - } - - mountList.emplace_back(mountId, mountName, mountMode); - } + getOutfitWindowCosmeticsList(msg, mountList, cosmeticsU16, false, multiSpr); } - std::vector > familiarList; + // familiars if (g_game.getFeature(Otc::GamePlayerFamiliars)) { - const uint16_t familiarCount = msg->getU16(); - for (auto i = 0; i < familiarCount; ++i) { - const uint16_t familiarLookType = msg->getU16(); // familiar lookType - const auto& familiarName = msg->getString(); // familiar name - const uint8_t familiarMode = msg->getU8(); // 0x00 // mode: 0x00 - available, 0x01 store (requires U32 store offerId) - if (familiarMode == 1) { - msg->getU32(); - } - familiarList.emplace_back(familiarLookType, familiarName); - } + getOutfitWindowCosmeticsList(msg, familiarList, cosmeticsU16, false, multiSpr); } - if (g_game.getClientVersion() >= 1281) { - msg->getU8(); // Try outfit mode (?) - msg->getU8(); // (bool) mounted - msg->getU8(); // (bool) randomize mount - } + // extended cosmetics + if (g_game.getFeature(Otc::GameWingsAurasEffectsShader)) { + // bool visible in outfit window + // if visible: read full list + if (msg->getU8() != 0) + getOutfitWindowCosmeticsList(msg, wingList, cosmeticsU16, false, multiSpr); - std::vector> wingList; - std::vector> auraList; - std::vector> effectList; - std::vector> shaderList; + if (msg->getU8() != 0) + getOutfitWindowCosmeticsList(msg, auraList, cosmeticsU16, false, multiSpr); - if (g_game.getFeature(Otc::GameWingsAurasEffectsShader)) { - const uint8_t wingCount = msg->getU8(); - for (auto i = 0; i < wingCount; ++i) { - const uint16_t wingId = msg->getU16(); - const auto& wingName = msg->getString(); - wingList.emplace_back(wingId, wingName); - } + if (msg->getU8() != 0) + getOutfitWindowCosmeticsList(msg, effectList, cosmeticsU16, false, multiSpr, true); - const uint8_t auraCount = msg->getU8(); - for (auto i = 0; i < auraCount; ++i) { - const uint16_t auraId = msg->getU16(); - const auto& auraName = msg->getString(); - auraList.emplace_back(auraId, auraName); - } + if (msg->getU8() != 0) + getOutfitWindowCosmeticsList(msg, shaderList, cosmeticsU16, false, multiSpr, true); + } - const uint8_t effectCount = msg->getU8(); - for (auto i = 0; i < effectCount; ++i) { - const uint16_t effectId = msg->getU16(); - const auto& effectName = msg->getString(); - effectList.emplace_back(effectId, effectName); - } + // window mode + // 0 - set outfit, 1 - try outfit, 2 - try mount + uint8_t windowMode = 0; + bool mounted = false; + bool randomizeMount = false; + if (g_game.getClientVersion() >= 1281) { + windowMode = msg->getU8(); + mounted = msg->getU8() != 0; - const uint8_t shaderCount = msg->getU8(); - for (auto i = 0; i < shaderCount; ++i) { - const uint16_t shaderId = msg->getU16(); - const auto& shaderName = msg->getString(); - shaderList.emplace_back(shaderId, shaderName); + // randomize mount boolean is only present in the packet when the window type is 0 + if (windowMode != 0) { + randomizeMount = msg->getU8() != 0; } } - g_game.processOpenOutfitWindow(currentOutfit, outfitList, mountList, familiarList, wingList, auraList, effectList, shaderList); + g_game.processOpenOutfitWindow(currentOutfit, outfitList, mountList, familiarList, wingList, auraList, effectList, shaderList, windowMode, mounted, randomizeMount); } void ProtocolGame::parseQuestTracker(const InputMessagePtr& msg) @@ -3477,7 +3391,10 @@ void ProtocolGame::parseItemInfo(const InputMessagePtr& msg) const for (auto i = 0; i < listCount; ++i) { const auto& item = std::make_shared(); - item->setId(msg->getU16()); + const uint16_t itemId = msg->getU16(); + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + + item->setId(itemId, resourceId); item->setCountOrSubType(g_game.getFeature(Otc::GameCountU16) ? msg->getU16() : msg->getU8()); const auto& desc = msg->getString(); itemList.emplace_back(item, desc); @@ -3487,6 +3404,11 @@ void ProtocolGame::parseItemInfo(const InputMessagePtr& msg) const } static inline uint32_t readPackedCount1500(const InputMessagePtr& msg) { + // since version 15.00, the item counts on the action bar use dynamic data length + // this is to shorten the packet length when the player is holding a lot of items + // there are two reasons for this: + // 1. this packet is sent very often - every time something changes in the player inventory + // 2. the maximum theoretical size of this packet got close to 40k bytes const uint8_t b1 = msg->getU8(); if (b1 < 0x40) { return b1; @@ -3511,25 +3433,17 @@ void ProtocolGame::parsePlayerInventory(const InputMessagePtr& msg) return; } - std::map, uint32_t> inventoryCounts; - + std::vector inventoryCounts; + inventoryCounts.reserve(size); + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + const bool simple = g_game.getProtocolVersion() < 1500; for (uint16_t i = 0; std::cmp_less(i, size); ++i) { - const uint16_t itemId = msg->getU16(); - const uint8_t attribute = msg->getU8(); - - const uint32_t amount = g_game.getProtocolVersion() < 1500 ? msg->getU16() : readPackedCount1500(msg); - - uint8_t tier = 0; - if (const auto thingType = g_things.getThingType(itemId, ThingCategoryItem)) { - if (std::cmp_greater(thingType->getClassification(), 0)) { - tier = attribute; - } - } - - const auto key = std::make_pair(itemId, tier); - auto& entry = inventoryCounts[key]; - const uint64_t sum = static_cast(entry) + amount; - entry = static_cast(std::min(sum, (std::numeric_limits::max)())); + ActionBarItem item; + item.id = msg->getU16(); + item.resourceId = multiSpr ? msg->getU16() : 0; + item.subType = msg->getU8(); + item.count = simple ? msg->getU16() : readPackedCount1500(msg); + inventoryCounts.emplace_back(std::move(item)); } if (const auto& localPlayer = g_game.getLocalPlayer()) { @@ -3703,7 +3617,12 @@ int ProtocolGame::setTileDescription(const InputMessagePtr& msg, const Position g_logger.traceError("ProtocolGame::setTileDescription: too many things, pos={}, stackpos={}", position, stackPos); } - const auto& thing = getThing(msg); + const auto thing = getThing(msg); + if (!thing) { + g_logger.traceError("ProtocolGame::setTileDescription: failed to get thing at pos={}, stackpos={}", position, stackPos); + continue; + } + if (thing->isLocalPlayer()) { thing->static_self_cast()->resetPreWalk(); } @@ -3714,67 +3633,179 @@ int ProtocolGame::setTileDescription(const InputMessagePtr& msg, const Position return 0; } -Outfit ProtocolGame::getOutfit(const InputMessagePtr& msg, const bool parseMount/* = true*/) const +bool ProtocolGame::setMagicEffect(const InputMessagePtr& msg, Position& pos, uint8_t effectType, uint8_t& delay) { + uint16_t resourceId = 0; + switch (effectType) { + case Otc::MAGIC_EFFECTS_END_LOOP: + // returning false ends the "while" loop + return false; + case Otc::MAGIC_EFFECTS_DELAY: + delay = msg->getU8(); + break; + case Otc::MAGIC_EFFECTS_DELTA: { + // packed magic effect offset + pos.offsetByDelta(m_localPlayer->getPosition(), msg->getU8()); + break; + } + + case Otc::MAGIC_EFFECTS_CREATE_DISTANCEEFFECT: + case Otc::MAGIC_EFFECTS_CREATE_DISTANCEEFFECT_REVERSED: { + const uint16_t shotId = g_game.getFeature(Otc::GameEffectU16) ? msg->getU16() : msg->getU8(); + if (g_game.getFeature(Otc::GameMultiSpr)) { + resourceId = msg->getU16(); + } + const auto offsetX = static_cast(msg->getU8()); + const auto offsetY = static_cast(msg->getU8()); + if (!g_things.isValidDatId(shotId, ThingCategoryMissile, resourceId)) { + g_logger.traceError("invalid missile id {}", shotId); + + // end the "while" loop + return false; + } + + g_dispatcher.scheduleEvent([posCopy = Position(pos), shotId, resourceId, offsetX, offsetY, effectType] { + const auto& missile = std::make_shared(); + missile->setId(shotId, resourceId); + + if (effectType == Otc::MAGIC_EFFECTS_CREATE_DISTANCEEFFECT) { + missile->setPath(posCopy, Position(posCopy.x + offsetX, posCopy.y + offsetY, posCopy.z)); + } else { + missile->setPath(Position(posCopy.x + offsetX, posCopy.y + offsetY, posCopy.z), posCopy); + } + + g_map.addThing(missile, posCopy); + }, delay); + break; + } + + case Otc::MAGIC_EFFECTS_CREATE_EFFECT: { + const uint16_t effectId = g_game.getFeature(Otc::GameEffectU16) ? msg->getU16() : msg->getU8(); + if (g_game.getFeature(Otc::GameMultiSpr)) { + resourceId = msg->getU16(); + } + + if (!g_things.isValidDatId(effectId, ThingCategoryEffect, resourceId)) { + g_logger.traceError("invalid effect id {}", effectId); + + // effect cannot be drawn + // but the loop may continue + break; + } + + g_dispatcher.scheduleEvent([posCopy = Position(pos), effectId, resourceId, effectType] { + const auto& effect = std::make_shared(); + effect->setId(effectId, resourceId); + g_map.addThing(effect, posCopy); + }, delay); + break; + } + + case Otc::MAGIC_EFFECTS_CREATE_SOUND_MAIN_EFFECT: { + msg->getU8(); // Source + msg->getU16(); // Sound ID + break; + } + + case Otc::MAGIC_EFFECTS_CREATE_SOUND_SECONDARY_EFFECT: { + msg->getU8(); // ENUM + msg->getU8(); // Source + msg->getU16(); // Sound ID + break; + } + default: + break; + } + + // continue the "while" loop + return true; +} + +Outfit ProtocolGame::getOutfit(const InputMessagePtr& msg, const bool parseMount/* = true*/, const bool forceReadMountColors /* = false*/) const +{ + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + Outfit outfit; - uint16_t lookType = g_game.getFeature(Otc::GameLooktypeU16) ? msg->getU16() : msg->getU8(); + ColorOutfit baseOutfit; + baseOutfit.type = g_game.getFeature(Otc::GameLooktypeU16) ? msg->getU16() : msg->getU8(); + baseOutfit.resourceId = multiSpr ? msg->getU16() : 0; - if (lookType != 0) { + if (baseOutfit.type != 0) { outfit.setCategory(ThingCategoryCreature); - const uint8_t head = msg->getU8(); - const uint8_t body = msg->getU8(); - const uint8_t legs = msg->getU8(); - const uint8_t feet = msg->getU8(); + baseOutfit.head = msg->getU8(); + baseOutfit.body = msg->getU8(); + baseOutfit.legs = msg->getU8(); + baseOutfit.feet = msg->getU8(); + baseOutfit.applyColors(); + const uint8_t addons = g_game.getFeature(Otc::GamePlayerAddons) ? msg->getU8() : 0; - if (!g_things.isValidDatId(lookType, ThingCategoryCreature)) { - g_logger.traceError("invalid outfit looktype {}", lookType); - lookType = 0; + if (!g_things.isValidDatId(baseOutfit.type, ThingCategoryCreature, baseOutfit.resourceId)) { + g_logger.traceError("invalid outfit looktype {}", baseOutfit.type); + baseOutfit.type = 0; } - outfit.setId(lookType); - outfit.setHead(head); - outfit.setBody(body); - outfit.setLegs(legs); - outfit.setFeet(feet); outfit.setAddons(addons); } else { - uint16_t lookTypeEx = msg->getU16(); - if (lookTypeEx == 0) { + baseOutfit.typeEx = msg->getU16(); + + if (baseOutfit.typeEx == 0) { outfit.setCategory(ThingCategoryEffect); - outfit.setAuxId(13); // invisible effect id + baseOutfit.typeEx = 13; // invisible effect id + baseOutfit.resourceId = 0; } else { - if (!g_things.isValidDatId(lookTypeEx, ThingCategoryItem)) { - g_logger.traceError("invalid outfit looktypeex {}", lookTypeEx); - lookTypeEx = 0; + if (!g_things.isValidDatId(baseOutfit.typeEx, ThingCategoryItem, baseOutfit.resourceId)) { + g_logger.traceError("invalid outfit looktypeex {}", baseOutfit.typeEx); + baseOutfit.typeEx = 0; + baseOutfit.resourceId = 0; } outfit.setCategory(ThingCategoryItem); - outfit.setAuxId(lookTypeEx); } } - + outfit.applyOutfit(baseOutfit); + if (g_game.getFeature(Otc::GamePlayerMounts) && parseMount) { - const uint16_t mount = msg->getU16(); - if (g_game.getClientVersion() >= 1281 && mount != 0) { - msg->getU8(); //head - msg->getU8(); //body - msg->getU8(); //legs - msg->getU8(); //feet + ColorOutfit mount; + mount.type = msg->getU16(); + mount.resourceId = multiSpr ? msg->getU16() : 0; + + if (g_game.getClientVersion() >= 1281 && (mount.type != 0 || forceReadMountColors)) { + mount.head = msg->getU8(); + mount.body = msg->getU8(); + mount.legs = msg->getU8(); + mount.feet = msg->getU8(); + mount.applyColors(); } - outfit.setMount(mount); + outfit.applyMount(mount); } if (g_game.getFeature(Otc::GameWingsAurasEffectsShader) && parseMount) { - const uint16_t wings = msg->getU16(); - outfit.setWing(wings); - - const uint16_t auras = msg->getU16(); - outfit.setAura(auras); - - const uint16_t effects = msg->getU16(); - outfit.setEffect(effects); - + // wings + SimpleOutfit wings; + wings.type = msg->getU16(); + wings.resourceId = multiSpr ? msg->getU16() : 0; + outfit.applyWings(wings); + + // aura + EffectOutfit aura; + aura.type = msg->getU16(); + if (multiSpr) { + aura.resourceId = msg->getU16(); + aura.category = static_cast(msg->getU8()); + } + outfit.applyAura(aura); + + // effect + EffectOutfit effect; + effect.type = msg->getU16(); + if (multiSpr) { + effect.resourceId = msg->getU16(); + effect.category = static_cast(msg->getU8()); + } + outfit.applyParticles(effect); + + // shader outfit.setShader(msg->getString()); } @@ -3832,236 +3863,22 @@ CreaturePtr ProtocolGame::getCreature(const InputMessagePtr& msg, int type) cons CreaturePtr creature; const bool known = type != Proto::UnknownCreature; if (type == Proto::OutdatedCreature || type == Proto::UnknownCreature) { - if (known) { - const uint32_t creatureId = msg->getU32(); - creature = g_map.getCreatureById(creatureId); - if (!creature) { - g_logger.traceError("ProtocolGame::getCreature: server said that a creature is known, but it's not"); - } - } else { - const uint32_t removeId = msg->getU32(); - const uint32_t id = msg->getU32(); + internalGetCreature(msg, creature, known); + } else if (type == Proto::Creature) { + // this is send creature turn + const uint32_t creatureId = msg->getU32(); + creature = g_map.getCreatureById(creatureId); + if (!creature) { + g_logger.traceError("ProtocolGame::getCreature: invalid creature"); + } - if (id == removeId) { - creature = g_map.getCreatureById(id); - } else { - g_map.removeCreatureById(removeId); - } + const auto direction = static_cast(msg->getU8()); + if (creature) { + creature->turn(direction); + } - uint8_t creatureType; - if (g_game.getClientVersion() >= 910) { - creatureType = msg->getU8(); - } else { - if (id >= Proto::PlayerStartId && id < Proto::PlayerEndId) - creatureType = Proto::CreatureTypePlayer; - else if (id >= Proto::MonsterStartId && id < Proto::MonsterEndId) - creatureType = Proto::CreatureTypeMonster; - else - creatureType = Proto::CreatureTypeNpc; - } - - uint32_t masterId = 0; - if (g_game.getClientVersion() >= 1281 && creatureType == Proto::CreatureTypeSummonOwn) { - masterId = msg->getU32(); - if (m_localPlayer->getId() != masterId) { - creatureType = Proto::CreatureTypeSummonOther; - } - } - - const auto& name = g_game.formatCreatureName(msg->getString()); - - if (!creature) { - if ((id == m_localPlayer->getId()) || - // fixes a bug server side bug where GameInit is not sent and local player id is unknown - (creatureType == Proto::CreatureTypePlayer && !m_localPlayer->getId() && name == m_localPlayer->getName())) { - creature = m_localPlayer; - } else { - switch (creatureType) { - case Proto::CreatureTypePlayer: - creature = std::make_shared(); - break; - - case Proto::CreatureTypeNpc: - creature = std::make_shared(); - break; - - case Proto::CreatureTypeHidden: - case Proto::CreatureTypeMonster: - case Proto::CreatureTypeSummonOwn: - case Proto::CreatureTypeSummonOther: - creature = std::make_shared(); - break; - - default: - g_logger.traceError("ProtocolGame::getCreature: creature type is invalid"); - } - - if (creature) { - creature->onCreate(); - } - } - } - - if (creature) { - creature->setId(id); - creature->setName(name); - creature->setMasterId(masterId); - - g_map.addCreature(creature); - } - } - - const uint8_t healthPercent = msg->getU8(); - const auto direction = static_cast(msg->getU8()); - const auto& outfit = getOutfit(msg); - - Light light; - light.intensity = msg->getU8(); - light.color = msg->getU8(); - - const uint16_t speed = msg->getU16(); - - if (g_game.getClientVersion() >= 1281) { - addCreatureIcon(msg, creature->getId()); - } - - const uint8_t skull = msg->getU8(); - const uint8_t shield = msg->getU8(); - - // emblem is sent only when the creature is not known - uint8_t emblem = 0; - uint8_t creatureType = 0; - uint8_t icon = 0; - bool unpass = true; - - if (g_game.getFeature(Otc::GameCreatureEmblems) && !known) { - emblem = msg->getU8(); - } - - if (g_game.getFeature(Otc::GameThingMarks)) { - creatureType = msg->getU8(); - } - - uint32_t masterId = 0; - if (g_game.getClientVersion() >= 1281) { - if (creatureType == Proto::CreatureTypeSummonOwn) { - masterId = msg->getU32(); - if (m_localPlayer->getId() != masterId) { - creatureType = Proto::CreatureTypeSummonOther; - } - } else if (creatureType == Proto::CreatureTypePlayer) { - uint8_t vocationId = msg->getU8(); - creature->setVocation(vocationId); - } - } - - if (g_game.getFeature(Otc::GameCreatureIcons)) { - icon = msg->getU8(); - } - - if (g_game.getFeature(Otc::GameThingMarks)) { - const uint8_t mark = msg->getU8(); // mark - - if (g_game.getClientVersion() < 1281) { - msg->getU16(); // helpers - } - - if (creature) { - if (mark == 0xff) { - creature->hideStaticSquare(); - } else { - creature->showStaticSquare(Color::from8bit(mark)); - } - } - } - - if (g_game.getClientVersion() >= 1281) { - msg->getU8(); // inspection type - } - - if (g_game.getClientVersion() >= 854) { - 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(); - } - - std::vector attachedEffectList; - if (g_game.getFeature(Otc::GameCreatureAttachedEffect)) { - const uint8_t listSize = msg->getU8(); - for (auto i = -1; ++i < listSize;) { - attachedEffectList.push_back(msg->getU16()); - } - } - - if (creature) { - creature->setHealthPercent(healthPercent); - creature->turn(direction); - creature->setOutfit(outfit); - creature->setSpeed(speed); - creature->setSkull(skull); - creature->setShield(shield); - creature->setPassable(!unpass); - creature->setLight(light); - creature->setMasterId(masterId); - creature->setShader(shader); - creature->clearTemporaryAttachedEffects(); - std::unordered_set currentAttachedEffectIds; - for (const auto& attachedEffect : creature->getAttachedEffects()) { - currentAttachedEffectIds.insert(attachedEffect->getId()); - } - - for (const auto effectId : attachedEffectList) { - const auto& effect = g_attachedEffects.getById(effectId); - if (effect && currentAttachedEffectIds.find(effectId) == currentAttachedEffectIds.end()) { - const auto& clonedEffect = effect->clone(); - clonedEffect->setPermanent(false); - creature->attachEffect(clonedEffect); - } - } - - if (emblem > 0) { - creature->setEmblem(emblem); - } - - if (creatureType > 0) { - creature->setType(creatureType); - } - - if (icon > 0) { - creature->setIcon(icon); - } - - if (creature == m_localPlayer && !m_localPlayer->isKnown()) { - m_localPlayer->setKnown(true); - } - } - } else if (type == Proto::Creature) { - // this is send creature turn - const uint32_t creatureId = msg->getU32(); - creature = g_map.getCreatureById(creatureId); - if (!creature) { - g_logger.traceError("ProtocolGame::getCreature: invalid creature"); - } - - const auto direction = static_cast(msg->getU8()); - if (creature) { - creature->turn(direction); - } - - if (g_game.getClientVersion() >= 953) { - const bool unpass = static_cast(msg->getU8()); + if (g_game.getClientVersion() >= 953) { + const bool unpass = static_cast(msg->getU8()); if (creature) { creature->setPassable(!unpass); @@ -4080,7 +3897,9 @@ ItemPtr ProtocolGame::getItem(const InputMessagePtr& msg, int id) id = msg->getU16(); } - const auto& item = Item::create(id); + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + + const auto& item = Item::create(id, resourceId); if (!item) { throw Exception("ProtocolGame::getItem: unable to create item with invalid id {}", id); @@ -4161,9 +3980,13 @@ ItemPtr ProtocolGame::getItem(const InputMessagePtr& msg, int id) } if (g_game.getFeature(Otc::GameThingPodium)) { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); if (item->isPodium()) { const uint16_t looktype = msg->getU16(); if (looktype != 0) { + if (multiSpr) { + msg->getU16(); // outfit resource id + } msg->getU8(); // lookHead msg->getU8(); // lookBody msg->getU8(); // lookLegs @@ -4171,10 +3994,16 @@ ItemPtr ProtocolGame::getItem(const InputMessagePtr& msg, int id) msg->getU8(); // lookAddons } else if (g_game.getFeature(Otc::GameThingPodiumItemType)) { msg->getU16(); // LookTypeEx + if (multiSpr) { + msg->getU16(); // item resource id + } } const uint16_t lookmount = msg->getU16(); if (lookmount != 0) { + if (multiSpr) { + msg->getU16(); // mount resource id + } msg->getU8(); // lookHead msg->getU8(); // lookBody msg->getU8(); // lookLegs @@ -4182,7 +4011,7 @@ ItemPtr ProtocolGame::getItem(const InputMessagePtr& msg, int id) } msg->getU8(); // direction - msg->getU8(); // visible (bool) + msg->getU8(); // bool: show platform (when false: only outfit is displayed) } } @@ -4269,12 +4098,12 @@ void ProtocolGame::parseTaskHuntingBasicData(const InputMessagePtr& msg) const uint16_t preys = msg->getU16(); for (auto i = 0; i < preys; ++i) { msg->getU16(); // RaceID - msg->getU8(); // Difficult + msg->getU8(); // Difficulty } const uint8_t options = msg->getU8(); for (auto i = 0; i < options; ++i) { - msg->getU8(); // Difficult + msg->getU8(); // Difficulty msg->getU8(); // Stars msg->getU16(); // First kill msg->getU16(); // First reward @@ -4344,20 +4173,33 @@ void ProtocolGame::parseExperienceTracker(const InputMessagePtr& msg) void ProtocolGame::parseLootContainers(const InputMessagePtr& msg) { + // checkbox: fallback to main container const bool quickLootFallbackToMainContainer = static_cast(msg->getU8()); + // list of configured loot containers [category, quickloot bp, retrieve bp] + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); const uint8_t containersCount = msg->getU8(); - std::vector> lootList; - + std::vector lootList; + lootList.reserve(containersCount); for (auto i = 0; i < containersCount; ++i) { - const uint8_t categoryType = msg->getU8(); - const uint16_t lootContainerId = msg->getU16(); - uint16_t obtainerContainerId = 0; + LootContainerConf conf; + conf.categoryType = msg->getU8(); + + // container for monster loot + conf.lootId = msg->getU16(); + if (multiSpr) { + conf.lootResourceId = msg->getU16(); + } + + // container for npc products (and stash retrieve?) if (g_game.getClientVersion() >= 1332) { - obtainerContainerId = msg->getU16(); + conf.retrieveId = msg->getU16(); + if (multiSpr) { + conf.retrieveResourceId = msg->getU16(); + } } - lootList.emplace_back(categoryType, lootContainerId, obtainerContainerId); + lootList.emplace_back(conf); } g_lua.callGlobalField("g_game", "onQuickLootContainers", quickLootFallbackToMainContainer, lootList); @@ -4427,7 +4269,7 @@ void ProtocolGame::parseCyclopediaHouseList(const InputMessagePtr& msg) { const uint16_t housesCount = msg->getU16(); // housesCount for (auto i = 0; i < housesCount; ++i) { - msg->getU32(); // clientId + msg->getU32(); // house id in staticdata msg->getU8(); // 0x00 = Renovation, 0x01 = Available const auto type = static_cast(msg->getU8()); @@ -4505,12 +4347,14 @@ void ProtocolGame::parseCyclopediaHouseList(const InputMessagePtr& msg) void ProtocolGame::parseSupplyStash(const InputMessagePtr& msg) { const uint16_t itemsCount = msg->getU16(); - std::vector> stashItems; + std::vector> stashItems; + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); for (auto i = 0; i < itemsCount; ++i) { uint16_t itemId = msg->getU16(); + uint16_t resourceId = multiSpr ? msg->getU16() : 0; uint32_t amount = msg->getU32(); - stashItems.push_back({ itemId, amount }); + stashItems.push_back({ itemId, amount, resourceId }); } if (g_game.getProtocolVersion() < 1410) { msg->getU16(); // free slots @@ -4543,7 +4387,7 @@ void ProtocolGame::parsePartyAnalyzer(const InputMessagePtr& msg) for (auto i = 0; i < partyMembersSize; ++i) { const uint32_t memberID = msg->getU32(); // party member id const uint8_t highlight = msg->getU8(); // highlight - const uint64_t loot = msg->getU64(); // loot + const uint64_t loot = msg->getU64(); // loot (profit in gp) const uint64_t supply = msg->getU64(); // supply const uint64_t damage = msg->getU64(); // damage const uint64_t healing = msg->getU64(); // healing @@ -4708,12 +4552,15 @@ void ProtocolGame::parseUpdateImpactTracker(const InputMessagePtr& msg) void ProtocolGame::parseItemsPrice(const InputMessagePtr& msg) { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + const uint16_t priceCount = msg->getU16(); // count for (auto i = 0; i < priceCount; ++i) { const uint16_t itemId = msg->getU16(); // item client id + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; if (g_game.getClientVersion() >= 1281) { - const auto& item = Item::create(itemId); + const auto& item = Item::create(itemId, resourceId); // note: vanilla client allows made-up client ids // their classification is assumed as 0 @@ -4732,9 +4579,10 @@ void ProtocolGame::parseItemsPrice(const InputMessagePtr& msg) void ProtocolGame::parseUpdateSupplyTracker(const InputMessagePtr& msg) { const auto itemId = msg->getU16(); // item client ID + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; // Call the onSupplyTracker callback to expose the data to Lua - g_lua.callGlobalField("g_game", "onSupplyTracker", itemId); + g_lua.callGlobalField("g_game", "onSupplyTracker", itemId, resourceId); } void ProtocolGame::parseUpdateLootTracker(const InputMessagePtr& msg) @@ -4748,7 +4596,8 @@ void ProtocolGame::parseUpdateLootTracker(const InputMessagePtr& msg) void ProtocolGame::parseBestiaryEntryChanged(const InputMessagePtr& msg) { - msg->getU16(); // monster ID + // sets the cyclopedia button to open on this raceid next time it's clicked + msg->getU16(); // race ID // TODO: implement bestiary entry changed usage } @@ -4773,7 +4622,7 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) msg->getString(); // player vocation name msg->getU16(); // player level getOutfit(msg, false); - msg->getU8(); // ??? + msg->getU8(); // hide stamina bar (hidden if 0x01) if (g_game.getFeature(Otc::GameTournamentPackets)) { msg->getU8(); // ??? } @@ -4808,8 +4657,8 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) stats.capacity = msg->getU32(); stats.baseCapacity = msg->getU32(); stats.freeCapacity = msg->getU32(); - msg->getU8(); - msg->getU8(); + msg->getU8(); // number of skills to be displayed + msg->getU8(); // skill id: magic level stats.magicLevel = msg->getU16(); stats.baseMagicLevel = msg->getU16(); stats.loyaltyMagicLevel = msg->getU16(); @@ -4851,8 +4700,8 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) } } - const uint16_t skillLevel = msg->getU16(); - msg->getU16(); + const uint16_t skillLevel = msg->getU16(); // total skill + msg->getU16(); // base skill additionalSkillsArray.push_back({ skill, skillLevel }); } } @@ -4863,8 +4712,8 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) // forge skill stats const uint8_t lastSkill = g_game.getClientVersion() >= 1332 ? Otc::LastSkill : Otc::Momentum + 1; for (uint16_t skill = Otc::Fatal; skill < lastSkill; ++skill) { - const uint16_t skillLevel = msg->getU16(); - msg->getU16(); + const uint16_t skillLevel = msg->getU16(); // total skill + msg->getU16(); // base skill forgeSkillsArray.push_back({ skill, skillLevel }); } } @@ -4904,12 +4753,14 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) } const uint8_t concoctionsCount = msg->getU8(); - std::vector> concoctionsArray; + std::vector> concoctionsArray; + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); for (auto i = 0; i < concoctionsCount; ++i) { - const uint16_t concoctionFirst = msg->getU16(); - const uint16_t concoctionSecond = msg->getU16(); - concoctionsArray.emplace_back(concoctionFirst, concoctionSecond); + const uint16_t potionItemId = msg->getU16(); // item id + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const uint16_t potionDuration = msg->getU16(); // item duration [s] + concoctionsArray.emplace_back(potionItemId, potionDuration, resourceId); } g_game.processCyclopediaCharacterCombatStats(data, mitigation, additionalSkillsArray, forgeSkillsArray, perfectShotDamageRangesArray, combatsArray, concoctionsArray); @@ -4918,8 +4769,8 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) case Otc::CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS: { CyclopediaCharacterRecentDeaths data; - msg->getU16(); - msg->getU16(); + msg->getU16(); // current page + msg->getU16(); // page count const uint16_t entriesCount = msg->getU16(); for (auto i = 0; i < entriesCount; ++i) { @@ -4935,8 +4786,8 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) case Otc::CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS: { CyclopediaCharacterRecentPvPKills data; - msg->getU16(); - msg->getU16(); + msg->getU16(); // current page + msg->getU16(); // page count const uint16_t entriesCount = msg->getU16(); for (auto i = 0; i < entriesCount; ++i) { @@ -4952,17 +4803,36 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) } case Otc::CYCLOPEDIA_CHARACTERINFO_ACHIEVEMENTS: { + msg->getU16(); // achievement points + msg->getU16(); // secret achievements count + + const uint16_t count = msg->getU16(); // player achievements count + for (int i = 0; i < count; i++) { + msg->getU16(); // achievement id from staticdata (flagged as "secret" if not present in staticdata) + msg->getU32(); // timestamp - unlockedAt + + // bool: isSecret + if (uint8_t secret = msg->getU8(); secret > 0) { + msg->getString(); // achievement title + msg->getString(); // achievement description + msg->getU8(); // grade + } + } + break; } case Otc::CYCLOPEDIA_CHARACTERINFO_ITEMSUMMARY: { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + CyclopediaCharacterItemSummary data; const uint16_t inventoryItemsCount = msg->getU16(); for (auto i = 0; i < inventoryItemsCount; ++i) { ItemSummary item; const uint16_t itemId = msg->getU16(); - const auto& itemCreated = Item::create(itemId); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const auto& itemCreated = Item::create(itemId, resourceId); const uint16_t classification = itemCreated->getClassification(); uint8_t itemTier = 0; @@ -4980,7 +4850,8 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) for (auto i = 0; i < storeItemsCount; ++i) { ItemSummary item; const uint16_t itemId = msg->getU16(); - const auto& itemCreated = Item::create(itemId); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const auto& itemCreated = Item::create(itemId, resourceId); const uint16_t classification = itemCreated->getClassification(); uint8_t itemTier = 0; @@ -4998,8 +4869,9 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) for (auto i = 0; i < stashItemsCount; ++i) { ItemSummary item; const uint16_t itemId = msg->getU16(); - const auto& thing = g_things.getThingType(itemId, ThingCategoryItem); - if (!thing) { + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const auto& thing = g_things.getThingType(itemId, ThingCategoryItem, resourceId); + if (thing->getId() == 0) { continue; } const uint16_t classification = thing->getClassification(); @@ -5019,7 +4891,8 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) for (auto i = 0; i < depotItemsCount; ++i) { ItemSummary item; const uint16_t itemId = msg->getU16(); - const auto& itemCreated = Item::create(itemId); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const auto& itemCreated = Item::create(itemId, resourceId); const uint16_t classification = itemCreated->getClassification(); uint8_t itemTier = 0; @@ -5037,7 +4910,8 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) for (auto i = 0; i < inboxItemsCount; ++i) { ItemSummary item; const uint16_t itemId = msg->getU16(); - const auto& itemCreated = Item::create(itemId); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const auto& itemCreated = Item::create(itemId, resourceId); const uint16_t classification = itemCreated->getClassification(); uint8_t itemTier = 0; @@ -5056,12 +4930,15 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) } case Otc::CYCLOPEDIA_CHARACTERINFO_OUTFITSMOUNTS: { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + const uint16_t outfitsSize = msg->getU16(); std::vector outfits; for (auto i = 0; i < outfitsSize; ++i) { CharacterInfoOutfits outfit; outfit.lookType = msg->getU16(); + outfit.resourceId = multiSpr ? msg->getU16() : 0; outfit.name = msg->getString(); outfit.addons = msg->getU8(); outfit.type = msg->getU8(); // store / quest / none @@ -5083,6 +4960,7 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) for (auto i = 0; i < mountsSize; ++i) { CharacterInfoMounts mount; mount.mountId = msg->getU16(); + mount.resourceId = multiSpr ? msg->getU16() : 0; mount.name = msg->getString(); mount.type = msg->getU8(); // store / quest / none mount.isCurrent = msg->getU32(); // 1000 = true / 0 = false @@ -5102,6 +4980,7 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) for (auto i = 0; i < familiarsSize; ++i) { CharacterInfoFamiliar familiar; familiar.lookType = msg->getU16(); + familiar.resourceId = multiSpr ? msg->getU16() : 0; familiar.name = msg->getString(); familiar.type = msg->getU8(); // quest / none familiar.isCurrent = msg->getU32(); // 1000 = true / 0 = false @@ -5155,6 +5034,34 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) } case Otc::CYCLOPEDIA_CHARACTERINFO_INSPECTION: { + // based on protocol 13.20, the structure may differ in other versions + const uint8_t inventorySize = msg->getU8(); + for (int inventorySlot = 0; inventorySlot < inventorySize; ++inventorySlot) { + msg->getU8(); // slotId (CONST_SLOT_...) + msg->getString(); // item name + getItem(msg); + + msg->getU8(); // imbuing slots + // likely contains imbuement details + + // item information (key-value pairs) + const uint8_t inspectAttrCount = msg->getU8(); + for (int attr = 0; attr < inspectAttrCount; ++attr) { + msg->getString(); // key + msg->getString(); // value + } + } + + msg->getString(); // name of inspected player + getOutfit(msg, false); // outfit without mount/other things + + // player information (key-value pairs) + const uint8_t inspectAttrCount = msg->getU8(); + for (int attr = 0; attr < inspectAttrCount; ++attr) { + msg->getString(); // key + msg->getString(); // value + } + break; } case Otc::CYCLOPEDIA_CHARACTERINFO_BADGES: @@ -5260,6 +5167,7 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg) data.weaponAccuracy.push_back(msg->getDouble()); } + // wheel of destiny (?) if (g_game.getClientVersion() >= 1510) { msg->getDouble(); // unused msg->getU16(); // unused @@ -5411,6 +5319,8 @@ void ProtocolGame::parseOpenRewardWall(const InputMessagePtr& msg) namespace { DailyRewardDay parseRewardDay(const InputMessagePtr& msg) { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + DailyRewardDay day; day.redeemMode = msg->getU8(); // reward type day.itemsToSelect = 0; // reward type @@ -5421,6 +5331,9 @@ namespace { for (auto listIndex = 0; listIndex < itemListSize; ++listIndex) { DailyRewardItem item; item.itemId = msg->getU16(); // Item ID + if (multiSpr) { + item.resourceId = msg->getU16(); + } item.name = msg->getString(); // Item name item.weight = msg->getU32(); // Item weight day.selectableItems.emplace_back(item); @@ -5437,6 +5350,9 @@ namespace { case 1: { // Items bundle.itemId = msg->getU16(); // Item ID + if (multiSpr) { + bundle.resourceId = msg->getU16(); + } bundle.name = msg->getString(); // Item name bundle.count = msg->getU8(); // Item Count break; @@ -5721,11 +5637,13 @@ Imbuement ProtocolGame::getImbuementInfo(const InputMessagePtr& msg) } const uint8_t itemsSize = msg->getU8(); + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); for (auto i = 0; std::cmp_less(i, itemsSize); ++i) { const uint16_t itemId = msg->getU16(); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; const auto& itemName = msg->getString(); const uint16_t itemCount = msg->getU16(); - const auto& item = Item::create(itemId); + const auto& item = Item::create(itemId, resourceId); item->setCount(itemCount); imbuement.sources.emplace_back(item, itemName); } @@ -5747,78 +5665,65 @@ void ProtocolGame::parseImbuementWindow(const InputMessagePtr& msg) { uint8_t windowType = Otc::IMBUEMENT_WINDOW_SELECT_ITEM; if (g_game.getClientVersion() >= 1510) { - windowType = static_cast(msg->getU8()); // window type + // 0 = Choice, 1 = Select Item, 2 = Scroll + windowType = static_cast(msg->getU8()); msg->getU8(); // unknown byte } - switch (windowType) { - case Otc::IMBUEMENT_WINDOW_CHOICE: { - const uint16_t itemId = msg->getU16(); // item client ID - const uint32_t unknown = msg->getU32(); // unknown - g_lua.callGlobalField("g_game", "onOpenImbuementWindow", itemId, unknown); - break; - } - case Otc::IMBUEMENT_WINDOW_SCROLL: { - msg->getU8(); // unknown byte - msg->getU8(); // unknown byte - const uint16_t imbuementsSize = msg->getU16(); - std::vector imbuements; - for (auto i = 0; i < imbuementsSize; ++i) { - imbuements.push_back(getImbuementInfo(msg)); - } - const uint32_t neededItemsListCount = msg->getU32(); - std::vector neededItemsList; - neededItemsList.reserve(neededItemsListCount); - for (uint32_t i = 0; i < neededItemsListCount; ++i) { - const uint16_t needItemId = msg->getU16(); - const uint16_t count = msg->getU16(); - const auto& needItem = Item::create(needItemId); - needItem->setCount(count); - neededItemsList.push_back(needItem); - } + if (windowType >= Otc::IMBUEMENT_WINDOW_LAST) { + g_logger.error("ProtocolGame::parseImbuementWindow: Unsupported window type"); + return; + } else if (windowType == Otc::IMBUEMENT_WINDOW_CHOICE) { + const uint16_t itemId = msg->getU16(); + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + const uint32_t unknown = msg->getU32(); // new imbuement duration? - g_lua.callGlobalField("g_game", "onImbuementScroll", imbuements, neededItemsList); - break; - } - case Otc::IMBUEMENT_WINDOW_SELECT_ITEM: { - const uint16_t itemId = msg->getU16(); // item client ID - const auto& thing = g_things.getThingType(itemId, ThingCategoryItem); - if (thing) { - const uint16_t classification = thing->getClassification(); - if (classification > 0) { - msg->getU8(); // upgradeClass - } - } - const uint8_t slot = msg->getU8(); - std::unordered_map> activeSlots; - for (auto i = 0; i < slot; i++) { - const uint8_t firstByte = msg->getU8(); - if (firstByte == 0x01) { - Imbuement imbuement = getImbuementInfo(msg); - const uint32_t duration = msg->getU32(); - const uint32_t removalCost = msg->getU32(); - activeSlots[i] = std::make_tuple(imbuement, duration, removalCost); - } - } - const uint16_t imbuementsSize = msg->getU16(); - std::vector imbuements; - for (auto i = 0; i < imbuementsSize; ++i) { - imbuements.push_back(getImbuementInfo(msg)); - } - const uint32_t neededItemsListCount = msg->getU32(); - std::vector neededItemsList; - neededItemsList.reserve(neededItemsListCount); - for (uint32_t i = 0; i < neededItemsListCount; ++i) { - const uint16_t needItemId = msg->getU16(); - const uint16_t count = msg->getU16(); - const auto& needItem = Item::create(needItemId); - needItem->setCount(count); - neededItemsList.push_back(needItem); - } - g_lua.callGlobalField("g_game", "onImbuementWindow", itemId, slot, activeSlots, imbuements, neededItemsList); - break; + g_lua.callGlobalField("g_game", "onOpenImbuementWindow", itemId, unknown, resourceId); + return; + } + + std::vector imbuements; + std::vector neededItemsList; + + if (windowType == Otc::IMBUEMENT_WINDOW_SCROLL) { + msg->getU16(); // scroll clientid? + + getImbuingIngredients(msg, imbuements, neededItemsList); + g_lua.callGlobalField("g_game", "onImbuementScroll", imbuements, neededItemsList); + return; + } + + const uint16_t itemId = msg->getU16(); + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + const auto& item = Item::create(itemId, resourceId); + if (!item) { + throw Exception("ProtocolGame::parseImbuementWindow: unable to create item with invalid id {}", itemId); + } + if (item->getId() == 0) { + throw Exception("ProtocolGame::parseImbuementWindow: unable to create item with invalid id {}", itemId); + } + + if (item->getClassification() > 0) { + msg->getU8(); // tier + } + + // imbuing slots + const uint8_t imbuingSlotCount = msg->getU8(); + std::unordered_map> activeSlots; + + for (auto i = 0; std::cmp_less(i, imbuingSlotCount); i++) { + const uint8_t firstByte = msg->getU8(); + if (firstByte == 0x01) { + Imbuement imbuement = getImbuementInfo(msg); + const uint32_t duration = msg->getU32(); + const uint32_t removalCost = msg->getU32(); + activeSlots[i] = std::make_tuple(imbuement, duration, removalCost); } } + + getImbuingIngredients(msg, imbuements, neededItemsList); + + g_lua.callGlobalField("g_game", "onImbuementItem", itemId, imbuingSlotCount, activeSlots, imbuements, neededItemsList); } void ProtocolGame::parseCloseImbuementWindow(const InputMessagePtr& /*msg*/) @@ -5833,14 +5738,14 @@ void ProtocolGame::parseError(const InputMessagePtr& msg) g_lua.callGlobalField("g_game", "onServerError", code, error); } -static uint8_t readMarketItemTier(const InputMessagePtr& msg, uint16_t itemId, int clientVersion) +static uint8_t readMarketItemTier(const InputMessagePtr& msg, uint16_t itemId, uint16_t resourceId, int clientVersion) { if (clientVersion < 1281) { return 0; } - const auto& thing = g_things.getThingType(itemId, ThingCategoryItem); - if (!thing) { + const auto& thing = g_things.getThingType(itemId, ThingCategoryItem, resourceId); + if (thing->getId() == 0) { return 0; } @@ -5857,12 +5762,14 @@ void ProtocolGame::parseMarketEnter(const InputMessagePtr& msg) const uint16_t itemsSentCount = msg->getU16(); std::vector> depotItems; - + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + const int version = g_game.getClientVersion(); for (auto i = 0; i < itemsSentCount; ++i) { const uint16_t itemId = msg->getU16(); - const uint8_t itemTier = readMarketItemTier(msg, itemId, g_game.getClientVersion()); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const uint8_t itemTier = readMarketItemTier(msg, itemId, resourceId, version); const uint16_t count = msg->getU16(); - depotItems.push_back({ itemId, itemTier, count }); + depotItems.push_back({ itemId, itemTier, count, resourceId }); } g_lua.callGlobalField("g_game", "onMarketEnter", depotItems, offers, -1, -1); @@ -5870,8 +5777,10 @@ void ProtocolGame::parseMarketEnter(const InputMessagePtr& msg) void ProtocolGame::parseMarketEnterOld(const InputMessagePtr& msg) { - const uint64_t balance = g_game.getClientVersion() >= 981 ? msg->getU64() : msg->getU32(); - const uint8_t vocation = g_game.getClientVersion() < 950 ? msg->getU8() : g_game.getLocalPlayer()->getVocation(); + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + const int version = g_game.getClientVersion(); + const uint64_t balance = version >= 981 ? msg->getU64() : msg->getU32(); + const uint8_t vocation = version < 950 ? msg->getU8() : g_game.getLocalPlayer()->getVocation(); const uint8_t offers = msg->getU8(); const uint16_t itemsSent = msg->getU16(); @@ -5879,8 +5788,9 @@ void ProtocolGame::parseMarketEnterOld(const InputMessagePtr& msg) std::vector> depotItems; for (auto i = 0; i < itemsSent; ++i) { const uint16_t itemId = msg->getU16(); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; const uint16_t count = msg->getU16(); - depotItems.push_back({ itemId, count }); + depotItems.push_back({ itemId, 0, count, resourceId }); } g_lua.callGlobalField("g_game", "onMarketEnter", depotItems, offers, balance, vocation); @@ -5986,11 +5896,12 @@ static std::vector> readMarketStatsList( void ProtocolGame::parseMarketDetail(const InputMessagePtr& msg) { const uint16_t itemId = msg->getU16(); + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; const int clientVersion = g_game.getClientVersion(); const bool pricesAreU64 = (clientVersion >= 1281); - const uint8_t itemTier = readMarketItemTier(msg, itemId, clientVersion); + const uint8_t itemTier = readMarketItemTier(msg, itemId, resourceId, clientVersion); auto descriptions = readMarketDescriptions(msg, clientVersion); const uint32_t timeThing = (time(nullptr) / 1000) * 86400; @@ -6006,12 +5917,16 @@ MarketOffer ProtocolGame::readMarketOffer(const InputMessagePtr& msg, const uint const uint32_t timestamp = msg->getU32(); const uint16_t counter = msg->getU16(); uint16_t itemId = 0; + uint16_t resourceId = 0; uint8_t itemTier = 0; const int clientVersion = g_game.getClientVersion(); if (var == Otc::OLD_MARKETREQUEST_MY_OFFERS || var == Otc::MARKETREQUEST_OWN_OFFERS || var == Otc::OLD_MARKETREQUEST_MY_HISTORY || var == Otc::MARKETREQUEST_OWN_HISTORY) { itemId = msg->getU16(); - itemTier = readMarketItemTier(msg, itemId, clientVersion); + if (g_game.getFeature(Otc::GameMultiSpr)) { + resourceId = msg->getU16(); + } + itemTier = readMarketItemTier(msg, itemId, resourceId, clientVersion); } else { itemId = var; } @@ -6041,8 +5956,9 @@ void ProtocolGame::parseMarketBrowse(const InputMessagePtr& msg) var = msg->getU8(); if (var == 3) { var = msg->getU16(); // itemId - const auto& thing = g_things.getThingType(var, ThingCategoryItem); - if (thing) { + const uint16_t resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + const auto& thing = g_things.getThingType(var, ThingCategoryItem, resourceId); + if (thing->getId() != 0) { const uint16_t classification = thing->getClassification(); if (classification > 0) { itemTier = msg->getU8(); @@ -6109,41 +6025,21 @@ void ProtocolGame::parseBosstiarySlots(const InputMessagePtr& msg) { BosstiarySlotsData data; - auto getBosstiarySlot = [&msg]() -> BosstiarySlot { - BosstiarySlot slot; - slot.bossRace = msg->getU8(); - slot.killCount = msg->getU32(); - slot.lootBonus = msg->getU16(); - slot.killBonus = msg->getU8(); - slot.bossRaceRepeat = msg->getU8(); - slot.removePrice = msg->getU32(); - slot.inactive = msg->getU8(); - return slot; - }; + data.playerPoints = msg->getU32(); // current boss points + data.totalPointsNextBonus = msg->getU32(); // points for next bonus + data.currentBonus = msg->getU16(); // loot bonus [%] + data.nextBonus = msg->getU16(); // next loot bonus [%] - data.playerPoints = msg->getU32(); - data.totalPointsNextBonus = msg->getU32(); - data.currentBonus = msg->getU16(); - data.nextBonus = msg->getU16(); + // left slot + getBosstiarySlot(msg, data.isSlotOneUnlocked, data.bossIdSlotOne, data.slotOneData); - data.isSlotOneUnlocked = msg->getU8(); - data.bossIdSlotOne = msg->getU32(); - if (data.isSlotOneUnlocked && data.bossIdSlotOne != 0) { - data.slotOneData = getBosstiarySlot(); - } + // right slot + getBosstiarySlot(msg, data.isSlotTwoUnlocked, data.bossIdSlotTwo, data.slotTwoData); - data.isSlotTwoUnlocked = msg->getU8(); - data.bossIdSlotTwo = msg->getU32(); - if (data.isSlotTwoUnlocked && data.bossIdSlotTwo != 0) { - data.slotTwoData = getBosstiarySlot(); - } - - data.isTodaySlotUnlocked = msg->getU8(); - data.boostedBossId = msg->getU32(); - if (data.isTodaySlotUnlocked && data.boostedBossId != 0) { - data.todaySlotData = getBosstiarySlot(); - } + // middle slot + getBosstiarySlot(msg, data.isTodaySlotUnlocked, data.boostedBossId, data.todaySlotData); + // bosstiary: selectable bosses data.bossesUnlocked = msg->getU8(); if (data.bossesUnlocked) { const uint16_t bossesUnlockedSize = msg->getU16(); @@ -6176,13 +6072,104 @@ void ProtocolGame::parseBosstiaryCooldownTimer(const InputMessagePtr& msg) void ProtocolGame::parseBosstiaryEntryChanged(const InputMessagePtr& msg) { + // does nothing on client 13.20 + // might be setting cyclopedia to open on selected boss when on higher protocols msg->getU32(); // bossId } void ProtocolGame::parseTakeScreenshot(const InputMessagePtr& msg) { - const uint8_t screenshotType = msg->getU8(); - m_localPlayer->takeScreenshot(screenshotType); + const uint8_t eventType = msg->getU8(); + + // old version: take screenshot + if (g_game.getClientVersion() < 1520) { + m_localPlayer->takeScreenshot(eventType); + return; + } + + /* + new version: game event triggered + this may require verifying, was added somewhere between 15.00 and 15.20 + event types: + 0 / other numbers - takes no bytes, nothing happens + 1 - screenshot / popup + 2 - achievement earned (takes string) + 3 - title unlocked (takes string) + 4 - level up (takes u16, max value 65535) + 5 - bosstiary / skill up (u8 skill id, u16 value) + 6 - bestiary race progess (u16 raceId, u8 progress level) + 7 - bosstiary race progess (u16 raceId, u8 progress level) + 8 - quest progress (string questName, u8: 0 - started, 1 - completed) + 9 - cosmetic unlocked: + - u16 lookType + - string cosmeticName + - u8 cosmeticType: + - 0 - outfit + - 1 - addon_1 + - 2 - addon_2 + - 3 - mount + - any other number - outfit unlocked + 10 - weapon proficiency (u16 item clientId, string itemName) + */ + + switch (eventType) { + case 1: + // screenshot / popup + simpleEvent1520(msg->getU8()); + return; + case 2: + case 3: + // 2 - achievement unlocked + // 3 - title unlocked + msg->getString(); + return; + case 4: + // level up + msg->getU16(); + return; + case 5: + // skill up + // 0 - vanilla client: bugged "bosstiary entry discovered", second argument ignored + // 1-8 - skill discovered (magic, sword, club, axe, fist, dist, shield, fishing) + msg->getU8(); + + // new skill level + msg->getU16(); + return; + case 6: + case 7: + // bestiary / bosstiary progress + msg->getU16(); // raceId + msg->getU8(); // progress level + return; + case 8: + // quest progress + msg->getString(); // quest name + msg->getU8(); // 0 - updated, 1 - completed + return; + case 9: + // cosmetic unlocked + msg->getU16(); // lookType + if (g_game.getFeature(Otc::GameMultiSpr)) + msg->getU16(); // resourceId + + msg->getString(); // cosmetic name + + // type: 0 - outfit, 1 and 2 - addon, 3 - mount, 4-255 - defaults to "outfit unlocked" + // vanilla client does not support sending the full outfit + msg->getU8(); + return; + case 10: + // weapon proficiency progress + msg->getU16(); // item id + if (g_game.getFeature(Otc::GameMultiSpr)) + msg->getU16(); // resourceId + + msg->getString(); // message + return; + default: + break; + } } void ProtocolGame::parseAttachedEffect(const InputMessagePtr& msg) @@ -6336,6 +6323,9 @@ void ProtocolGame::parseHighscores(const InputMessagePtr& msg) void ProtocolGame::parseWeaponProficiencyExperience(const InputMessagePtr& msg) { msg->getU16(); // itemId + if (g_game.getFeature(Otc::GameMultiSpr)) + msg->getU16(); // resourceId + msg->getU32(); // Experience msg->getU8(); // 1 } @@ -6343,6 +6333,9 @@ void ProtocolGame::parseWeaponProficiencyExperience(const InputMessagePtr& msg) void ProtocolGame::parseWeaponProficiencyInfo(const InputMessagePtr& msg) { msg->getU16(); // itemId + if (g_game.getFeature(Otc::GameMultiSpr)) + msg->getU16(); // resourceId + msg->getU32(); // experience const uint8_t size = msg->getU8(); @@ -6421,3 +6414,581 @@ PaperdollPtr ProtocolGame::getPaperdoll(const InputMessagePtr& msg) const { return paperdoll; } + +void ProtocolGame::internalGetCreature(const InputMessagePtr& msg, CreaturePtr& creature, const bool known) const +{ + uint32_t creatureId; + creatureFromPacket(msg, creature, creatureId, known); + + const uint8_t healthPercent = msg->getU8(); + const auto direction = static_cast(msg->getU8()); + const auto& outfit = getOutfit(msg); + + Light light; + light.intensity = msg->getU8(); + light.color = msg->getU8(); + + const uint16_t speed = msg->getU16(); + + // ui icons (skull, emblem, quest icons, npc icons, etc) + // NOTE: creatureId is passed here separately because it's in the packet but the creaure may be null + setCreatureIcons(msg, creature, creatureId, known); + + int version = g_game.getClientVersion(); + + // open pvp "aggression" frames + if (g_game.getFeature(Otc::GameThingMarks)) { + const uint8_t frameColor = msg->getU8(); + + if (creature) { + if (frameColor == 0xff) { + // creature does not have any frame + creature->hideStaticSquare(); + } else { + // display frame with color + creature->showStaticSquare(Color::from8bit(frameColor)); + } + } + + // lightning icon indicating multiple guild members online + if (version < 1281) { + msg->getU16(); + } + } + + // permissions for inspect player feature + if (version >= 1281) { + msg->getU8(); // inspection type + } + + // walkthrough + bool unpass = false; + if (version >= 854) { + unpass = static_cast(msg->getU8()); + } + + // aura, wings, shader, paperdoll + setExtendedCosmetics(msg, creature); + + if (creature) { + creature->setHealthPercent(healthPercent); + creature->turn(direction); + creature->setOutfit(outfit); + creature->setSpeed(speed); + creature->setPassable(!unpass); + creature->setLight(light); + if (creature == m_localPlayer && !m_localPlayer->isKnown()) { + m_localPlayer->setKnown(true); + } + } +} + +void ProtocolGame::creatureFromPacket(const InputMessagePtr& msg, CreaturePtr& creature, uint32_t& creatureId, const bool known) const +{ + if (known) { + creatureId = msg->getU32(); + creature = g_map.getCreatureById(creatureId); + if (!creature) { + g_logger.traceError("ProtocolGame::getCreature: server said that a creature is known, but it's not"); + } + + return; + } + + const uint32_t removeId = msg->getU32(); + creatureId = msg->getU32(); + + if (creatureId == removeId) { + creature = g_map.getCreatureById(creatureId); + } else { + g_map.removeCreatureById(removeId); + } + + uint8_t creatureType; + if (g_game.getClientVersion() >= 910) { + creatureType = msg->getU8(); + } else { + if (creatureId >= Proto::PlayerStartId && creatureId < Proto::PlayerEndId) + creatureType = Proto::CreatureTypePlayer; + else if (creatureId >= Proto::MonsterStartId && creatureId < Proto::MonsterEndId) + creatureType = Proto::CreatureTypeMonster; + else + creatureType = Proto::CreatureTypeNpc; + } + + uint32_t masterId = 0; + if (g_game.getClientVersion() >= 1281 && creatureType == Proto::CreatureTypeSummonOwn) { + masterId = msg->getU32(); + if (m_localPlayer->getId() != masterId) { + creatureType = Proto::CreatureTypeSummonOther; + } + } + + const auto& name = g_game.formatCreatureName(msg->getString()); + + if (!creature) { + if ((creatureId == m_localPlayer->getId()) || + // fixes a bug server side bug where GameInit is not sent and local player id is unknown + (creatureType == Proto::CreatureTypePlayer && !m_localPlayer->getId() && name == m_localPlayer->getName())) { + creature = m_localPlayer; + } else { + makeCreature(creature, creatureType); + } + } + + if (creature) { + creature->setId(creatureId); + creature->setName(name); + creature->setMasterId(masterId); + + g_map.addCreature(creature); + } +} + +void ProtocolGame::makeCreature(CreaturePtr& creature, uint8_t creatureType) const +{ + switch (creatureType) { + case Proto::CreatureTypePlayer: + creature = std::make_shared(); + break; + + case Proto::CreatureTypeNpc: + creature = std::make_shared(); + break; + + case Proto::CreatureTypeHidden: + case Proto::CreatureTypeMonster: + case Proto::CreatureTypeSummonOwn: + case Proto::CreatureTypeSummonOther: + creature = std::make_shared(); + break; + + default: + g_logger.traceError("ProtocolGame::getCreature: creature type is invalid"); + } + + if (creature) { + creature->onCreate(); + } +} + +void ProtocolGame::getImbuingIngredients(const InputMessagePtr& msg, std::vector& imbuements, std::vector& neededItemsList) +{ + const uint16_t imbuementsSize = msg->getU16(); + for (auto i = 0; i < imbuementsSize; ++i) { + imbuements.push_back(getImbuementInfo(msg)); + } + + const uint32_t neededItemsListCount = msg->getU32(); + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + neededItemsList.reserve(neededItemsListCount); + for (uint32_t i = 0; i < neededItemsListCount; ++i) { + const uint16_t needItemId = msg->getU16(); + const uint16_t resourceId = multiSpr ? msg->getU16() : 0; + const uint16_t count = msg->getU16(); + const auto& needItem = Item::create(needItemId, resourceId); + needItem->setCount(count); + neededItemsList.push_back(needItem); + } +} + +void ProtocolGame::setExtendedCosmetics(const InputMessagePtr& msg, const CreaturePtr& creature) const +{ + 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(); + } + + std::vector attachedEffectList; + if (g_game.getFeature(Otc::GameCreatureAttachedEffect)) { + const uint8_t listSize = msg->getU8(); + for (auto i = -1; ++i < listSize;) { + attachedEffectList.push_back(msg->getU16()); + } + } + + if (!creature) { + return; + } + + creature->setShader(shader); + creature->clearTemporaryAttachedEffects(); + std::unordered_set currentAttachedEffectIds; + for (const auto& attachedEffect : creature->getAttachedEffects()) { + currentAttachedEffectIds.insert(attachedEffect->getId()); + } + + for (const auto effectId : attachedEffectList) { + const auto& effect = g_attachedEffects.getById(effectId); + if (effect && currentAttachedEffectIds.find(effectId) == currentAttachedEffectIds.end()) { + const auto& clonedEffect = effect->clone(); + clonedEffect->setPermanent(false); + creature->attachEffect(clonedEffect); + } + } +} + +void ProtocolGame::setCreatureIcons(const InputMessagePtr& msg, const CreaturePtr& creature, const uint32_t creatureId, const bool known) const +{ + // creature icons (quest, fiendish, weakened, etc) + int version = g_game.getClientVersion(); + if (version >= 1281) { + addCreatureIcon(msg, creatureId); + } + + const uint8_t skull = msg->getU8(); + const uint8_t partyShield = msg->getU8(); + + // emblem is sent only when the creature is not known + uint8_t guildEmblem = 0; + uint8_t creatureType = 0; + uint8_t npcIcon = 0; + bool unpass = true; + + if (g_game.getFeature(Otc::GameCreatureEmblems) && !known) { + guildEmblem = msg->getU8(); + } + + if (g_game.getFeature(Otc::GameThingMarks)) { + creatureType = msg->getU8(); + } + + uint32_t masterId = 0; + uint8_t vocationId = 0; + if (version >= 1281) { + if (creatureType == Proto::CreatureTypeSummonOwn) { + masterId = msg->getU32(); + if (m_localPlayer->getId() != masterId) { + creatureType = Proto::CreatureTypeSummonOther; + } + } else if (creatureType == Proto::CreatureTypePlayer) { + vocationId = msg->getU8(); + } + } + + // npc icon + if (g_game.getFeature(Otc::GameCreatureIcons)) { + npcIcon = msg->getU8(); + } + + if (creature) { + creature->setSkull(skull); + creature->setShield(partyShield); + creature->setMasterId(masterId); + creature->setVocation(vocationId); + + if (guildEmblem > 0) { + creature->setEmblem(guildEmblem); + } + + if (creatureType > 0) { + creature->setType(creatureType); + } + + if (npcIcon > 0) { + creature->setIcon(npcIcon); + } + } +} + +ForgeItemInfo ProtocolGame::getForgeItem(const InputMessagePtr& msg, const bool multiSpr, const bool skipTier) +{ + ForgeItemInfo item; + item.id = msg->getU16(); + if (multiSpr) { + item.resourceId = msg->getU16(); + } + if (!skipTier) { + item.tier = msg->getU8(); + } + item.count = msg->getU16(); + return item; +} + +void ProtocolGame::getForgeTransfers(const InputMessagePtr& msg, std::vector& transfers, const bool multiSpr) +{ + const uint8_t transferTotalCount = msg->getU8(); + transfers.reserve(transferTotalCount); + for (auto i = 0; i < transferTotalCount; ++i) { + ForgeTransferData transfer; + const uint16_t donorCount = msg->getU16(); + transfer.donors.reserve(donorCount); + for (auto j = 0; j < donorCount; ++j) { + transfer.donors.emplace_back(getForgeItem(msg, multiSpr)); + } + const uint16_t receiverCount = msg->getU16(); + transfer.receivers.reserve(receiverCount); + for (auto j = 0; j < receiverCount; ++j) { + transfer.receivers.emplace_back(getForgeItem(msg, multiSpr, true)); + } + transfers.emplace_back(transfer); + } +} + +void ProtocolGame::getBosstiarySlot(const InputMessagePtr& msg, bool& unlocked, uint32_t& bossId, std::optional& slotInfo) +{ + unlocked = msg->getU8(); + bossId = msg->getU32(); + if (!unlocked || bossId == 0) { + return; + } + + slotInfo.emplace(); + auto& slot = *slotInfo; + slot.bossRace = msg->getU8(); + slot.killCount = msg->getU32(); + slot.lootBonus = msg->getU16(); + slot.killBonus = msg->getU8(); + slot.bossRaceRepeat = msg->getU8(); + slot.removePrice = msg->getU32(); + slot.inactive = msg->getU8(); +} + +StoreOffer ProtocolGame::getStoreOffer(const InputMessagePtr& msg) +{ + StoreOffer offer; + + // offer name + offer.name = msg->getString(); + + // offer variants + // example use case: 1 potion -> 5 tc, 5 potions -> 25tc + const uint8_t subOffersCount = msg->getU8(); + for (auto j = 0; j < subOffersCount; ++j) { + offer.subOffers.push_back(getStoreSubOffer(msg)); + } + + // picture of the offer (image, item, outfit) + getStoreOfferImage(msg, offer); + + // try on mode + // 0 - unavailable, 1 - mount, 2 - outfit + offer.tryOnType = msg->getU8(); + + // parent name (menu or dropdown menu) + // to do: offer.collection as a string + msg->getString(); + + offer.popularityScore = msg->getU16(); + offer.stateNewUntil = msg->getU32(); // published at + offer.configurable = msg->getU8() != 0; // show "configure" button instead of "buy" + + // bundle size (eg. furniture set, beds) + offer.productsCapacity = msg->getU16(); + for (auto j = 0; j < offer.productsCapacity; ++j) { + getStorePackageItem(msg); + } + + return offer; +} + +void ProtocolGame::getStoreOfferImage(const InputMessagePtr& msg, StoreOffer& offer) +{ + //offer type - default (image), mount, outfit, item, hireling + offer.type = msg->getU8(); + if (offer.type == Otc::GameStoreInfoType_t::SHOW_NONE) { + offer.icon = msg->getString(); // offer image + } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_MOUNT) { + offer.mountId = msg->getU16(); + offer.resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_ITEM) { + offer.itemId = msg->getU16(); + offer.resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_OUTFIT) { + offer.outfitId = msg->getU16(); + offer.resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + offer.outfitHead = msg->getU8(); + offer.outfitBody = msg->getU8(); + offer.outfitLegs = msg->getU8(); + offer.outfitFeet = msg->getU8(); + } else if (offer.type == Otc::GameStoreInfoType_t::SHOW_HIRELING) { + offer.sex = msg->getU8(); // current selection - 1 - male, 2 - female + offer.maleOutfitId = msg->getU16(); + offer.femaleOutfitId = msg->getU16(); + offer.resourceId = g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; + offer.outfitHead = msg->getU8(); + offer.outfitBody = msg->getU8(); + offer.outfitLegs = msg->getU8(); + offer.outfitFeet = msg->getU8(); + } else { + // 13.20: vanilla client falls back to reading a string when invalid enum is provided + offer.icon = msg->getString(); // offer image + } +} + +SubOffer ProtocolGame::getStoreSubOffer(const InputMessagePtr& msg) +{ + SubOffer subOffer{}; + subOffer.id = msg->getU32(); + subOffer.count = msg->getU16(); + subOffer.price = msg->getU32(); + subOffer.coinType = msg->getU8(); + + // offer error codes + // the list of codes is sent in ProtocolGame::parseStoreOffers + subOffer.disabled = msg->getU8() != 0; + if (subOffer.disabled) { + const uint8_t reasonCount = msg->getU8(); + for (auto reason = 0; reason < reasonCount; ++reason) { + if (g_game.getClientVersion() >= 1300) { + subOffer.reasonIdDisable = msg->getU16(); + } else { + msg->getString(); + } + } + } + + // 0 - normal, 1 - new, 2 - discount, 3 - limited time offer + subOffer.state = msg->getU8(); + if (subOffer.state == Otc::GameStoreInfoStatesType_t::STATE_SALE) { + subOffer.validUntil = msg->getU32(); // valid until - unix timestamp in seconds + subOffer.basePrice = msg->getU32(); // price before discount (must be higher than subOffer.price) + } + + return subOffer; +} + +void ProtocolGame::getStorePackageItem(const InputMessagePtr& msg) +{ + // "package contains" list item + + // item name + msg->getString(); + + // item image + const uint8_t offerType = msg->getU8(); // offer type + switch (offerType) { + case 0: + // web image + msg->getString(); // offer image + break; + case 1: + case 3: + // item + msg->getU16(); // item clientId + g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; // resourceId + break; + case 2: + // full outfit + msg->getU16(); // lookType + g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; // resourceId + msg->getU8(); // head + msg->getU8(); // body + msg->getU8(); // legs + msg->getU8(); // feet + break; + case 4: + // full outfit (hireling-style packet) + msg->getU8(); // supported enums: 1 - male, 2 - female + msg->getU16(); // male outfit id + msg->getU16(); // female outfit id + g_game.getFeature(Otc::GameMultiSpr) ? msg->getU16() : 0; // resourceId + msg->getU8(); // head + msg->getU8(); // body + msg->getU8(); // legs + msg->getU8(); // feet + break; + default: + // vanilla client throws invalid enum + g_logger.warning("gamestore: invalid offer package item image type {}", static_cast(offerType)); + break; + } +} + +OutfitWindowThing ProtocolGame::getOutfitWindowThing(const InputMessagePtr& msg, const bool addons, const bool multiSpr, const bool thingCategories) const +{ + OutfitWindowThing o; + o.id = msg->getU16(); + if (multiSpr) { + o.resourceId = msg->getU16(); + } + + o.name = msg->getString(); + + if (addons) { + o.addons = msg->getU8(); + } + + if (thingCategories) { + o.category = static_cast(msg->getU8()); + } + + if (g_game.getClientVersion() >= 1281) { + // 0 - ok, 1 - store (u32 offerId), 2 - golden outfit tooltip, 3 - crown set outfit tooltip + o.lockReason = msg->getU8(); + if (o.lockReason == 1) { + o.offerId = msg->getU32(); + } + } + return o; +} + +void ProtocolGame::getOutfitWindowCosmeticsList(const InputMessagePtr& msg, std::vector& thingList, const bool listInU16, const bool addons, const bool multiSpr, const bool thingCategories) const +{ + const uint16_t thingCount = listInU16 ? msg->getU16() : msg->getU8(); + thingList.reserve(thingCount); + for (auto i = 0; i < thingCount; ++i) { + thingList.emplace_back(getOutfitWindowThing(msg, addons, multiSpr, thingCategories)); + } +} + +void ProtocolGame::simpleEvent1520(uint8_t eventId) +{ + /* + in 1520 the events changed their ids + new enums translation (server -> otc): + + screenshot events: + 0 - crash with no error -> 0 + 1 - bossDefeated -> 4 + 2 - nothing -> 5 (death pve) + 3 - nothing -> 6 (death pvp) + 4 - pk assist -> 8 + 5 - playerkill -> 9 + 6 - playerAttacking -> 10 + 7 - treasureFound -> 11 + 8 - giftOfLife -> 13 + + popups: + 9 - attack stopped -> 14 + 10 - capacity limit -> 15 + 11 - out of ammo -> 16 + 12 - target too close -> 17 + 13 - out of soul points -> 18 + 14 - (new tutorial finish popup) -> 19 + popup text: Off to new shores - leave the village and set sail to start your real adventure + */ + + // no translation for event 0 + if (eventId == 0) + return; + + // first 3 events were moved to a different type + else if (eventId < 4) + eventId += 3; + + // level up was moved to a different type + else if (eventId >= 4 && eventId < 8) + eventId += 4; + + // skillup was moved to a different type + else + eventId += 5; + + // screenshot events: + if (eventId < 13) { + m_localPlayer->takeScreenshot(eventId); + return; + } + + // popup events: to do +} diff --git a/src/client/protocolgamesend.cpp b/src/client/protocolgamesend.cpp index 739248a91c..a550479a6c 100644 --- a/src/client/protocolgamesend.cpp +++ b/src/client/protocolgamesend.cpp @@ -80,7 +80,9 @@ void ProtocolGame::sendLoginPacket(const uint32_t challengeTimestamp, const uint msg->addU32(m_xteaKey[3]); } - msg->addU8(0); // is gm set? + // gamemaster flag + // old clients were sending "1" when launched with --gamemaster argument + msg->addU8(0); if (g_game.getFeature(Otc::GameSessionKey)) { msg->addString(m_sessionKey); @@ -300,20 +302,28 @@ void ProtocolGame::sendGmTeleport(const Position& pos) send(msg); } -void ProtocolGame::sendEquipItemWithTier(const uint16_t itemId, const uint8_t tier) +void ProtocolGame::sendEquipItemWithTier(const uint16_t itemId, const uint16_t resourceId, const uint8_t tier) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientEquipItem); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + msg->addU8(tier); send(msg); } -void ProtocolGame::sendEquipItemWithCountOrSubType(const uint16_t itemId, const uint16_t countOrSubType) +void ProtocolGame::sendEquipItemWithCountOrSubType(const uint16_t itemId, const uint16_t resourceId, const uint16_t countOrSubType) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientEquipItem); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + if (g_game.getFeature(Otc::GameCountU16)) { msg->addU16(countOrSubType); } else { @@ -322,12 +332,16 @@ void ProtocolGame::sendEquipItemWithCountOrSubType(const uint16_t itemId, const send(msg); } -void ProtocolGame::sendMove(const Position& fromPos, const uint16_t thingId, const uint8_t stackpos, const Position& toPos, const uint16_t count) +void ProtocolGame::sendMove(const Position& fromPos, const uint16_t thingId, const uint16_t resourceId, const uint8_t stackpos, const Position& toPos, const uint16_t count) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientMove); addPosition(msg, fromPos); msg->addU16(thingId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + msg->addU8(stackpos); addPosition(msg, toPos); if (g_game.getFeature(Otc::GameCountU16)) @@ -337,11 +351,15 @@ void ProtocolGame::sendMove(const Position& fromPos, const uint16_t thingId, con send(msg); } -void ProtocolGame::sendInspectNpcTrade(const uint16_t itemId, const uint16_t count) +void ProtocolGame::sendInspectNpcTrade(const uint16_t itemId, const uint16_t resourceId, const uint16_t count) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientInspectNpcTrade); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + if (g_game.getFeature(Otc::GameCountU16)) msg->addU16(count); else @@ -349,11 +367,15 @@ void ProtocolGame::sendInspectNpcTrade(const uint16_t itemId, const uint16_t cou send(msg); } -void ProtocolGame::sendBuyItem(const uint16_t itemId, const uint8_t subType, const uint16_t amount, const bool ignoreCapacity, const bool buyWithBackpack) +void ProtocolGame::sendBuyItem(const uint16_t itemId, uint16_t resourceId, const uint8_t subType, const uint16_t amount, const bool ignoreCapacity, const bool buyWithBackpack) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientBuyItem); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + msg->addU8(subType); if (g_game.getFeature(Otc::GameDoubleShopSellAmount)) msg->addU16(amount); @@ -364,11 +386,15 @@ void ProtocolGame::sendBuyItem(const uint16_t itemId, const uint8_t subType, con send(msg); } -void ProtocolGame::sendSellItem(const uint16_t itemId, const uint8_t subType, const uint16_t amount, const bool ignoreEquipped) +void ProtocolGame::sendSellItem(const uint16_t itemId, const uint16_t resourceId, const uint8_t subType, const uint16_t amount, const bool ignoreEquipped) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientSellItem); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + msg->addU8(subType); if (g_game.getFeature(Otc::GameDoubleShopSellAmount)) msg->addU16(amount); @@ -385,12 +411,16 @@ void ProtocolGame::sendCloseNpcTrade() send(msg); } -void ProtocolGame::sendRequestTrade(const Position& pos, const uint16_t thingId, const uint8_t stackpos, const uint32_t creatureId) +void ProtocolGame::sendRequestTrade(const Position& pos, const uint16_t thingId, const uint16_t resourceId, const uint8_t stackpos, const uint32_t creatureId) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientRequestTrade); addPosition(msg, pos); msg->addU16(thingId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + msg->addU8(stackpos); msg->addU32(creatureId); send(msg); @@ -494,7 +524,7 @@ void ProtocolGame::sendEditText(const uint32_t id, const std::string_view text) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientEditText); - msg->addU32(id); + msg->addU32(id); // window unique id msg->addString(text); send(msg); } @@ -504,17 +534,20 @@ void ProtocolGame::sendEditList(const uint32_t id, const uint8_t doorId, const s const auto& msg = std::make_shared(); msg->addU8(Proto::ClientEditList); msg->addU8(doorId); - msg->addU32(id); + msg->addU32(id); // window unique id msg->addString(text); send(msg); } -void ProtocolGame::sendLook(const Position& position, const uint16_t itemId, const uint8_t stackpos) +void ProtocolGame::sendLook(const Position& position, const uint16_t itemId, const uint16_t resourceId, const uint8_t stackpos) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientLook); addPosition(msg, position); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } msg->addU8(stackpos); send(msg); } @@ -711,9 +744,18 @@ void ProtocolGame::sendPartyAnalyzerAction(const uint8_t action, const std::vect // Only add items data for PARTYANALYZERACTION_PRICEVALUE (action 3) if (action == 3) { // PARTYANALYZERACTION_PRICEVALUE + + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + msg->addU16(static_cast(items.size())); for (const auto& [itemId, price] : items) { msg->addU16(itemId); + + // resourceId should go there, but the party analyzer is not implemented yet + if (multiSpr) { + msg->addU16(0); + } + msg->addU64(price); } } @@ -797,6 +839,9 @@ void ProtocolGame::sendTyping(const bool typing) void ProtocolGame::sendChangeOutfit(const Outfit& outfit) { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + const auto base = outfit.getBaseOutfit(); + const auto& msg = std::make_shared(); msg->addU8(Proto::ClientChangeOutfit); @@ -805,44 +850,80 @@ void ProtocolGame::sendChangeOutfit(const Outfit& outfit) } if (g_game.getFeature(Otc::GameLooktypeU16)) - msg->addU16(outfit.getId()); + msg->addU16(base.type); else - msg->addU8(static_cast(outfit.getId())); + msg->addU8(static_cast(base.type)); - msg->addU8(outfit.getHead()); - msg->addU8(outfit.getBody()); - msg->addU8(outfit.getLegs()); - msg->addU8(outfit.getFeet()); + if (multiSpr) + msg->addU16(base.resourceId); + + msg->addU8(base.head); + msg->addU8(base.body); + msg->addU8(base.legs); + msg->addU8(base.feet); if (g_game.getFeature(Otc::GamePlayerAddons)) msg->addU8(outfit.getAddons()); if (g_game.getFeature(Otc::GamePlayerMounts)) { - msg->addU16(outfit.getMount()); + const auto mount = outfit.getMount(); + msg->addU16(mount.type); + if (multiSpr) + msg->addU16(mount.resourceId); + if (g_game.getClientVersion() >= 1281) { - msg->addU8(0x00); - msg->addU8(0x00); - msg->addU8(0x00); - msg->addU8(0x00); + msg->addU8(mount.head); + msg->addU8(mount.body); + msg->addU8(mount.legs); + msg->addU8(mount.feet); } } + // outfit window "mount" checkbox if (g_game.getClientVersion() >= 1334) { msg->addU8(outfit.hasMount()); } + // familiar if (g_game.getFeature(Otc::GamePlayerFamiliars)) { - msg->addU16(outfit.getFamiliar()); //familiars + const auto familiar = outfit.getFamiliar(); + msg->addU16(familiar.type); //familiars + + if (multiSpr) + msg->addU16(familiar.resourceId); } + // outfit window "randomize mount" checkbox if (g_game.getClientVersion() >= 1281) { - msg->addU8(0x00); //randomizeMount + msg->addU8(0x00); // not implemented } + + // extended cosmetics if (g_game.getFeature(Otc::GameWingsAurasEffectsShader)) { - msg->addU16(outfit.getWing()); // wings - msg->addU16(outfit.getAura()); // auras - msg->addU16(outfit.getEffect()); // effects - msg->addString(outfit.getShader()); // shader + // wings + const auto wings = outfit.getWings(); + msg->addU16(wings.type); + if (multiSpr) + msg->addU16(wings.resourceId); + + // aura + const auto aura = outfit.getAura(); + msg->addU16(aura.type); + if (multiSpr) { + msg->addU16(aura.resourceId); + msg->addU8(aura.category); + } + + // effect + const auto effect = outfit.getEffect(); + msg->addU16(effect.type); + if (multiSpr) { + msg->addU16(effect.resourceId); + msg->addU8(effect.category); + } + + // shader + msg->addString(outfit.getShader()); } send(msg); @@ -982,12 +1063,15 @@ void ProtocolGame::sendNewNewRuleViolation(const uint8_t reason, const uint8_t a send(msg); } -void ProtocolGame::sendRequestItemInfo(const uint16_t itemId, const uint8_t subType, const uint8_t index) +void ProtocolGame::sendRequestItemInfo(const uint16_t itemId, const uint16_t resourceId, const uint8_t subType, const uint8_t index) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientRequestItemInfo); msg->addU8(subType); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } msg->addU8(index); send(msg); } @@ -1037,7 +1121,7 @@ void ProtocolGame::sendInspectionNormalObject(const Position& position) send(msg); } -void ProtocolGame::sendInspectionObject(const Otc::InspectObjectTypes inspectionType, const uint16_t itemId, const uint8_t itemCount) +void ProtocolGame::sendInspectionObject(const Otc::InspectObjectTypes inspectionType, const uint16_t itemId, const uint16_t resourceId, const uint8_t itemCount) { if (inspectionType != Otc::INSPECT_NPCTRADE && inspectionType != Otc::INSPECT_CYCLOPEDIA) { return; @@ -1047,6 +1131,10 @@ void ProtocolGame::sendInspectionObject(const Otc::InspectObjectTypes inspection msg->addU8(Proto::ClientInspectionObject); msg->addU8(inspectionType); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + msg->addU8(itemCount); send(msg); } @@ -1329,7 +1417,7 @@ void ProtocolGame::sendMarketLeave() send(msg); } -void ProtocolGame::sendMarketBrowse(const uint8_t browseId, const uint16_t browseType, const uint8_t tier) +void ProtocolGame::sendMarketBrowse(const uint8_t browseId, const uint16_t browseType, const uint8_t tier, const uint16_t resourceId) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientMarketBrowse); @@ -1337,10 +1425,16 @@ void ProtocolGame::sendMarketBrowse(const uint8_t browseId, const uint16_t brows msg->addU8(browseId); if (browseType > 0) { msg->addU16(browseType); - // If browseId is 3 (browse item), send tier if item has classification + // If browseId is 3 (browse item), send extra info if (browseId == 3) { - const auto& thing = g_things.getThingType(browseType, ThingCategoryItem); - if (thing && thing->getClassification() > 0) { + // resourceId + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + + // tier if classification > 0 + const auto& thing = g_things.getThingType(browseType, ThingCategoryItem, resourceId); + if (thing->getClassification() > 0) { msg->addU8(tier); } } @@ -1351,13 +1445,17 @@ void ProtocolGame::sendMarketBrowse(const uint8_t browseId, const uint16_t brows send(msg); } -void ProtocolGame::sendMarketCreateOffer(const uint8_t type, const uint16_t itemId, const uint8_t itemTier, const uint16_t amount, const uint64_t price, const uint8_t anonymous) +void ProtocolGame::sendMarketCreateOffer(const uint8_t type, const uint16_t itemId, const uint16_t resourceId, const uint8_t itemTier, const uint16_t amount, const uint64_t price, const uint8_t anonymous) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientMarketCreate); msg->addU8(type); msg->addU16(itemId); - if (const auto& thing = g_things.getThingType(itemId, ThingCategoryItem)) { + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + + if (const auto& thing = g_things.getThingType(itemId, ThingCategoryItem, resourceId)) { if (thing->getClassification() > 0) { msg->addU8(itemTier); } @@ -1414,16 +1512,32 @@ void ProtocolGame::sendOpenPortableForge() { send(msg); } -void ProtocolGame::sendForgeRequest(Otc::ForgeAction_t actionType, bool convergence, uint16_t firstItemid, uint8_t firstItemTier, uint16_t secondItemId, bool improveChance, bool tierLoss) { +void ProtocolGame::sendForgeRequest( + Otc::ForgeAction_t actionType, bool convergence, uint16_t firstItemid, uint16_t firstItemResourceId, uint8_t firstItemTier, + uint16_t secondItemId, uint16_t secondItemResourceId, bool improveChance, bool tierLoss +) { + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); + const auto& msg = std::make_shared(); msg->addU8(Proto::ClientForgeEnter); msg->addU8(static_cast(actionType)); if (actionType == Otc::ForgeAction_t::FUSION || actionType == Otc::ForgeAction_t::TRANSFER) { msg->addU8(static_cast(convergence)); + + // left item msg->addU16(firstItemid); + if (multiSpr) { + msg->addU16(firstItemResourceId); + } msg->addU8(firstItemTier); + + // right item msg->addU16(secondItemId); + if (multiSpr) { + msg->addU16(secondItemResourceId); + } + msg->addU8(static_cast(improveChance)); msg->addU8(static_cast(tierLoss)); } @@ -1485,31 +1599,47 @@ void ProtocolGame::sendGetRewardDaily(const uint8_t bonusShrine, const std::map< msg->addU8(Proto::sendGetRewardDaily); msg->addU8(bonusShrine); msg->addU8(items.size()); + + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); for (const auto& [itemId, count] : items) { msg->addU16(itemId); + + // to do: implement in the daily reward module + if (multiSpr) { + msg->addU16(0); + } msg->addU8(count); } send(msg); } -void ProtocolGame::sendStashWithdraw(const uint16_t itemId, const uint32_t count, const uint8_t stackpos) +void ProtocolGame::sendStashWithdraw(const uint16_t itemId, const uint16_t resourceId, const uint32_t count, const uint8_t stackpos) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientUseStash); msg->addU8(Otc::Supply_Stash_Actions_t::SUPPLY_STASH_ACTION_WITHDRAW); + msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + msg->addU32(count); msg->addU8(stackpos); send(msg); } -void ProtocolGame::sendStashStow(const Position& position, const uint16_t itemId, const uint32_t count, const uint8_t stackpos, const uint8_t action) +void ProtocolGame::sendStashStow(const Position& position, const uint16_t itemId, const uint16_t resourceId, const uint32_t count, const uint8_t stackpos, const uint8_t action) { const auto& msg = std::make_shared(); msg->addU8(Proto::ClientUseStash); msg->addU8(action); addPosition(msg, position); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } + msg->addU8(stackpos); if (action == Otc::Supply_Stash_Actions_t::SUPPLY_STASH_ACTION_STOW_ITEM) { @@ -1542,7 +1672,7 @@ void ProtocolGame::sendImbuementDurations(const bool isOpen) send(msg); } -void ProtocolGame::sendQuickLoot(const uint8_t variant, const Position& pos, const uint16_t itemId, const uint8_t stackpos) +void ProtocolGame::sendQuickLoot(const uint8_t variant, const Position& pos, const uint16_t itemId, const uint16_t resourceId, const uint8_t stackpos) { const auto msg = std::make_shared(); msg->addU8(Proto::ClientSendQuickLoot); @@ -1552,6 +1682,9 @@ void ProtocolGame::sendQuickLoot(const uint8_t variant, const Position& pos, con addPosition(msg, pos); if (variant != 2) { msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } msg->addU8(stackpos); } send(msg); @@ -1564,13 +1697,19 @@ void ProtocolGame::requestQuickLootBlackWhiteList(const uint8_t filter, const ui msg->addU8(filter); msg->addU16(size); + const bool multiSpr = g_game.getFeature(Otc::GameMultiSpr); for (const uint16_t item : listedItems) { msg->addU16(item); + + // to do: implement in the quick loot module + if (multiSpr) { + msg->addU16(0); + } } send(msg); } -void ProtocolGame::openContainerQuickLoot(const uint8_t action, const uint8_t category, const Position& pos, const uint16_t itemId, const uint8_t stackpos, const bool useMainAsFallback) +void ProtocolGame::openContainerQuickLoot(const uint8_t action, const uint8_t category, const Position& pos, const uint16_t itemId, const uint16_t resourceId, const uint8_t stackpos, const bool useMainAsFallback) { const auto msg = std::make_shared(); msg->addU8(Proto::ClientLootContainer); @@ -1580,6 +1719,9 @@ void ProtocolGame::openContainerQuickLoot(const uint8_t action, const uint8_t ca msg->addU8(category); addPosition(msg, pos); msg->addU16(itemId); + if (g_game.getFeature(Otc::GameMultiSpr)) { + msg->addU16(resourceId); + } msg->addU8(stackpos); } else if (action == 3) { msg->addU8(useMainAsFallback); diff --git a/src/client/spriteappearances.cpp b/src/client/spriteappearances.cpp deleted file mode 100644 index 19ad0321b0..0000000000 --- a/src/client/spriteappearances.cpp +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright (c) 2022 Nekiro - * - * 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 "spriteappearances.h" - -#include -#include "lzma.h" -#include "gameconfig.h" -#include "framework/core/filestream.h" -#include "framework/core/resourcemanager.h" -#include "framework/graphics/image.h" - - // warnings related to protobuf - // https://android.googlesource.com/platform/external/protobuf/+/brillo-m9-dev/vsprojects/readme.txt - -using json = nlohmann::json; - -SpriteAppearances g_spriteAppearances; - -void SpriteAppearances::init() -{ - // in tibia 12.81 there is currently 3482 sheets - m_sheets.reserve(4000); -} - -void SpriteAppearances::terminate() -{ - unload(); -} - -Size SpriteSheet::getSpriteSize() const -{ - // this array includes all possible combinations within 384x384 sheet - // if you intend to change that, you will also have to modify the assets editor - // CHANGING THIS MAY BREAK READING EXISTING SPRITESHEETS - - // tile sizes in spritesheets, see SpriteLayout for array key definitions - static const std::array sizes = { - Size(32,32), // 0 - Size(32,64), // 1 - Size(64,32), // 2 - Size(64,64), // 3 - Size(32,96), // 4 - Size(32,128), // 5 - Size(32,192), // 6 - Size(32,384), // 7 - Size(64,96), // 8 - Size(64,128), // 9 - Size(64,192), // 10 - Size(64,384), // 11 - Size(96,32), // 12 - Size(96,64), // 13 - Size(96,96), // 14 - Size(96,128), // 15 - Size(96,192), // 16 - Size(96,384), // 17 - Size(128,32), // 18 - Size(128,64), // 19 - Size(128,96), // 20 - Size(128,128), // 21 - Size(128,192), // 22 - Size(128,384), // 23 - Size(192,32), // 24 - Size(192,64), // 25 - Size(192,96), // 26 - Size(192,128), // 27 - Size(192,192), // 28 - Size(192,384), // 29 - Size(384,32), // 30 - Size(384,64), // 31 - Size(384,96), // 32 - Size(384,128), // 33 - Size(384,192), // 34 - Size(384,384) // 35 - }; - - const size_t idx = static_cast(spriteLayout); - if (idx < sizes.size()) - return sizes[idx]; - - return sizes[0]; -} - -int SpriteSheet::getSpritesPerSheet() const -{ - const Size& size = getSpriteSize(); - const int spritesPerColumn = SpriteSheet::SIZE / size.height(); - - return getColumns() * spritesPerColumn; -} - -bool SpriteAppearances::loadSpriteSheet(const SpriteSheetPtr& sheet) const -{ - if (sheet->m_loadingState.load(std::memory_order_acquire) == SpriteLoadState::LOADING) - return false; - - if (sheet->data) - return true; - - if (sheet->m_loadingState.exchange(SpriteLoadState::LOADING, std::memory_order_acq_rel) == SpriteLoadState::LOADING) - return false; - - try { - const auto& path = fmt::format("{}{}", g_spriteAppearances.getPath(), sheet->file); - if (!g_resources.fileExists(path)) - return false; - - const auto& fin = g_resources.openFile(path); - fin->cache(true); - - thread_local static std::array decompressBuffer; - - /* - CIP's header, always 32 (0x20) bytes. - Header format: - [0x00, X): A variable number of NULL (0x00) bytes. The amount of pad-bytes can vary depending on how many - bytes the "7-bit integer encoded LZMA file size" take. - [X, X + 0x05): The constant byte sequence [0x70 0x0A 0xFA 0x80 0x24] - [X + 0x05, 0x20]: LZMA file size (Note: excluding the 32 bytes of this header) encoded as a 7-bit integer - */ - - while (fin->getU8() == 0x00); - fin->skip(4); - while ((fin->getU8() & 0x80) == 0x80); - - const uint8_t lclppb = fin->getU8(); - - lzma_options_lzma options{}; - options.lc = lclppb % 9; - - const int remainder = lclppb / 9; - options.lp = remainder % 5; - options.pb = remainder / 5; - - uint32_t dictionarySize = 0; - for (uint8_t i = 0; i < 4; ++i) { - dictionarySize += fin->getU8() << (i * 8); - } - - options.dict_size = dictionarySize; - - fin->skip(8); // cip compressed size - - lzma_stream stream = LZMA_STREAM_INIT; - - const lzma_filter filters[2] = { - lzma_filter{LZMA_FILTER_LZMA1, &options}, - lzma_filter{LZMA_VLI_UNKNOWN, nullptr} - }; - - lzma_ret ret = lzma_raw_decoder(&stream, filters); - if (ret != LZMA_OK) { - throw stdext::exception(fmt::format("failed to initialize lzma raw decoder result: {}", ret)); - } - - stream.next_in = &fin->m_data[fin->tell()]; - stream.avail_in = fin->size() - fin->tell(); - stream.next_out = decompressBuffer.data(); - stream.avail_out = decompressBuffer.size(); - - const auto result = lzma_code(&stream, LZMA_RUN); - lzma_end(&stream); - - if (result != LZMA_STREAM_END) - throw stdext::exception("LZMA decompression failed"); - - // pixel offset - const uint8_t* bmpOffsetPtr = decompressBuffer.data() + 10; - const uint32_t bmpDataOffset = - bmpOffsetPtr[0] | - (bmpOffsetPtr[1] << 8) | - (bmpOffsetPtr[2] << 16) | - (bmpOffsetPtr[3] << 24); - - // validate offset - if (bmpDataOffset + BYTES_IN_SPRITE_SHEET > LZMA_UNCOMPRESSED_SIZE) - throw stdext::exception("sprite sheet image offset out of bounds"); - - uint8_t* bufferStart = decompressBuffer.data() + bmpDataOffset; - - // swap BGR ? RGB and fix magenta - for (int i = 0; i < BYTES_IN_SPRITE_SHEET; i += 4) { - std::swap(bufferStart[i], bufferStart[i + 2]); // B <-> R - - const uint32_t rgb = bufferStart[i] | (bufferStart[i + 1] << 8) | (bufferStart[i + 2] << 16); - if (rgb == 0xFF00FF) { - bufferStart[i + 0] = 0x00; - bufferStart[i + 1] = 0x00; - bufferStart[i + 2] = 0x00; - bufferStart[i + 3] = 0x00; - } - } - - // vertical flip - constexpr int halfHeight = SpriteSheet::SIZE / 2; - uint8_t tempLine[SPRITE_SHEET_WIDTH_BYTES]; - for (int y = 0; y < halfHeight; ++y) { - uint8_t* top = bufferStart + y * SPRITE_SHEET_WIDTH_BYTES; - uint8_t* bottom = bufferStart + (SpriteSheet::SIZE - 1 - y) * SPRITE_SHEET_WIDTH_BYTES; - - std::memcpy(tempLine, top, SPRITE_SHEET_WIDTH_BYTES); - std::memcpy(top, bottom, SPRITE_SHEET_WIDTH_BYTES); - std::memcpy(bottom, tempLine, SPRITE_SHEET_WIDTH_BYTES); - } - - sheet->data = std::make_unique(BYTES_IN_SPRITE_SHEET); - std::memcpy(sheet->data.get(), bufferStart, BYTES_IN_SPRITE_SHEET); - - sheet->m_loadingState.store(SpriteLoadState::LOADED, std::memory_order_release); - return true; - } catch (const std::exception& e) { - sheet->m_loadingState.store(SpriteLoadState::NONE, std::memory_order_release); - g_logger.error("Failed to load single sprite sheet '{}': {}", sheet->file, e.what()); - return false; - } -} - -void SpriteAppearances::unload() -{ - m_spritesCount = 0; - m_sheets.clear(); -} - -SpriteSheetPtr SpriteAppearances::getSheetBySpriteId(const int id, bool& isLoading, const bool load /* = true */) -{ - if (id == 0) { - return nullptr; - } - - // find sheet - const auto sheetIt = std::ranges::find_if(m_sheets, [=](const SpriteSheetPtr& sheet) { - return id >= sheet->firstId && id <= sheet->lastId; - }); - - if (sheetIt == m_sheets.end()) - return nullptr; - - const auto& sheet = *sheetIt; - - if (load && !loadSpriteSheet(sheet)) { - isLoading = sheet->m_loadingState == SpriteLoadState::LOADING; - return nullptr; - } - - return sheet; -} - -ImagePtr SpriteAppearances::getSpriteImage(const int id, bool& isLoading) -{ - try { - const auto& sheet = getSheetBySpriteId(id, isLoading, true); - if (!sheet) { - return nullptr; - } - - const Size& size = sheet->getSpriteSize(); - - const auto& image = std::make_shared(size); - uint8_t* pixelData = image->getPixelData(); - - const int spriteOffset = id - sheet->firstId; - const int allColumns = sheet->getColumns(); - const int spritesPerSheet = sheet->getSpritesPerSheet(); - - if (spriteOffset < 0 || spriteOffset >= spritesPerSheet) { - g_logger.error("Sprite id {} is out of bounds for sheet {} (offset {}, max {})", id, sheet->file, spriteOffset, spritesPerSheet); - return nullptr; - } - const int spriteRow = std::floor(static_cast(spriteOffset) / static_cast(allColumns)); - const int spriteColumn = spriteOffset % allColumns; - - const int spriteWidthBytes = size.width() * 4; - - for (int height = size.height() * spriteRow, offset = 0; height < size.height() + (spriteRow * size.height()); height++, offset++) { - std::memcpy(&pixelData[offset * spriteWidthBytes], &sheet->data[(height * SPRITE_SHEET_WIDTH_BYTES) + (spriteColumn * spriteWidthBytes)], spriteWidthBytes); - } - - if (!image->hasTransparentPixel()) { - // The image must be more than 4 pixels transparent to be considered transparent. - uint8_t cntTrans = 0; - const auto& buf = image->getPixels(); - for (size_t i = 3, n = buf.size(); i < n; i += 4) { - if (buf[i] == 0x00 && ++cntTrans > 4) { - image->setTransparentPixel(true); - break; - } - } - } - - return image; - } catch (const stdext::exception& e) { - g_logger.error("Failed to get sprite id {}: {}", id, e.what()); - return nullptr; - } -} - -void SpriteAppearances::saveSpriteToFile(const int id, const std::string& file) -{ - if (const auto& sprite = getSpriteImage(id)) { - sprite->savePNG(file); - } -} - -void SpriteAppearances::saveSheetToFileBySprite(const int id, const std::string& file) -{ - if (const auto& sheet = getSheetBySpriteId(id)) { - Image image({ SpriteSheet::SIZE }, 4, sheet->data.get()); - image.savePNG(file); - } -} - -void SpriteAppearances::saveSheetToFile(const SpriteSheetPtr& sheet, const std::string& file) -{ - Image image({ SpriteSheet::SIZE }, 4, sheet->data.get()); - image.savePNG(file); -} \ No newline at end of file diff --git a/src/client/spriteappearances.h b/src/client/spriteappearances.h deleted file mode 100644 index c84a56d287..0000000000 --- a/src/client/spriteappearances.h +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2022 Nekiro - * - * 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 -#include - -enum class SpriteLayout -{ - // default sheet sizes - SIZE_32_32 = 0, - SIZE_32_64 = 1, - SIZE_64_32 = 2, - SIZE_64_64 = 3, - - // extended sheet sizes (all possible combinations within 384x384 spritesheet) - SIZE_32_96 = 4, - SIZE_32_128 = 5, - SIZE_32_192 = 6, - SIZE_32_384 = 7, - SIZE_64_96 = 8, - SIZE_64_128 = 9, - SIZE_64_192 = 10, - SIZE_64_384 = 11, - SIZE_96_32 = 12, - SIZE_96_64 = 13, - SIZE_96_96 = 14, - SIZE_96_128 = 15, - SIZE_96_192 = 16, - SIZE_96_384 = 17, - SIZE_128_32 = 18, - SIZE_128_64 = 19, - SIZE_128_96 = 20, - SIZE_128_128 = 21, - SIZE_128_192 = 22, - SIZE_128_384 = 23, - SIZE_192_32 = 24, - SIZE_192_64 = 25, - SIZE_192_96 = 26, - SIZE_192_128 = 27, - SIZE_192_192 = 28, - SIZE_192_384 = 29, - SIZE_384_32 = 30, - SIZE_384_64 = 31, - SIZE_384_96 = 32, - SIZE_384_128 = 33, - SIZE_384_192 = 34, - SIZE_384_384 = 35 -}; - -enum class SpriteLoadState -{ - NONE, - LOADING, - LOADED -}; - -class SpriteSheet -{ -public: - static constexpr uint16_t SIZE = 384; - - SpriteSheet(const int firstId, const int lastId, const SpriteLayout spriteLayout, std::string file) : firstId(firstId), lastId(lastId), spriteLayout(spriteLayout), file(std::move( - file)) - { - } - - Size getSpriteSize() const; - - int getSpritesPerSheet() const; - - // 64 pixel width == 6 columns each 64x or 32 pixels, 12 columns - int getColumns() const { return SIZE / getSpriteSize().width(); } - - int firstId = 0; - int lastId = 0; - - SpriteLayout spriteLayout = SpriteLayout::SIZE_32_32; - std::atomic m_loadingState = SpriteLoadState::NONE; - std::unique_ptr data; - std::string file; -}; - -//@bindsingleton g_spriteAppearances -class SpriteAppearances -{ -public: - void init(); - void terminate(); - - void unload(); - - void setSpritesCount(const int count) { m_spritesCount = count; } - int getSpritesCount() const { return m_spritesCount; } - - void setPath(const std::string& path) { m_path = path; } - std::string getPath() const { return m_path; } - - bool loadSpriteSheet(const SpriteSheetPtr& sheet) const; - void saveSheetToFileBySprite(int id, const std::string& file); - void saveSheetToFile(const SpriteSheetPtr& sheet, const std::string& file); - SpriteSheetPtr getSheetBySpriteId(int id, bool load = true) { - bool isLoading = false; - return getSheetBySpriteId(id, isLoading, load); - } - SpriteSheetPtr getSheetBySpriteId(int id, bool& isLoading, bool load = true); - - void addSpriteSheet(const SpriteSheetPtr& sheet) { m_sheets.emplace_back(sheet); } - - ImagePtr getSpriteImage(int id) { - bool isLoading = false; - return getSpriteImage(id, isLoading); - } - ImagePtr getSpriteImage(int id, bool& isLoading); - void saveSpriteToFile(int id, const std::string& file); - -private: - uint32_t m_spritesCount{ 0 }; - std::vector m_sheets; - std::string m_path; -}; - -extern SpriteAppearances g_spriteAppearances; diff --git a/src/client/spritemanager.cpp b/src/client/spritemanager.cpp index 5fdee13daf..777bbbcc38 100644 --- a/src/client/spritemanager.cpp +++ b/src/client/spritemanager.cpp @@ -24,14 +24,17 @@ #include "game.h" #include "gameconfig.h" -#include "spriteappearances.h" #include "framework/core/asyncdispatcher.h" #include "framework/core/filestream.h" #include "framework/core/graphicalapplication.h" #include "framework/core/resourcemanager.h" #include "framework/graphics/image.h" -SpriteManager g_sprites; +#include +#include "lzma.h" + +// warnings related to protobuf +// https://android.googlesource.com/platform/external/protobuf/+/brillo-m9-dev/vsprojects/readme.txt FileMetadata::FileMetadata(const FileStreamPtr& file) { offset = file->getU32(); @@ -40,10 +43,7 @@ FileMetadata::FileMetadata(const FileStreamPtr& file) { spriteId = std::stoi(fileName); } -void SpriteManager::init() {} -void SpriteManager::terminate() { unload(); } - -void SpriteManager::reload() { +void LegacySpriteManager::reload() { if (g_app.isEncrypted()) return; @@ -53,7 +53,7 @@ void SpriteManager::reload() { load(); } -void SpriteManager::load() { +void LegacySpriteManager::load() { m_spritesFiles.resize(g_asyncDispatcher.get_thread_count()); if (g_app.isLoadingAsyncTexture()) { for (auto& file : m_spritesFiles) @@ -61,7 +61,7 @@ void SpriteManager::load() { } else (m_spritesFiles[0] = std::make_unique(g_resources.openFile(m_lastFileName)))->file->cache(true); } -bool SpriteManager::loadSpr(std::string file) +bool LegacySpriteManager::loadSpr(std::string file) { m_spritesCount = 0; m_signature = 0; @@ -82,7 +82,7 @@ bool SpriteManager::loadSpr(std::string file) return false; } -bool SpriteManager::loadRegularSpr(std::string file) +bool LegacySpriteManager::loadRegularSpr(std::string file) { try { m_lastFileName = g_resources.guessFilePath(file, "spr"); @@ -101,7 +101,7 @@ bool SpriteManager::loadRegularSpr(std::string file) } } -bool SpriteManager::loadCwmSpr(std::string file) +bool LegacySpriteManager::loadCwmSpr(std::string file) { m_cwmSpritesMetadata.clear(); @@ -148,7 +148,7 @@ bool SpriteManager::loadCwmSpr(std::string file) } #ifdef FRAMEWORK_EDITOR -void SpriteManager::saveSpr(const std::string& fileName) +void LegacySpriteManager::saveSpr(const std::string& fileName) { if (!m_loaded) throw Exception("failed to save, spr is not loaded"); @@ -206,19 +206,8 @@ void SpriteManager::saveSpr(const std::string& fileName) } #endif -void SpriteManager::unload() -{ - m_spritesCount = 0; - m_signature = 0; - m_spritesFiles.clear(); -} - -ImagePtr SpriteManager::getSpriteImage(const int id, bool& isLoading) +ImagePtr LegacySpriteManager::getSpriteImage(const int id, bool& isLoading) { - if (g_game.getProtocolVersion() >= 1281 && !g_game.getFeature(Otc::GameLoadSprInsteadProtobuf)) { - return g_spriteAppearances.getSpriteImage(id, isLoading); - } - const auto threadId = g_app.isLoadingAsyncTexture() ? stdext::getThreadId() : 0; if (const auto& sf = m_spritesFiles[threadId % m_spritesFiles.size()]) { if (sf->m_loadingState.exchange(SpriteLoadState::LOADING, std::memory_order_acq_rel) == SpriteLoadState::LOADING) { @@ -236,7 +225,7 @@ ImagePtr SpriteManager::getSpriteImage(const int id, bool& isLoading) return nullptr; } -ImagePtr SpriteManager::getSpriteImageHd(const int id, const FileStreamPtr& file) +ImagePtr LegacySpriteManager::getSpriteImageHd(const int id, const FileStreamPtr& file) { const auto it = m_cwmSpritesMetadata.find(id); if (it == m_cwmSpritesMetadata.end()) @@ -258,7 +247,7 @@ uint16_t readU16FromBuffer(const uint8_t* data, size_t& offset) { return val; } -ImagePtr SpriteManager::getSpriteImage(const int id, const FileStreamPtr& file) +ImagePtr LegacySpriteManager::getSpriteImage(const int id, const FileStreamPtr& file) { if (id == 0 || !file) return nullptr; @@ -317,26 +306,9 @@ ImagePtr SpriteManager::getSpriteImage(const int id, const FileStreamPtr& file) offset += bytesToRead; if (useAlpha) { - for (int i = 0, src = 0; i < actualColoredPixels && writePos + 4 <= maxWriteSize; ++i, src += 4) { - pixels[writePos + 0] = tempBuffer[src + 0]; - pixels[writePos + 1] = tempBuffer[src + 1]; - pixels[writePos + 2] = tempBuffer[src + 2]; - const uint8_t alpha = tempBuffer[src + 3]; - pixels[writePos + 3] = alpha; - - if (alpha != 0xFF) hasAlpha = true; - else if (transparentCount <= 4 && alpha == 0x00) ++transparentCount; - - writePos += 4; - } + setPixelsRGBA(pixels, tempBuffer, writePos, actualColoredPixels, maxWriteSize, hasAlpha, transparentCount); } else { - for (int i = 0, src = 0; i < actualColoredPixels && writePos + 4 <= maxWriteSize; ++i, src += 3) { - pixels[writePos + 0] = tempBuffer[src + 0]; - pixels[writePos + 1] = tempBuffer[src + 1]; - pixels[writePos + 2] = tempBuffer[src + 2]; - pixels[writePos + 3] = 0xFF; - writePos += 4; - } + setPixelsRGB(pixels, tempBuffer, writePos, actualColoredPixels, maxWriteSize); } } @@ -353,4 +325,312 @@ ImagePtr SpriteManager::getSpriteImage(const int id, const FileStreamPtr& file) g_logger.error("Failed to get sprite id {}: {}", id, e.what()); return nullptr; } -} \ No newline at end of file +} + +void LegacySpriteManager::setPixelsRGB(uint8_t* pixels, const uint8_t* tempBuffer, int& writePos, const int actualColoredPixels, const int maxWriteSize) +{ + for (int i = 0, src = 0; i < actualColoredPixels && writePos + 4 <= maxWriteSize; ++i, src += 3) { + pixels[writePos + 0] = tempBuffer[src + 0]; + pixels[writePos + 1] = tempBuffer[src + 1]; + pixels[writePos + 2] = tempBuffer[src + 2]; + pixels[writePos + 3] = 0xFF; + writePos += 4; + } +} + +void LegacySpriteManager::setPixelsRGBA(uint8_t* pixels, const uint8_t* tempBuffer, int& writePos, const int actualColoredPixels, const int maxWriteSize, bool& hasAlpha, int& transparentCount) +{ + for (int i = 0, src = 0; i < actualColoredPixels && writePos + 4 <= maxWriteSize; ++i, src += 4) { + pixels[writePos + 0] = tempBuffer[src + 0]; + pixels[writePos + 1] = tempBuffer[src + 1]; + pixels[writePos + 2] = tempBuffer[src + 2]; + const uint8_t alpha = tempBuffer[src + 3]; + pixels[writePos + 3] = alpha; + + if (alpha != 0xFF) hasAlpha = true; + else if (transparentCount <= 4 && alpha == 0x00) ++transparentCount; + + writePos += 4; + } +} + +using json = nlohmann::json; + +Size SpriteSheet::getSpriteSize() const +{ + // this array includes all possible combinations within 384x384 sheet + // if you intend to change that, you will also have to modify the assets editor + // CHANGING THIS MAY BREAK READING EXISTING SPRITESHEETS + + // tile sizes in spritesheets, see SpriteLayout for array key definitions + static const std::array sizes = { + Size(32,32), // 0 + Size(32,64), // 1 + Size(64,32), // 2 + Size(64,64), // 3 + Size(32,96), // 4 + Size(32,128), // 5 + Size(32,192), // 6 + Size(32,384), // 7 + Size(64,96), // 8 + Size(64,128), // 9 + Size(64,192), // 10 + Size(64,384), // 11 + Size(96,32), // 12 + Size(96,64), // 13 + Size(96,96), // 14 + Size(96,128), // 15 + Size(96,192), // 16 + Size(96,384), // 17 + Size(128,32), // 18 + Size(128,64), // 19 + Size(128,96), // 20 + Size(128,128), // 21 + Size(128,192), // 22 + Size(128,384), // 23 + Size(192,32), // 24 + Size(192,64), // 25 + Size(192,96), // 26 + Size(192,128), // 27 + Size(192,192), // 28 + Size(192,384), // 29 + Size(384,32), // 30 + Size(384,64), // 31 + Size(384,96), // 32 + Size(384,128), // 33 + Size(384,192), // 34 + Size(384,384) // 35 + }; + + if (const auto idx = static_cast(spriteLayout); idx < sizes.size()) + return sizes[idx]; + + return sizes[0]; +} + +int SpriteSheet::getSpritesPerSheet() const +{ + const Size& size = getSpriteSize(); + const int spritesPerColumn = SpriteSheet::SIZE / size.height(); + + return getColumns() * spritesPerColumn; +} + +bool ProtobufSpriteManager::loadSpriteSheet(const SpriteSheetPtr& sheet) const +{ + if (sheet->m_loadingState.load(std::memory_order_acquire) == SpriteLoadState::LOADING) + return false; + + if (sheet->data) + return true; + + if (sheet->m_loadingState.exchange(SpriteLoadState::LOADING, std::memory_order_acq_rel) == SpriteLoadState::LOADING) + return false; + + try { + const auto& path = fmt::format("{}{}", getPath(), sheet->file); + if (!g_resources.fileExists(path)) + return false; + + const auto& fin = g_resources.openFile(path); + fin->cache(true); + + thread_local static std::array decompressBuffer; + + /* + CIP's header, always 32 (0x20) bytes. + Header format: + [0x00, X): A variable number of NULL (0x00) bytes. The amount of pad-bytes can vary depending on how many + bytes the "7-bit integer encoded LZMA file size" take. + [X, X + 0x05): The constant byte sequence [0x70 0x0A 0xFA 0x80 0x24] + [X + 0x05, 0x20]: LZMA file size (Note: excluding the 32 bytes of this header) encoded as a 7-bit integer + */ + + while (fin->getU8() == 0x00); + fin->skip(4); + while ((fin->getU8() & 0x80) == 0x80); + + const uint8_t lclppb = fin->getU8(); + + lzma_options_lzma options{}; + options.lc = lclppb % 9; + + const int remainder = lclppb / 9; + options.lp = remainder % 5; + options.pb = remainder / 5; + + uint32_t dictionarySize = 0; + for (uint8_t i = 0; i < 4; ++i) { + dictionarySize += fin->getU8() << (i * 8); + } + + options.dict_size = dictionarySize; + + fin->skip(8); // cip compressed size + + lzma_stream stream = LZMA_STREAM_INIT; + + const lzma_filter filters[2] = { + lzma_filter{LZMA_FILTER_LZMA1, &options}, + lzma_filter{LZMA_VLI_UNKNOWN, nullptr} + }; + + if (lzma_ret ret = lzma_raw_decoder(&stream, filters); ret != LZMA_OK) { + throw stdext::exception(fmt::format("failed to initialize lzma raw decoder result: {}", ret)); + } + + stream.next_in = &fin->m_data[fin->tell()]; + stream.avail_in = fin->size() - fin->tell(); + stream.next_out = decompressBuffer.data(); + stream.avail_out = decompressBuffer.size(); + + const auto result = lzma_code(&stream, LZMA_RUN); + lzma_end(&stream); + + if (result != LZMA_STREAM_END) + throw stdext::exception("LZMA decompression failed"); + + // pixel offset + const uint8_t* bmpOffsetPtr = decompressBuffer.data() + 10; + const uint32_t bmpDataOffset = + bmpOffsetPtr[0] | + (bmpOffsetPtr[1] << 8) | + (bmpOffsetPtr[2] << 16) | + (bmpOffsetPtr[3] << 24); + + // validate offset + if (bmpDataOffset + BYTES_IN_SPRITE_SHEET > LZMA_UNCOMPRESSED_SIZE) + throw stdext::exception("sprite sheet image offset out of bounds"); + + uint8_t* bufferStart = decompressBuffer.data() + bmpDataOffset; + + // swap BGR ? RGB and fix magenta + for (int i = 0; i < BYTES_IN_SPRITE_SHEET; i += 4) { + std::swap(bufferStart[i], bufferStart[i + 2]); // B <-> R + + const uint32_t rgb = bufferStart[i] | (bufferStart[i + 1] << 8) | (bufferStart[i + 2] << 16); + if (rgb == 0xFF00FF) { + bufferStart[i + 0] = 0x00; + bufferStart[i + 1] = 0x00; + bufferStart[i + 2] = 0x00; + bufferStart[i + 3] = 0x00; + } + } + + // vertical flip + constexpr int halfHeight = SpriteSheet::SIZE / 2; + uint8_t tempLine[SPRITE_SHEET_WIDTH_BYTES]; + for (int y = 0; y < halfHeight; ++y) { + uint8_t* top = bufferStart + y * SPRITE_SHEET_WIDTH_BYTES; + uint8_t* bottom = bufferStart + (SpriteSheet::SIZE - 1 - y) * SPRITE_SHEET_WIDTH_BYTES; + + std::memcpy(tempLine, top, SPRITE_SHEET_WIDTH_BYTES); + std::memcpy(top, bottom, SPRITE_SHEET_WIDTH_BYTES); + std::memcpy(bottom, tempLine, SPRITE_SHEET_WIDTH_BYTES); + } + + sheet->data = std::make_unique(BYTES_IN_SPRITE_SHEET); + std::memcpy(sheet->data.get(), bufferStart, BYTES_IN_SPRITE_SHEET); + + sheet->m_loadingState.store(SpriteLoadState::LOADED, std::memory_order_release); + return true; + } catch (const std::exception& e) { + sheet->m_loadingState.store(SpriteLoadState::NONE, std::memory_order_release); + g_logger.error("Failed to load single sprite sheet '{}': {}", sheet->file, e.what()); + return false; + } +} + +SpriteSheetPtr ProtobufSpriteManager::getSheetBySpriteId(const int id, bool& isLoading, const bool load /* = true */) +{ + if (id == 0) { + return nullptr; + } + + // find sheet + const auto sheetIt = std::ranges::find_if(m_sheets, [=](const SpriteSheetPtr& sheet) { + return id >= sheet->firstId && id <= sheet->lastId; + }); + + if (sheetIt == m_sheets.end()) + return nullptr; + + const auto& sheet = *sheetIt; + + if (load && !loadSpriteSheet(sheet)) { + isLoading = sheet->m_loadingState == SpriteLoadState::LOADING; + return nullptr; + } + + return sheet; +} + +ImagePtr ProtobufSpriteManager::getSpriteImage(const int id, bool& isLoading) +{ + try { + const auto& sheet = getSheetBySpriteId(id, isLoading, true); + if (!sheet) { + return nullptr; + } + + const Size& size = sheet->getSpriteSize(); + + const auto& image = std::make_shared(size); + uint8_t* pixelData = image->getPixelData(); + + const int spriteOffset = id - sheet->firstId; + const int allColumns = sheet->getColumns(); + if ( + const int spritesPerSheet = sheet->getSpritesPerSheet(); + spriteOffset < 0 || spriteOffset >= spritesPerSheet + ) { + g_logger.error("Sprite id {} is out of bounds for sheet {} (offset {}, max {})", id, sheet->file, spriteOffset, spritesPerSheet); + return nullptr; + } + const int spriteRow = std::floor(static_cast(spriteOffset) / static_cast(allColumns)); + const int spriteColumn = spriteOffset % allColumns; + + const int spriteWidthBytes = size.width() * 4; + + for (int height = size.height() * spriteRow, offset = 0; height < size.height() + (spriteRow * size.height()); height++, offset++) { + std::memcpy(&pixelData[offset * spriteWidthBytes], &sheet->data[(height * SPRITE_SHEET_WIDTH_BYTES) + (spriteColumn * spriteWidthBytes)], spriteWidthBytes); + } + + if (!image->hasTransparentPixel()) { + image->checkTransparentPixels(); + } + + return image; + } catch (const stdext::exception& e) { + g_logger.error("Failed to get sprite id {}: {}", id, e.what()); + return nullptr; + } +} + +#ifdef FRAMEWORK_EDITOR +void ProtobufSpriteManager::saveSpriteToFile(const int id, const std::string& file) +{ + if (const auto& sprite = ISpriteManager::getSpriteImageById(id)) { + sprite->savePNG(file); + } +} + +void ProtobufSpriteManager::saveSpr(const std::string&) +{ + g_logger.traceError("ProtobufSpriteManager does not support saveSpr, consider using saveSheetToFile."); +} + +void ProtobufSpriteManager::saveSheetToFileBySprite(const int id, const std::string& file) +{ + if (const auto& sheet = getSheetBySpriteId(id)) { + Image image({ SpriteSheet::SIZE }, 4, sheet->data.get()); + image.savePNG(file); + } +} + +void ProtobufSpriteManager::saveSheetToFile(const SpriteSheetPtr& sheet, const std::string& file) +{ + Image image({ SpriteSheet::SIZE }, 4, sheet->data.get()); + image.savePNG(file); +} +#endif diff --git a/src/client/spritemanager.h b/src/client/spritemanager.h index 2bf23a7b50..bf20a2fe21 100644 --- a/src/client/spritemanager.h +++ b/src/client/spritemanager.h @@ -24,6 +24,81 @@ #include #include +#include "thingtype.h" + +enum class SpriteLoadState +{ + NONE, + LOADING, + LOADED +}; + +enum class SpriteLayout +{ + // default sheet sizes + SIZE_32_32 = 0, + SIZE_32_64 = 1, + SIZE_64_32 = 2, + SIZE_64_64 = 3, + + // extended sheet sizes (all possible combinations within 384x384 spritesheet) + SIZE_32_96 = 4, + SIZE_32_128 = 5, + SIZE_32_192 = 6, + SIZE_32_384 = 7, + SIZE_64_96 = 8, + SIZE_64_128 = 9, + SIZE_64_192 = 10, + SIZE_64_384 = 11, + SIZE_96_32 = 12, + SIZE_96_64 = 13, + SIZE_96_96 = 14, + SIZE_96_128 = 15, + SIZE_96_192 = 16, + SIZE_96_384 = 17, + SIZE_128_32 = 18, + SIZE_128_64 = 19, + SIZE_128_96 = 20, + SIZE_128_128 = 21, + SIZE_128_192 = 22, + SIZE_128_384 = 23, + SIZE_192_32 = 24, + SIZE_192_64 = 25, + SIZE_192_96 = 26, + SIZE_192_128 = 27, + SIZE_192_192 = 28, + SIZE_192_384 = 29, + SIZE_384_32 = 30, + SIZE_384_64 = 31, + SIZE_384_96 = 32, + SIZE_384_128 = 33, + SIZE_384_192 = 34, + SIZE_384_384 = 35 +}; + +class ISpriteManager { +public: + virtual ~ISpriteManager() = default; + + virtual ImagePtr getSpriteImage(int id, bool& isLoading) = 0; + ImagePtr getSpriteImageById(int id) + { + bool isLoading = false; + return getSpriteImage(id, isLoading); + } + + virtual int getSpritesCount() const = 0; + + virtual void reload() = 0; + virtual bool isLoaded() const { return false; } + virtual bool isProtobuf() const { return false; } + + virtual uint32_t getSignature() const { return 0; } + +#ifdef FRAMEWORK_EDITOR + virtual void saveSpr(const std::string& fileName) = 0; +#endif +}; class FileMetadata { @@ -42,41 +117,40 @@ class FileMetadata uint32_t spriteId = 0; }; -//@bindsingleton g_sprites -class SpriteManager +class LegacySpriteManager : public ISpriteManager { public: - void init(); - void terminate(); + LegacySpriteManager() = default; + ~LegacySpriteManager() override { + m_spritesCount = 0; + m_signature = 0; + m_spritesFiles.clear(); + } + + // non-copyable + LegacySpriteManager(const LegacySpriteManager&) = delete; + LegacySpriteManager& operator=(const LegacySpriteManager&) = delete; + + LegacySpriteManager(LegacySpriteManager&&) = delete; + LegacySpriteManager& operator=(LegacySpriteManager&&) = delete; bool loadSpr(std::string file); bool loadRegularSpr(std::string file); bool loadCwmSpr(std::string file); - void reload(); - void unload(); + void reload() override; #ifdef FRAMEWORK_EDITOR - void saveSpr(const std::string& fileName); + void saveSpr(const std::string& fileName) override; #endif - uint32_t getSignature() { return m_signature; } - int getSpritesCount() { return m_spritesCount; } - - ImagePtr getSpriteImage(int id) { - bool isLoading = false; - return getSpriteImage(id, isLoading); - } + uint32_t getSignature() const override { return m_signature; } + int getSpritesCount() const override { return m_spritesCount; } - ImagePtr getSpriteImage(int id, bool& isLoading); - bool isLoaded() { return m_loaded; } + ImagePtr getSpriteImage(int id, bool& isLoading) override; + bool isLoaded() const override { return m_loaded; } + bool isProtobuf() const override { return false; } private: - enum class SpriteLoadState - { - NONE, - LOADING, - LOADED - }; struct FileStream_m { @@ -93,6 +167,9 @@ class SpriteManager ImagePtr getSpriteImageHd(int id, const FileStreamPtr& file); ImagePtr getSpriteImage(int id, const FileStreamPtr& file); + void setPixelsRGB(uint8_t* pixels, const uint8_t* tempBuffer, int& writePos, const int actualColoredPixels, const int maxWriteSize); + void setPixelsRGBA(uint8_t* pixels, const uint8_t* tempBuffer, int& writePos, const int actualColoredPixels, const int maxWriteSize, bool& hasAlpha, int& transparentCount); + std::string m_lastFileName; @@ -106,4 +183,83 @@ class SpriteManager std::unordered_map m_cwmSpritesMetadata; }; -extern SpriteManager g_sprites; +class SpriteSheet +{ +public: + static constexpr uint16_t SIZE = 384; + + SpriteSheet(const int firstId, const int lastId, const SpriteLayout spriteLayout, std::string file) : firstId(firstId), lastId(lastId), spriteLayout(spriteLayout), file(std::move( + file)) + { + } + + Size getSpriteSize() const; + + int getSpritesPerSheet() const; + + // 64 pixel width == 6 columns each 64x or 32 pixels, 12 columns + int getColumns() const { return SIZE / getSpriteSize().width(); } + + int firstId = 0; + int lastId = 0; + + SpriteLayout spriteLayout = SpriteLayout::SIZE_32_32; + std::atomic m_loadingState = SpriteLoadState::NONE; + std::unique_ptr data; + std::string file; +}; + +class ProtobufSpriteManager : public ISpriteManager +{ +public: + ProtobufSpriteManager() { + // in tibia 12.81 there is currently 3482 sheets + m_sheets.reserve(4000); + } + ~ProtobufSpriteManager() override { + m_spritesCount = 0; + m_sheets.clear(); + } + + // non-copyable + ProtobufSpriteManager(const ProtobufSpriteManager&) = delete; + ProtobufSpriteManager& operator=(const ProtobufSpriteManager&) = delete; + + ProtobufSpriteManager(ProtobufSpriteManager&&) = delete; + ProtobufSpriteManager& operator=(ProtobufSpriteManager&&) = delete; + + void reload() override { /* for protobuf assets this is managed per sheet */ }; + + uint32_t getSignature() const override { return 0; } + + void setSpritesCount(const int count) { m_spritesCount = count; } + int getSpritesCount() const override { return m_spritesCount; } + + void setPath(const std::string_view path) { m_path = path; } + std::string getPath() const { return m_path; } + + bool loadSpriteSheet(const SpriteSheetPtr& sheet) const; + SpriteSheetPtr getSheetBySpriteId(int id, bool load = true) { + bool isLoading = false; + return getSheetBySpriteId(id, isLoading, load); + } + SpriteSheetPtr getSheetBySpriteId(int id, bool& isLoading, bool load = true); + + void addSpriteSheet(const SpriteSheetPtr& sheet) { m_sheets.emplace_back(sheet); } + + ImagePtr getSpriteImage(int id, bool& isLoading) override; + +#ifdef FRAMEWORK_EDITOR + void saveSheetToFileBySprite(int id, const std::string& file); + void saveSheetToFile(const SpriteSheetPtr& sheet, const std::string& file); + void saveSpr(const std::string& fileName) override; + void saveSpriteToFile(int id, const std::string& file); +#endif + + bool isLoaded() const override { return true; } + bool isProtobuf() const override { return true; } +private: + uint32_t m_spritesCount{ 0 }; + std::vector m_sheets; + std::string m_path; +}; diff --git a/src/client/staticdata.h b/src/client/staticdata.h index 31bc741010..3a711438cf 100644 --- a/src/client/staticdata.h +++ b/src/client/staticdata.h @@ -297,6 +297,7 @@ struct StoreOffer uint16_t mountId; uint16_t itemId; uint16_t outfitId; + uint16_t resourceId; uint8_t outfitHead, outfitBody, outfitLegs, outfitFeet; uint8_t sex; uint16_t maleOutfitId, femaleOutfitId; @@ -308,30 +309,6 @@ struct StoreOffer uint16_t productsCapacity; }; -struct HomeOffer -{ - std::string name; - uint8_t unknownByte; - uint32_t id; - uint16_t unknownU16; - uint32_t price; - uint8_t coinType; - uint16_t disabledReasonIndex; - uint8_t unknownByte2; - uint8_t type; - std::string icon; - uint16_t mountClientId; - uint16_t itemType; - uint16_t sexId; - struct { uint8_t lookHead, lookBody, lookLegs, lookFeet; } outfit; - uint8_t tryOnType; - uint16_t collection; - uint16_t popularityScore; - uint32_t stateNewUntil; - uint8_t userConfiguration; - uint16_t productsCapacity; -}; - struct Banner { std::string image; @@ -345,7 +322,7 @@ struct StoreData std::string categoryName; uint32_t redirectId; std::vector disableReasons; - std::vector homeOffers; + std::vector homeOffers; std::vector storeOffers; std::vector banners; uint8_t bannerDelay; @@ -566,6 +543,7 @@ struct OutfitColorStruct struct CharacterInfoOutfits { uint16_t lookType; + uint16_t resourceId; std::string name; uint8_t addons; uint8_t type; @@ -575,6 +553,7 @@ struct CharacterInfoOutfits struct CharacterInfoMounts { uint16_t mountId; + uint16_t resourceId; std::string name; uint8_t type; uint32_t isCurrent; @@ -583,6 +562,7 @@ struct CharacterInfoMounts struct CharacterInfoFamiliar { uint16_t lookType; + uint16_t resourceId; std::string name; uint8_t type; uint32_t isCurrent; @@ -591,16 +571,18 @@ struct CharacterInfoFamiliar struct DailyRewardItem { uint16_t itemId; + uint16_t resourceId; std::string name; uint32_t weight; }; struct DailyRewardBundle { - uint8_t bundleType; - uint16_t itemId; + uint8_t bundleType{ 0 }; + uint16_t itemId{ 0 }; + uint16_t resourceId{ 0 }; std::string name; - uint8_t count; + uint8_t count{ 0 }; }; struct DailyRewardDay @@ -744,6 +726,7 @@ struct CyclopediaCharacterMiscStats struct ForgeItemInfo { uint16_t id{ 0 }; + uint16_t resourceId{ 0 }; uint8_t tier{ 0 }; uint16_t count{ 0 }; }; @@ -778,8 +761,12 @@ struct ForgeResultData bool convergence{ false }; bool success{ false }; uint16_t leftItemId{ 0 }; + uint16_t leftItemResourceId{ 0 }; uint8_t leftTier{ 0 }; uint16_t rightItemId{ 0 }; + uint16_t rightItemResourceId{ 0 }; + uint16_t outcomeItemId{ 0 }; + uint16_t outcomeResourceId{ 0 }; uint8_t rightTier{ 0 }; uint8_t bonus{ 0 }; uint8_t coreCount{ 0 }; @@ -823,3 +810,31 @@ struct ForgeHistory std::string description; uint8_t bonus; }; + +struct OutfitWindowThing +{ + uint16_t id{ 0 }; + uint16_t resourceId{ 0 }; + std::string name; + uint8_t addons{ 0 }; + uint8_t lockReason{ 0 }; + uint32_t offerId{ 0 }; + ThingCategory category{ ThingCategoryCreature }; +}; + +struct ActionBarItem +{ + uint16_t id{ 0 }; + uint16_t resourceId{ 0 }; + uint8_t subType{ 0 }; // fluid/tier + uint32_t count{ 0 }; +}; + +struct LootContainerConf +{ + uint16_t lootId{ 0 }; + uint16_t lootResourceId{ 0 }; + uint16_t retrieveId{ 0 }; + uint16_t retrieveResourceId{ 0 }; + uint8_t categoryType{ 0 }; +}; \ No newline at end of file diff --git a/src/client/thing.cpp b/src/client/thing.cpp index a236f07b9c..b786218e20 100644 --- a/src/client/thing.cpp +++ b/src/client/thing.cpp @@ -230,7 +230,7 @@ int Thing::getNumPatternZ() const { } int Thing::getAnimationPhases() const { if (const auto t = getThingType(); t) - return t->getAnimationPhases(); + return t->getAnimationPhase(); return 0; } int Thing::getGroundSpeed() const { @@ -543,7 +543,7 @@ bool Thing::hasExpireStop() const { } bool Thing::hasAnimationPhases() const { if (const auto t = getThingType(); t) - return t->getAnimationPhases() > 1; + return t->getAnimationPhase() > 1; return false; } bool Thing::isDecoKit() const { diff --git a/src/client/thing.h b/src/client/thing.h index b5deda3cdd..245fe6199b 100644 --- a/src/client/thing.h +++ b/src/client/thing.h @@ -39,10 +39,10 @@ class Thing : public AttachableObject LuaObjectPtr attachedObjectToLuaObject() override { return asLuaObject(); } bool isThing() override { return true; } - virtual void setId(uint32_t /*id*/) {} virtual void setPosition(const Position& position, uint8_t stackPos = 0); virtual uint32_t getId() { return m_clientId; } + virtual uint16_t getResourceId() { return m_resourceId; } uint16_t getClientId() const { return m_clientId; } virtual Position getPosition() { return m_position; } @@ -185,7 +185,7 @@ class Thing : public AttachableObject bool isHighlighted() { return m_highlightColor != Color::white; } void setHighlight(const Color& color) { if (m_highlightColor != color) m_highlightColor = color; } - bool isHided() { return isOwnerHidden(); } + bool isHidden() { return isOwnerHidden(); } uint8_t getPatternX()const { return m_numPatternX; } uint8_t getPatternY()const { return m_numPatternY; } @@ -226,6 +226,7 @@ class Thing : public AttachableObject Position m_position; uint16_t m_clientId{ 0 }; + uint16_t m_resourceId{ 0 }; int8_t m_stackPos{ -1 }; uint8_t m_numPatternX{ 0 }; diff --git a/src/client/thingtype.cpp b/src/client/thingtype.cpp index b2e8d058c1..daff247e1f 100644 --- a/src/client/thingtype.cpp +++ b/src/client/thingtype.cpp @@ -26,8 +26,8 @@ #include "game.h" #include "gameconfig.h" #include "lightview.h" -#include "spriteappearances.h" #include "spritemanager.h" +#include "thingtypemanager.h" #include "framework/core/asyncdispatcher.h" #include "framework/core/filestream.h" #include "framework/graphics/drawpoolmanager.h" @@ -50,65 +50,65 @@ namespace { } } -void ThingType::unserializeAppearance(const uint16_t clientId, const ThingCategory category, const appearances::Appearance& appearance) +void ThingType::unserializeAppearance(const uint16_t clientId, const uint16_t resourceId, ProtobufSpriteManagerPtr spriteManager, const ThingCategory category, const appearances::Appearance& appearance) { m_null = false; m_id = clientId; + m_resourceId = resourceId; m_category = category; m_name = appearance.name(); m_description = appearance.description(); applyAppearanceFlags(appearance.flags()); - if (!g_game.getFeature(Otc::GameLoadSprInsteadProtobuf)) { - m_animationPhases = 0; - int totalSpritesCount = 0; - - for (const auto& framegroup : appearance.frame_group()) { - const int frameGroupType = framegroup.fixed_frame_group(); - const auto& spriteInfo = framegroup.sprite_info(); - const auto& animation = spriteInfo.animation(); - spriteInfo.sprite_id(); // sprites - const auto& spritesPhases = animation.sprite_phase(); + m_animationPhases = 0; + int totalSpritesCount = 0; - m_numPatternX = spriteInfo.pattern_width(); - m_numPatternY = spriteInfo.pattern_height(); - m_numPatternZ = spriteInfo.pattern_depth(); - m_layers = spriteInfo.layers(); - m_opaque = spriteInfo.is_opaque(); + for (const auto& framegroup : appearance.frame_group()) { + const int frameGroupType = framegroup.fixed_frame_group(); + const auto& spriteInfo = framegroup.sprite_info(); + const auto& animation = spriteInfo.animation(); + spriteInfo.sprite_id(); // sprites + const auto& spritesPhases = animation.sprite_phase(); - m_animationPhases += std::max(1, spritesPhases.size()); + m_numPatternX = spriteInfo.pattern_width(); + m_numPatternY = spriteInfo.pattern_height(); + m_numPatternZ = spriteInfo.pattern_depth(); + m_layers = spriteInfo.layers(); + m_opaque = spriteInfo.is_opaque(); - if (const auto& sheet = g_spriteAppearances.getSheetBySpriteId(spriteInfo.sprite_id(0), false)) { - m_size = sheet->getSpriteSize() / g_gameConfig.getSpriteSize(); - } + m_animationPhases += std::max(1, spritesPhases.size()); - // animations - if (spritesPhases.size() > 1) { - auto* animator = new Animator; - animator->unserializeAppearance(animation); + if (const auto& sheet = spriteManager->getSheetBySpriteId(spriteInfo.sprite_id(0), false)) { + m_size = sheet->getSpriteSize() / g_gameConfig.getSpriteSize(); + } - if (frameGroupType == FrameGroupMoving) - m_animator = animator; - else if (frameGroupType == FrameGroupIdle || frameGroupType == FrameGroupInitial) - m_idleAnimator = animator; - } + // animations + if (spritesPhases.size() > 1) { + auto* animator = new Animator; + animator->unserializeAppearance(animation); - const int totalSprites = m_layers * m_numPatternX * m_numPatternY * m_numPatternZ * std::max(1, spritesPhases.size()); + if (frameGroupType == FrameGroupMoving) + m_animator = animator; + else if (frameGroupType == FrameGroupIdle || frameGroupType == FrameGroupInitial) + m_idleAnimator = animator; + } - if (totalSpritesCount + totalSprites > 4096) - throw Exception("a thing type has more than 4096 sprites"); + const int totalSprites = m_layers * m_numPatternX * m_numPatternY * m_numPatternZ * std::max(1, spritesPhases.size()); - m_spritesIndex.resize(totalSpritesCount + totalSprites); - for (int j = totalSpritesCount, spriteId = 0; j < (totalSpritesCount + totalSprites); ++j, ++spriteId) { - m_spritesIndex[j] = spriteInfo.sprite_id(spriteId); - } + if (totalSpritesCount + totalSprites > 4096) + throw Exception("a thing type has more than 4096 sprites"); - totalSpritesCount += totalSprites; + m_spritesIndex.resize(totalSpritesCount + totalSprites); + for (int j = totalSpritesCount, spriteId = 0; j < (totalSpritesCount + totalSprites); ++j, ++spriteId) { + m_spritesIndex[j] = spriteInfo.sprite_id(spriteId); } - m_textureData.resize(m_animationPhases); + totalSpritesCount += totalSprites; } + + m_textureData.resize(m_animationPhases); + } void ThingType::applyAppearanceFlags(const appearances::AppearanceFlags& flags) @@ -278,14 +278,7 @@ void ThingType::applyAppearanceFlags(const appearances::AppearanceFlags& flags) m_market.category = static_cast(flags.market().category()); m_market.tradeAs = flags.market().trade_as_object_id(); m_market.showAs = flags.market().show_as_object_id(); - if (g_game.getFeature(Otc::GameLoadSprInsteadProtobuf)) { - // keep from tibia.dat - if (m_market.name.empty() && !flags.market().name().empty()) { - m_market.name = flags.market().name(); - } - } else { - m_market.name = m_name; - } + m_market.name = m_name; for (const int32_t voc : flags.market().restrict_to_profession()) { uint16_t vocBitMask = std::pow(2, voc - 1); @@ -371,15 +364,18 @@ void ThingType::applyAppearanceFlags(const appearances::AppearanceFlags& flags) } } -void ThingType::unserialize(const uint16_t clientId, const ThingCategory category, const FileStreamPtr& fin) +void ThingType::unserialize(const uint16_t clientId, const uint16_t resourceId, const ThingCategory category, const FileStreamPtr& fin) { m_null = false; m_id = clientId; + m_resourceId = resourceId; m_category = category; int count = 0; int attr = -1; bool done = false; + int version = g_game.getClientVersion(); + for (int i = 0; i < ThingLastAttr; ++i) { ++count; attr = fin->getU8(); @@ -388,225 +384,21 @@ void ThingType::unserialize(const uint16_t clientId, const ThingCategory categor break; } - if (g_game.getClientVersion() >= 1000) { - /* In 10.10+ all attributes from 16 and up were - * incremented by 1 to make space for 16 as - * "No Movement Animation" flag. - */ - if (attr == 16) - attr = ThingAttrNoMoveAnimation; - else if (attr == 254) { // Usable - attr = ThingAttrUsable; - } else if (attr == 35) { // Default Action - attr = ThingAttrDefaultAction; - } else if (attr > 16) - attr -= 1; - } else if (g_game.getClientVersion() >= 860) { - /* Default attribute values follow - * the format of 8.6-9.86. - * Therefore no changes here. - */ - } else if (g_game.getClientVersion() >= 780) { - /* In 7.80-8.54 all attributes from 8 and higher were - * incremented by 1 to make space for 8 as - * "Item Charges" flag. - */ - if (attr == 8) { - attr = ThingAttrChargeable; - continue; - } - if (attr > 8) - attr -= 1; - } else if (g_game.getClientVersion() >= 755) { - /* In 7.55-7.72 attributes 23 is "Floor Change". */ - if (attr == 23) - attr = ThingAttrFloorChange; - } else if (g_game.getClientVersion() >= 740) { - /* In 7.4-7.5 attribute "Ground Border" did not exist - * attributes 1-15 have to be adjusted. - * Several other changes in the format. - */ - if (attr > 0 && attr <= 15) - attr += 1; - else if (attr == 16) - attr = ThingAttrLight; - else if (attr == 17) - attr = ThingAttrFloorChange; - else if (attr == 18) - attr = ThingAttrFullGround; - else if (attr == 19) - attr = ThingAttrElevation; - else if (attr == 20) - attr = ThingAttrDisplacement; - else if (attr == 22) - attr = ThingAttrMinimapColor; - else if (attr == 23) - attr = ThingAttrRotateable; - else if (attr == 24) - attr = ThingAttrLyingCorpse; - else if (attr == 25) - attr = ThingAttrHangable; - else if (attr == 26) - attr = ThingAttrHookSouth; - else if (attr == 27) - attr = ThingAttrHookEast; - else if (attr == 28) - attr = ThingAttrAnimateAlways; - - /* "Multi Use" and "Force Use" are swapped */ - if (attr == ThingAttrMultiUse) - attr = ThingAttrForceUse; - else if (attr == ThingAttrForceUse) - attr = ThingAttrMultiUse; - } + // translate flag id from dat to otc + translateFlagId(version, attr); const auto thingAttr = static_cast(attr); m_flags |= thingAttrToThingFlagAttr(thingAttr); - switch (attr) { - case ThingAttrDisplacement: - { - if (g_game.getClientVersion() >= 755) { - if (g_game.getFeature(Otc::GameNegativeOffset)) { - m_displacement.x = fin->get16(); - m_displacement.y = fin->get16(); - } else { - m_displacement.x = fin->getU16(); - m_displacement.y = fin->getU16(); - } - } else { - m_displacement.x = 8; - m_displacement.y = 8; - } - break; - } - case ThingAttrLight: - { - m_light.intensity = fin->getU16(); - m_light.color = fin->getU16(); - break; - } - case ThingAttrMarket: - { - m_market.category = static_cast(fin->getU16()); - m_market.tradeAs = fin->getU16(); - m_market.showAs = fin->getU16(); - m_market.name = fin->getString(); - m_market.restrictVocation = fin->getU16(); - m_market.requiredLevel = fin->getU16(); - break; - } - case ThingAttrElevation: m_elevation = fin->getU16(); break; - case ThingAttrGround: m_groundSpeed = fin->getU16(); break; - case ThingAttrWritable: m_maxTextLength = fin->getU16(); break; - case ThingAttrWritableOnce:m_maxTextLength = fin->getU16(); break; - case ThingAttrMinimapColor: m_minimapColor = fin->getU16(); break; - case ThingAttrCloth: m_clothSlot = fin->getU16(); break; - case ThingAttrLensHelp: m_lensHelp = fin->getU16(); break; - case ThingAttrDefaultAction: m_defaultAction = static_cast(fin->getU16()); break; - } + // read flag properties + unserializeAttribute(attr, fin); } if (!done) throw Exception("corrupt data (id: {}, category: {}, count: {}, lastAttr: {})", m_id, m_category, count, attr); - const bool hasFrameGroups = category == ThingCategoryCreature && g_game.getFeature(Otc::GameIdleAnimations); - const uint8_t groupCount = hasFrameGroups ? fin->getU8() : 1; - - m_animationPhases = 0; - int totalSpritesCount = 0; - std::vector sizes; - std::vector total_sprites; - - for (int i = 0; i < groupCount; ++i) { - uint8_t frameGroupType = FrameGroupDefault; - if (hasFrameGroups) - frameGroupType = fin->getU8(); - - const uint8_t width = fin->getU8(); - const uint8_t height = fin->getU8(); - m_size = { width, height }; - sizes.emplace_back(m_size); - if (width > 1 || height > 1) { - m_realSize = std::max(m_realSize, fin->getU8()); - } - - m_layers = fin->getU8(); - m_numPatternX = fin->getU8(); - m_numPatternY = fin->getU8(); - if (g_game.getClientVersion() >= 755) - m_numPatternZ = fin->getU8(); - else - m_numPatternZ = 1; - - const int groupAnimationsPhases = fin->getU8(); - m_animationPhases += groupAnimationsPhases; - - if (groupAnimationsPhases > 1 && g_game.getFeature(Otc::GameEnhancedAnimations)) { - auto* animator = new Animator; - animator->unserialize(groupAnimationsPhases, fin); - - if (frameGroupType == FrameGroupMoving) - m_animator = animator; - else if (frameGroupType == FrameGroupIdle) - m_idleAnimator = animator; - } - - const int totalSprites = m_size.area() * m_layers * m_numPatternX * m_numPatternY * m_numPatternZ * groupAnimationsPhases; - total_sprites.push_back(totalSprites); - if (totalSpritesCount + totalSprites > 4096) - throw Exception("a thing type has more than 4096 sprites"); - - m_spritesIndex.resize(totalSpritesCount + totalSprites); - for (int j = totalSpritesCount; j < (totalSpritesCount + totalSprites); ++j) - m_spritesIndex[j] = g_game.getFeature(Otc::GameSpritesU32) ? fin->getU32() : fin->getU16(); - - totalSpritesCount += totalSprites; - } - if (sizes.size() > 1) { - bool hasDifferentSizes = false; - const Size& firstSize = sizes[0]; - for (size_t i = 1; i < sizes.size(); ++i) { - if (sizes[i] != firstSize) { - hasDifferentSizes = true; - break; - } - } - if (hasDifferentSizes) { - for (const auto& s : sizes) { - m_size.setWidth(std::max(m_size.width(), s.width())); - m_size.setHeight(std::max(m_size.height(), s.height())); - } - const size_t expectedSize = m_size.area() * m_layers * m_numPatternX * m_numPatternY * m_numPatternZ * m_animationPhases; - if (expectedSize != m_spritesIndex.size()) { - const std::vector sprites(std::move(m_spritesIndex)); - m_spritesIndex.clear(); - m_spritesIndex.reserve(expectedSize); - for (size_t i = 0, idx = 0; i < sizes.size(); ++i) { - const int totalSprites = total_sprites[i]; - if (m_size == sizes[i]) { - for (int j = 0; j < totalSprites; ++j) { - m_spritesIndex.push_back(sprites[idx++]); - } - continue; - } - const size_t patterns = (totalSprites / sizes[i].area()); - for (size_t p = 0; p < patterns; ++p) { - for (int x = 0; x < m_size.width(); ++x) { - for (int y = 0; y < m_size.height(); ++y) { - if (x < sizes[i].width() && y < sizes[i].height()) { - m_spritesIndex.push_back(sprites[idx++]); - continue; - } - m_spritesIndex.push_back(0); - } - } - } - } - } - } - } - m_textureData.resize(m_animationPhases); + // read sprite size, frame groups, animation info + unserializeSpriteInfo(fin); } void ThingType::unserializeOtml(const OTMLNodePtr& node) @@ -704,7 +496,7 @@ const TexturePtr& ThingType::getTexture(const int animationPhase) bool expected = false; if (m_loading.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { bool async = g_app.isLoadingAsyncTexture(); - if (g_game.isUsingProtobuf() && g_drawPool.getCurrentType() == DrawPoolType::FOREGROUND) + if (g_things.isUsingProtobuf(m_resourceId) && g_drawPool.getCurrentType() == DrawPoolType::FOREGROUND) async = false; if (!async) { @@ -744,7 +536,7 @@ void ThingType::loadTexture(const int animationPhase) const int indexSize = textureLayers * m_numPatternX * m_numPatternY * m_numPatternZ; const auto& textureSize = getBestTextureDimension(m_size.width(), m_size.height(), indexSize); const auto& fullImage = useCustomImage ? Image::load(m_customImage) : std::make_shared(textureSize * g_gameConfig.getSpriteSize()); - const bool protobufSupported = g_game.isUsingProtobuf(); + const bool protobufSupported = g_things.isUsingProtobuf(m_resourceId); static Color maskColors[] = { Color::red, Color::green, Color::blue, Color::yellow }; @@ -764,7 +556,7 @@ void ThingType::loadTexture(const int animationPhase) const uint32_t spriteIndex = getSpriteIndex(-1, -1, spriteMask ? 1 : l, x, y, z, animationPhase); auto spriteId = m_spritesIndex[spriteIndex]; bool isLoading = false; - const auto& spriteImage = g_sprites.getSpriteImage(spriteId, isLoading); + const auto& spriteImage = g_things.getSpriteImage(spriteId, m_resourceId, isLoading); if (isLoading) return; @@ -793,7 +585,7 @@ void ThingType::loadTexture(const int animationPhase) const uint32_t spriteIndex = getSpriteIndex(w, h, spriteMask ? 1 : l, x, y, z, animationPhase); auto spriteId = m_spritesIndex[spriteIndex]; bool isLoading = false; - const auto& spriteImage = g_sprites.getSpriteImage(spriteId, isLoading); + const auto& spriteImage = g_things.getSpriteImage(spriteId, m_resourceId, isLoading); if (isLoading) return; @@ -823,8 +615,7 @@ void ThingType::loadTexture(const int animationPhase) posData.rects = { framePos + Point(m_size.width(), m_size.height()) * g_gameConfig.getSpriteSize() - Point(1), framePos }; for (int fx = framePos.x; fx < framePos.x + m_size.width() * g_gameConfig.getSpriteSize(); ++fx) { for (int fy = framePos.y; fy < framePos.y + m_size.height() * g_gameConfig.getSpriteSize(); ++fy) { - const uint8_t* p = fullImage->getPixel(fx, fy); - if (p[3] == 0x00) + if (const uint8_t* p = fullImage->getPixel(fx, fy); p[3] == 0x00) continue; posData.rects.setTop(std::min(fy, posData.rects.top())); @@ -883,6 +674,266 @@ Size ThingType::getBestTextureDimension(int w, int h, const int count) return bestDimension; } +void ThingType::translateFlagId(const int version, int& flagId) +{ + // In 10.10+ all attributes after 16 were incremented by 1 + // in order to make space for "no movement animation" flag. + if (version >= 1000) + translateFlagId1000(flagId); + + // Default attribute values follow the format of 8.6-9.86. + // Therefore no changes here. + else if (version >= 860) + return; + + // In 7.80-8.54 all attributes after 8 were incremented by 1 + // in order to make space for rune charges flag + else if (version >= 780) + translateFlagId780(flagId); + + // In 7.55-7.72 attributes 23 is "Floor Change". + else if (version >= 755) + translateFlagId755(flagId); + + // In 7.4-7.5 attribute "Ground Border" did not exist + // attributes 1-15 have to be adjusted. + else if (version >= 740) + translateFlagId740(flagId); +} + +void ThingType::translateFlagId740(int& flagId) +{ + if (flagId > 0 && flagId <= 15) + flagId += 1; + + // several other changes in the format + else if (flagId == 16) + flagId = ThingAttrLight; + else if (flagId == 17) + flagId = ThingAttrFloorChange; + else if (flagId == 18) + flagId = ThingAttrFullGround; + else if (flagId == 19) + flagId = ThingAttrElevation; + else if (flagId == 20) + flagId = ThingAttrDisplacement; + else if (flagId == 22) + flagId = ThingAttrMinimapColor; + else if (flagId == 23) + flagId = ThingAttrRotateable; + else if (flagId == 24) + flagId = ThingAttrLyingCorpse; + else if (flagId == 25) + flagId = ThingAttrHangable; + else if (flagId == 26) + flagId = ThingAttrHookSouth; + else if (flagId == 27) + flagId = ThingAttrHookEast; + else if (flagId == 28) + flagId = ThingAttrAnimateAlways; + + // "Multi Use" and "Force Use" are swapped + if (flagId == ThingAttrMultiUse) + flagId = ThingAttrForceUse; + else if (flagId == ThingAttrForceUse) + flagId = ThingAttrMultiUse; +} + +void ThingType::translateFlagId755(int& flagId) +{ + if (flagId == 23) + flagId = ThingAttrFloorChange; +} + +void ThingType::translateFlagId780(int& flagId) +{ + if (flagId == 8) + flagId = ThingAttrChargeable; + else if (flagId > 8) + flagId -= 1; +} + +void ThingType::translateFlagId1000(int& flagId) +{ + if (flagId == 16) + flagId = ThingAttrNoMoveAnimation; + else if (flagId == 254) // Usable + flagId = ThingAttrUsable; + else if (flagId == 35) // Default Action + flagId = ThingAttrDefaultAction; + else if (flagId > 16) + flagId -= 1; +} + +void ThingType::unserializeAttribute(int attr, const FileStreamPtr& fin) +{ + switch (attr) { + case ThingAttrDisplacement: + { + if (g_game.getClientVersion() >= 755) { + if (g_game.getFeature(Otc::GameNegativeOffset)) { + m_displacement.x = fin->get16(); + m_displacement.y = fin->get16(); + } else { + m_displacement.x = fin->getU16(); + m_displacement.y = fin->getU16(); + } + } else { + m_displacement.x = 8; + m_displacement.y = 8; + } + break; + } + case ThingAttrLight: + { + m_light.intensity = fin->getU16(); + m_light.color = fin->getU16(); + break; + } + case ThingAttrMarket: + { + m_market.category = static_cast(fin->getU16()); + m_market.tradeAs = fin->getU16(); + m_market.showAs = fin->getU16(); + m_market.name = fin->getString(); + m_market.restrictVocation = fin->getU16(); + m_market.requiredLevel = fin->getU16(); + break; + } + case ThingAttrElevation: m_elevation = fin->getU16(); break; + case ThingAttrGround: m_groundSpeed = fin->getU16(); break; + case ThingAttrWritable: m_maxTextLength = fin->getU16(); break; + case ThingAttrWritableOnce:m_maxTextLength = fin->getU16(); break; + case ThingAttrMinimapColor: m_minimapColor = fin->getU16(); break; + case ThingAttrCloth: m_clothSlot = fin->getU16(); break; + case ThingAttrLensHelp: m_lensHelp = fin->getU16(); break; + case ThingAttrDefaultAction: m_defaultAction = static_cast(fin->getU16()); break; + } +} + +void ThingType::unserializeSpriteInfo(const FileStreamPtr& fin) +{ + const bool hasFrameGroups = m_category == ThingCategoryCreature && g_game.getFeature(Otc::GameIdleAnimations); + const uint8_t groupCount = hasFrameGroups ? fin->getU8() : 1; + + m_animationPhases = 0; + int totalSpritesCount = 0; + std::vector sizes; + std::vector total_sprites; + + for (int i = 0; i < groupCount; ++i) { + unserializeFrameGroup(fin, hasFrameGroups, sizes, total_sprites, totalSpritesCount); + } + + if (sizes.size() > 1) { + bool hasDifferentSizes = false; + const Size& firstSize = sizes[0]; + for (size_t i = 1; i < sizes.size(); ++i) { + if (sizes[i] != firstSize) { + hasDifferentSizes = true; + break; + } + } + if (hasDifferentSizes) { + adjustSpriteSizes(sizes, total_sprites); + } + } + m_textureData.resize(m_animationPhases); +} + +void ThingType::unserializeFrameGroup(const FileStreamPtr& fin, const bool hasFrameGroups, std::vector& sizes, std::vector& total_sprites, int& totalSpritesCount) +{ + uint8_t frameGroupType = FrameGroupDefault; + if (hasFrameGroups) + frameGroupType = fin->getU8(); + + const uint8_t width = fin->getU8(); + const uint8_t height = fin->getU8(); + m_size = { width, height }; + sizes.emplace_back(m_size); + if (width > 1 || height > 1) { + m_realSize = std::max(m_realSize, fin->getU8()); + } + + m_layers = fin->getU8(); + m_numPatternX = fin->getU8(); + m_numPatternY = fin->getU8(); + if (g_game.getClientVersion() >= 755) + m_numPatternZ = fin->getU8(); + else + m_numPatternZ = 1; + + const int groupAnimationsPhases = fin->getU8(); + m_animationPhases += groupAnimationsPhases; + + if (groupAnimationsPhases > 1 && g_game.getFeature(Otc::GameEnhancedAnimations)) { + auto* animator = new Animator; + animator->unserialize(groupAnimationsPhases, fin); + + if (frameGroupType == FrameGroupMoving) + m_animator = animator; + else if (frameGroupType == FrameGroupIdle) + m_idleAnimator = animator; + } + + const int totalSprites = m_size.area() * m_layers * m_numPatternX * m_numPatternY * m_numPatternZ * groupAnimationsPhases; + total_sprites.push_back(totalSprites); + if (totalSpritesCount + totalSprites > 4096) + throw Exception("a thing type has more than 4096 sprites"); + + m_spritesIndex.resize(totalSpritesCount + totalSprites); + for (int j = totalSpritesCount; j < (totalSpritesCount + totalSprites); ++j) + m_spritesIndex[j] = g_game.getFeature(Otc::GameSpritesU32) ? fin->getU32() : fin->getU16(); + + totalSpritesCount += totalSprites; +} + +void ThingType::adjustSpriteSizes(const std::vector& sizes, const std::vector& total_sprites) +{ + for (const auto& s : sizes) { + m_size.setWidth(std::max(m_size.width(), s.width())); + m_size.setHeight(std::max(m_size.height(), s.height())); + } + + const size_t expectedSize = m_size.area() * m_layers * m_numPatternX * m_numPatternY * m_numPatternZ * m_animationPhases; + + if (expectedSize == m_spritesIndex.size()) { + return; + } + + const std::vector sprites(std::move(m_spritesIndex)); + m_spritesIndex.clear(); + m_spritesIndex.reserve(expectedSize); + for (size_t frameId = 0, spriteIndex = 0; frameId < sizes.size(); ++frameId) { + const int totalSprites = total_sprites[frameId]; + if (m_size == sizes[frameId]) { + for (int j = 0; j < totalSprites; ++j) { + m_spritesIndex.push_back(sprites[spriteIndex]); + ++spriteIndex; + } + continue; + } + const size_t patterns = (totalSprites / sizes[frameId].area()); + for (size_t p = 0; p < patterns; ++p) { + adjustSpriteFrame(sizes, sprites, frameId, spriteIndex); + } + } +} + +void ThingType::adjustSpriteFrame(const std::vector& sizes, const std::vector& sprites, const size_t frameId, size_t& spriteIndex) +{ + for (int x = 0; x < m_size.width(); ++x) { + for (int y = 0; y < m_size.height(); ++y) { + if (x < sizes[frameId].width() && y < sizes[frameId].height()) { + m_spritesIndex.push_back(sprites[spriteIndex]); + ++spriteIndex; + continue; + } + m_spritesIndex.push_back(0); + } + } +} + uint32_t ThingType::getSpriteIndex(const int w, const int h, const int l, const int x, const int y, const int z, const int a) const { uint32_t index = ((((((a % m_animationPhases) @@ -1008,7 +1059,7 @@ ThingFlagAttr ThingType::thingAttrToThingFlagAttr(const ThingAttr attr) { } bool ThingType::isTall(const bool useRealSize) { return useRealSize ? getRealSize() > g_gameConfig.getSpriteSize() : getHeight() > 1; } -int ThingType::getAnimationPhases() { return m_animator ? m_animator->getAnimationPhases() : m_animationPhases; } +int ThingType::getAnimationPhase() const { return m_animator ? m_animator->getAnimationPhases() : m_animationPhases; } int ThingType::getMeanPrice() { static constexpr std::array, 3> forcedPrices = { { @@ -1130,6 +1181,7 @@ void ThingType::exportImage(const std::string& fileName) if (m_spritesIndex.empty()) throw Exception("cannot export thingtype without sprites"); + auto sprMgr = g_things.getSpriteManagerById(m_resourceId); const auto& image = std::make_shared(Size(g_gameConfig.getSpriteSize() * m_size.width() * m_layers * m_numPatternX, g_gameConfig.getSpriteSize() * m_size.height() * m_animationPhases * m_numPatternY * m_numPatternZ)); for (int z = 0; z < m_numPatternZ; ++z) { for (int y = 0; y < m_numPatternY; ++y) { @@ -1140,7 +1192,7 @@ void ThingType::exportImage(const std::string& fileName) for (int h = 0; h < m_size.height(); ++h) { image->blit(Point(g_gameConfig.getSpriteSize() * (m_size.width() - w - 1 + m_size.width() * x + m_size.width() * m_numPatternX * l), g_gameConfig.getSpriteSize() * (m_size.height() - h - 1 + m_size.height() * y + m_size.height() * m_numPatternY * a + m_size.height() * m_numPatternY * m_animationPhases * z)), - g_sprites.getSpriteImage(m_spritesIndex[getSpriteIndex(w, h, l, x, y, z, a)])); + sprMgr->getSpriteImageById(m_spritesIndex[getSpriteIndex(w, h, l, x, y, z, a)])); } } } diff --git a/src/client/thingtype.h b/src/client/thingtype.h index 2a41ab27df..c6a0c23d09 100644 --- a/src/client/thingtype.h +++ b/src/client/thingtype.h @@ -37,8 +37,8 @@ using namespace otclient::protobuf; class ThingType final : public LuaObject { public: - void unserializeAppearance(uint16_t clientId, ThingCategory category, const appearances::Appearance& appearance); - void unserialize(uint16_t clientId, ThingCategory category, const FileStreamPtr& fin); + void unserializeAppearance(uint16_t clientId, uint16_t resourceId, ProtobufSpriteManagerPtr spriteManager, ThingCategory category, const appearances::Appearance& appearance); + void unserialize(uint16_t clientId, uint16_t resourceId, ThingCategory category, const FileStreamPtr& fin); void unserializeOtml(const OTMLNodePtr& node); void applyAppearanceFlags(const appearances::AppearanceFlags& flags); @@ -52,6 +52,7 @@ class ThingType final : public LuaObject void drawWithFrameBuffer(const TexturePtr& texture, const Rect& screenRect, const Rect& textureRect, const Color& color); uint16_t getId() { return m_id; } + uint16_t getResourceId() { return m_resourceId; } ThingCategory getCategory() { return m_category; } bool isNull() { return m_null; } bool hasAttr(const ThingAttr attr) { return (m_flags & thingAttrToThingFlagAttr(attr)); } @@ -64,7 +65,7 @@ class ThingType final : public LuaObject int getNumPatternX() { return m_numPatternX; } int getNumPatternY() { return m_numPatternY; } int getNumPatternZ() { return m_numPatternZ; } - int getAnimationPhases(); + int getAnimationPhase() const; Animator* getAnimator() const { return m_animator; } Animator* getIdleAnimator() const { return m_idleAnimator; } @@ -93,56 +94,56 @@ class ThingType final : public LuaObject bool isTall(bool useRealSize = false); bool isSingleDimension() { return m_size.area() == 1; } - bool isGround() { return (m_flags & ThingFlagAttrGround); } - bool isGroundBorder() { return (m_flags & ThingFlagAttrGroundBorder); } - bool isOnBottom() { return (m_flags & ThingFlagAttrOnBottom); } - bool isOnTop() { return (m_flags & ThingFlagAttrOnTop); } + bool isGround() const { return (m_flags & ThingFlagAttrGround); } + bool isGroundBorder() const { return (m_flags & ThingFlagAttrGroundBorder); } + bool isOnBottom() const { return (m_flags & ThingFlagAttrOnBottom); } + bool isOnTop() const { return (m_flags & ThingFlagAttrOnTop); } bool isContainer() const { return (m_flags & ThingFlagAttrContainer); } - bool isStackable() { return (m_flags & ThingFlagAttrStackable); } - bool isForceUse() { return (m_flags & ThingFlagAttrForceUse); } - bool isMultiUse() { return (m_flags & ThingFlagAttrMultiUse); } - bool isWritable() { return (m_flags & ThingFlagAttrWritable); } - bool isChargeable() { return (m_flags & ThingFlagAttrChargeable); } - bool isWritableOnce() { return (m_flags & ThingFlagAttrWritableOnce); } - bool isFluidContainer() { return (m_flags & ThingFlagAttrFluidContainer); } - bool isSplash() { return (m_flags & ThingFlagAttrSplash); } - bool isNotWalkable() { return (m_flags & ThingFlagAttrNotWalkable); } - bool isNotMoveable() { return (m_flags & ThingFlagAttrNotMoveable); } - bool blockProjectile() { return (m_flags & ThingFlagAttrBlockProjectile); } - bool isNotPathable() { return (m_flags & ThingFlagAttrNotPathable); } - bool isPickupable() { return (m_flags & ThingFlagAttrPickupable); } - bool isHangable() { return (m_flags & ThingFlagAttrHangable); } - bool isHookSouth() { return (m_flags & ThingFlagAttrHookSouth); } - bool isHookEast() { return (m_flags & ThingFlagAttrHookEast); } - bool isRotateable() { return (m_flags & ThingFlagAttrRotateable); } - bool hasLight() { return (m_flags & ThingFlagAttrLight); } - bool isDontHide() { return (m_flags & ThingFlagAttrDontHide); } - bool isTranslucent() { return (m_flags & ThingFlagAttrTranslucent); } - bool hasDisplacement() { return (m_flags & ThingFlagAttrDisplacement); } - bool hasElevation() { return (m_flags & ThingFlagAttrElevation); } + bool isStackable() const { return (m_flags & ThingFlagAttrStackable); } + bool isForceUse() const { return (m_flags & ThingFlagAttrForceUse); } + bool isMultiUse() const { return (m_flags & ThingFlagAttrMultiUse); } + bool isWritable() const { return (m_flags & ThingFlagAttrWritable); } + bool isChargeable() const { return (m_flags & ThingFlagAttrChargeable); } + bool isWritableOnce() const { return (m_flags & ThingFlagAttrWritableOnce); } + bool isFluidContainer() const { return (m_flags & ThingFlagAttrFluidContainer); } + bool isSplash() const { return (m_flags & ThingFlagAttrSplash); } + bool isNotWalkable() const { return (m_flags & ThingFlagAttrNotWalkable); } + bool isNotMoveable() const { return (m_flags & ThingFlagAttrNotMoveable); } + bool blockProjectile() const { return (m_flags & ThingFlagAttrBlockProjectile); } + bool isNotPathable() const { return (m_flags & ThingFlagAttrNotPathable); } + bool isPickupable() const { return (m_flags & ThingFlagAttrPickupable); } + bool isHangable() const { return (m_flags & ThingFlagAttrHangable); } + bool isHookSouth() const { return (m_flags & ThingFlagAttrHookSouth); } + bool isHookEast() const { return (m_flags & ThingFlagAttrHookEast); } + bool isRotateable() const { return (m_flags & ThingFlagAttrRotateable); } + bool hasLight() const { return (m_flags & ThingFlagAttrLight); } + bool isDontHide() const { return (m_flags & ThingFlagAttrDontHide); } + bool isTranslucent() const { return (m_flags & ThingFlagAttrTranslucent); } + bool hasDisplacement() const { return (m_flags & ThingFlagAttrDisplacement); } + bool hasElevation() const { return (m_flags & ThingFlagAttrElevation); } bool hasFloorChange() const { return (m_flags & ThingFlagAttrFloorChange); } - bool isLyingCorpse() { return (m_flags & ThingFlagAttrLyingCorpse); } - bool isAnimateAlways() { return (m_flags & ThingFlagAttrAnimateAlways); } - bool hasMiniMapColor() { return (m_flags & ThingFlagAttrMinimapColor); } - bool hasLensHelp() { return (m_flags & ThingFlagAttrLensHelp); } - bool isFullGround() { return (m_flags & ThingFlagAttrFullGround); } - bool isIgnoreLook() { return (m_flags & ThingFlagAttrLook); } - bool isCloth() { return (m_flags & ThingFlagAttrCloth); } - bool isMarketable() { return (m_flags & ThingFlagAttrMarket); } - bool isUsable() { return (m_flags & ThingFlagAttrUsable); } - bool isWrapable() { return (m_flags & ThingFlagAttrWrapable); } - bool isUnwrapable() { return (m_flags & ThingFlagAttrUnwrapable); } - bool hasWearOut() { return (m_flags & ThingFlagAttrWearOut); } - bool hasClockExpire() { return (m_flags & ThingFlagAttrClockExpire); } - bool hasExpire() { return (m_flags & ThingFlagAttrExpire); } - bool hasExpireStop() { return (m_flags & ThingFlagAttrExpireStop); } - bool isPodium() { return (m_flags & ThingFlagAttrPodium); } - bool isTopEffect() { return (m_flags & ThingFlagAttrTopEffect); } - bool hasAction() { return (m_flags & ThingFlagAttrDefaultAction); } - bool isOpaque() { return m_opaque == 1; } - bool isDecoKit() { return (m_flags & ThingFlagAttrDecoKit); } + bool isLyingCorpse() const { return (m_flags & ThingFlagAttrLyingCorpse); } + bool isAnimateAlways() const { return (m_flags & ThingFlagAttrAnimateAlways); } + bool hasMiniMapColor() const { return (m_flags & ThingFlagAttrMinimapColor); } + bool hasLensHelp() const { return (m_flags & ThingFlagAttrLensHelp); } + bool isFullGround() const { return (m_flags & ThingFlagAttrFullGround); } + bool isIgnoreLook() const { return (m_flags & ThingFlagAttrLook); } + bool isCloth() const { return (m_flags & ThingFlagAttrCloth); } + bool isMarketable() const { return (m_flags & ThingFlagAttrMarket); } + bool isUsable() const { return (m_flags & ThingFlagAttrUsable); } + bool isWrapable() const { return (m_flags & ThingFlagAttrWrapable); } + bool isUnwrapable() const { return (m_flags & ThingFlagAttrUnwrapable); } + bool hasWearOut() const { return (m_flags & ThingFlagAttrWearOut); } + bool hasClockExpire() const { return (m_flags & ThingFlagAttrClockExpire); } + bool hasExpire() const { return (m_flags & ThingFlagAttrExpire); } + bool hasExpireStop() const { return (m_flags & ThingFlagAttrExpireStop); } + bool isPodium() const { return (m_flags & ThingFlagAttrPodium); } + bool isTopEffect() const { return (m_flags & ThingFlagAttrTopEffect); } + bool hasAction() const { return (m_flags & ThingFlagAttrDefaultAction); } + bool isOpaque() const { return m_opaque == 1; } + bool isDecoKit() const { return (m_flags & ThingFlagAttrDecoKit); } bool isLoading() const { return m_loading.load(std::memory_order_acquire); } - bool isAmmo() { return (m_flags & ThingFlagAttrAmmo); } + bool isAmmo() const { return (m_flags & ThingFlagAttrAmmo); } bool isItem() const { return m_category == ThingCategoryItem; } bool isEffect() const { return m_category == ThingCategoryEffect; } @@ -175,6 +176,16 @@ class ThingType final : public LuaObject private: static ThingFlagAttr thingAttrToThingFlagAttr(ThingAttr attr); static Size getBestTextureDimension(int w, int h, int count); + static void translateFlagId(const int version, int& flagId); + static void translateFlagId740(int& flagId); + static void translateFlagId755(int& flagId); + static void translateFlagId780(int& flagId); + static void translateFlagId1000(int& flagId); + void unserializeAttribute(int attr, const FileStreamPtr& fin); + void unserializeSpriteInfo(const FileStreamPtr& fin); + void unserializeFrameGroup(const FileStreamPtr& fin, const bool hasFrameGroups, std::vector& sizes, std::vector& total_sprites, int& totalSpritesCount); + void adjustSpriteSizes(const std::vector& sizes, const std::vector& total_sprites); + void adjustSpriteFrame(const std::vector& sizes, const std::vector& sprites, const size_t frameId, size_t& spriteIndex); void loadTexture(int animationPhase); @@ -220,6 +231,7 @@ class ThingType final : public LuaObject PLAYER_ACTION m_defaultAction{ 0 }; uint16_t m_id{ 0 }; + uint16_t m_resourceId{ 0 }; uint16_t m_groundSpeed{ 0 }; uint16_t m_maxTextLength{ 0 }; uint16_t m_upgradeClassification{ 0 }; diff --git a/src/client/thingtypemanager.cpp b/src/client/thingtypemanager.cpp index 6f561fab61..f1e5c3b440 100644 --- a/src/client/thingtypemanager.cpp +++ b/src/client/thingtypemanager.cpp @@ -26,7 +26,7 @@ #include #include "game.h" -#include "spriteappearances.h" +#include "spritemanager.h" #include "thingtype.h" #include "framework/core/filestream.h" #include "framework/core/resourcemanager.h" @@ -46,8 +46,7 @@ ThingTypeManager g_things; void ThingTypeManager::init() { m_nullThingType = std::make_shared(); - for (auto& m_thingType : m_thingTypes) - m_thingType.resize(1, m_nullThingType); + #ifdef FRAMEWORK_EDITOR m_nullItemType = std::make_shared(); m_itemTypes.resize(1, m_nullItemType); @@ -56,9 +55,6 @@ void ThingTypeManager::init() void ThingTypeManager::terminate() { - for (auto& m_thingType : m_thingTypes) - m_thingType.clear(); - m_nullThingType = nullptr; #ifdef FRAMEWORK_EDITOR @@ -68,46 +64,43 @@ void ThingTypeManager::terminate() #endif } -bool ThingTypeManager::loadDat(std::string file) +bool ThingTypeManager::loadDat(const std::string& file, const uint16_t resourceId) { - m_datLoaded = false; - m_datSignature = 0; - m_contentRevision = 0; - try { - file = g_resources.guessFilePath(file, "dat"); - - const auto& fin = g_resources.openFile(file); - fin->cache(true); + auto resource = AssetResource::Create(resourceId); + if (!resource->loadDat(file)) + return false; - m_datSignature = fin->getU32(); - m_contentRevision = static_cast(m_datSignature); + // resize vector before inserting if necessary + if (resourceId >= m_assetResources.size()) { + const auto newSize = static_cast(resourceId) + 1; + m_assetResources.resize(newSize); + m_spriteManagers.resize(newSize); + } - for (auto& thingType : m_thingTypes) { - const int count = fin->getU16() + 1; - thingType.clear(); - thingType.resize(count, m_nullThingType); - } + // insert into resource list + m_assetResources[resourceId] = std::move(resource); + m_spriteManagers[resourceId] = std::make_shared(); - for (int category = -1; ++category < ThingLastCategory;) { - const uint16_t firstId = category == ThingCategoryItem ? 100 : 1; + // notify Lua + // IMPORTANT: this may require moving so it's called only once + // or introducing a new method + g_lua.callGlobalField("g_things", "onLoadDat", file); - for (uint16_t id = firstId - 1, s = m_thingTypes[category].size(); ++id < s;) { - const auto& type = std::make_shared(); - type->unserialize(id, static_cast(category), fin); - m_thingTypes[category][id] = type; - } - } + return true; +} - m_datLoaded = true; - g_lua.callGlobalField("g_things", "onLoadDat", file); - return true; - } catch (const stdext::exception& e) { - g_logger.error("Failed to read dat '{}': {}'", file, e.what()); +bool ThingTypeManager::loadSpr(const std::string& file, const uint16_t resourceId) +{ + auto sprManager = dynamic_pointer_cast(getSpriteManagerById(resourceId)); + if (!sprManager) { + g_logger.error("Failed to read '{}': Sprite manager not initialized!'", file); return false; } + + return sprManager->loadSpr(file); } -bool ThingTypeManager::loadOtml(std::string file) +bool ThingTypeManager::loadOtml(std::string file, uint16_t resourceId) { try { file = g_resources.guessFilePath(file, "otml"); @@ -129,10 +122,10 @@ bool ThingTypeManager::loadOtml(std::string file) for (const auto& node2 : node->children()) { const auto id = stdext::safe_cast(node2->tag()); - const auto& type = getThingType(id, category); - if (!type) - throw OTMLException(node2, "thing not found"); - type->unserializeOtml(node2); + const auto& thing = getThingType(id, category, resourceId); + if (thing->getId() == 0) + throw OTMLException(node2, "thing not found, using "); + thing->unserializeOtml(node2); } } return true; @@ -142,86 +135,26 @@ bool ThingTypeManager::loadOtml(std::string file) } } -bool ThingTypeManager::loadAppearances(const std::string& file) +bool ThingTypeManager::loadAppearances(const std::string& file, uint16_t resourceId) { - try { - if (!g_game.getFeature(Otc::GameLoadSprInsteadProtobuf)) { - g_spriteAppearances.unload(); - int spritesCount = 0; - std::string appearancesFile; - json document = json::parse(g_resources.readFileContents(g_resources.resolvePath(g_resources.guessFilePath(file + "catalog-content", "json")))); - for (const auto& obj : document) { - const auto& type = obj["type"]; - if (type == "appearances") { - appearancesFile = obj["file"]; - } else if (type == "sprite") { - int lastSpriteId = obj["lastspriteid"].get(); - const auto& sheet = std::make_shared(obj["firstspriteid"].get(), lastSpriteId, static_cast(obj["spritetype"].get()), obj["file"].get()); - const int spritesPerSheet = sheet->getSpritesPerSheet(); - const int maxSpriteId = sheet->firstId + spritesPerSheet - 1; - if (lastSpriteId > maxSpriteId) { - g_logger.debug("Sprite sheet '{}' lastspriteid {} exceeds capacity {}, clamping to {}", sheet->file, lastSpriteId, maxSpriteId, maxSpriteId); - lastSpriteId = maxSpriteId; - sheet->lastId = maxSpriteId; - } - g_spriteAppearances.addSpriteSheet(sheet); - spritesCount = std::max(spritesCount, lastSpriteId); - } - } - g_spriteAppearances.setSpritesCount(spritesCount + 1); - g_spriteAppearances.setPath(file); - // load appearances.dat - std::stringstream fin; - g_resources.readFileStream(g_resources.resolvePath(fmt::format("{}{}", file, appearancesFile)), fin); - auto appearancesLib = appearances::Appearances(); - if (!appearancesLib.ParseFromIstream(&fin)) { - throw stdext::exception("Couldn't parse appearances lib."); - } - for (int category = ThingCategoryItem; category < ThingLastCategory; ++category) { - const google::protobuf::RepeatedPtrField* appearances = nullptr; - switch (category) { - case ThingCategoryItem: appearances = &appearancesLib.object(); break; - case ThingCategoryCreature: appearances = &appearancesLib.outfit(); break; - case ThingCategoryEffect: appearances = &appearancesLib.effect(); break; - case ThingCategoryMissile: appearances = &appearancesLib.missile(); break; - default: return false; - } - // fix for custom asserts, where ids are not sorted. - uint32_t lastAppearanceId = 0; - for (const auto& appearance : *appearances) { - if (appearance.id() > lastAppearanceId) - lastAppearanceId = appearance.id(); - } - auto& things = m_thingTypes[category]; - things.clear(); - things.resize(lastAppearanceId + 1, m_nullThingType); - for (const auto& appearance : *appearances) { - const auto& type = std::make_shared(); - const uint16_t id = appearance.id(); - type->unserializeAppearance(id, static_cast(category), appearance); - m_thingTypes[category][id] = type; - } - } - m_datLoaded = true; - } else { - std::stringstream datFileStream; - auto appearancesLib = appearances::Appearances(); - g_resources.readFileStream(g_resources.resolvePath(g_resources.guessFilePath(file, "dat")), datFileStream); - if (!appearancesLib.ParseFromIstream(&datFileStream)) { - throw stdext::exception("Couldn't parse appearances.dat."); - } - for (const auto& appearance : appearancesLib.object()) { - const uint16_t id = appearance.id(); - if (auto* type = getRawThingType(id, ThingCategoryItem)) { - type->applyAppearanceFlags(appearance.flags()); - } - } - } - return true; - } catch (const std::exception& e) { - g_logger.error("Failed to load '{}' (Appearances): {}", file, e.what()); + auto resource = AssetResource::Create(resourceId); + auto sprManager = resource->loadAppearances(file); + if (!sprManager) { return false; } + + // resize vector before inserting if necessary + if (resourceId >= m_assetResources.size()) { + const auto newSize = static_cast(resourceId) + 1; + m_assetResources.resize(newSize); + m_spriteManagers.resize(newSize); + } + + // insert into resource list + m_assetResources[resourceId] = std::move(resource); + m_spriteManagers[resourceId] = std::move(sprManager); + + return true; } namespace { @@ -235,22 +168,25 @@ namespace { otcRaceType.name = protobufRace.name(); otcRaceType.boss = boss; - Outfit otcOutfit; const auto& protobufOutfit = protobufRace.outfit(); - if (protobufOutfit.lookitem() != 0) { - otcOutfit.setAuxId(static_cast(protobufOutfit.lookitem())); - } else { - otcOutfit.setId(static_cast(protobufOutfit.looktype())); - otcOutfit.setAddons(static_cast(protobufOutfit.lookaddons())); - if (protobufOutfit.has_colors()) { - const auto& pbColors = protobufOutfit.colors(); - otcOutfit.setHead(static_cast(pbColors.head())); - otcOutfit.setBody(static_cast(pbColors.body())); - otcOutfit.setLegs(static_cast(pbColors.legs())); - otcOutfit.setFeet(static_cast(pbColors.feet())); - } + + ColorOutfit parsedOutfit; + parsedOutfit.type = static_cast(protobufOutfit.looktype()); + parsedOutfit.typeEx = static_cast(protobufOutfit.lookitem()); + + if (protobufOutfit.has_colors()) { + const auto& pbColors = protobufOutfit.colors(); + parsedOutfit.head = static_cast(pbColors.head()); + parsedOutfit.body = static_cast(pbColors.body()); + parsedOutfit.legs = static_cast(pbColors.legs()); + parsedOutfit.feet = static_cast(pbColors.feet()); + parsedOutfit.applyColors(); } + Outfit otcOutfit; + otcOutfit.applyOutfit(parsedOutfit); + otcOutfit.setAddons(static_cast(protobufOutfit.lookaddons())); + otcRaceType.outfit = otcOutfit; otcRaceList.emplace_back(otcRaceType); } @@ -300,37 +236,178 @@ bool ThingTypeManager::loadStaticData(const std::string& file) return false; } -const ThingTypeList& ThingTypeManager::getThingTypes(const ThingCategory category) +PackInfoResourceList ThingTypeManager::decodePackInfo(const std::string& file) { - if (category < ThingLastCategory) - return m_thingTypes[category]; + PackInfoResourceList resourceList; - throw Exception("invalid thing type category {}", category); + try { + pugi::xml_document doc; + if (pugi::xml_parse_result result = doc.load_string( + g_resources.readFileContents( + g_resources.resolvePath( + g_resources.guessFilePath(file + "packinfo", "xml") + ) + ).c_str() + ); !result) { + throw Exception("cannot load '{}: '{}'", file, result.description()); + } + + pugi::xml_node root = doc.child("resources"); + if (root.empty()) + throw Exception("malformed packinfo file"); + + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) { + if (node.name() != std::string("resource")) + throw Exception("invalid resource node"); + + AssetResourceInfo res = {}; + res.resourceId = node.attribute("id").as_int(); + res.clientVersionId = node.attribute("version").as_int(); + res.dir = node.attribute("dir").as_string(); + + resourceList.push_back(res); + } + + g_logger.debug("Packinfo read successfully."); + } catch (const std::exception& e) { + g_logger.error("Failed to load '{}': {}", file, e.what()); + } + + return resourceList; } -const ThingTypePtr& ThingTypeManager::getThingType(const uint16_t id, const ThingCategory category) +const ThingTypeList& ThingTypeManager::getThingTypes(const ThingCategory category, uint16_t resourceId) { - if (category >= ThingLastCategory || id >= m_thingTypes[category].size()) { - g_logger.error("invalid thing type client id {} in category {}", id, static_cast(category)); - return m_nullThingType; + auto res = getResourceById(resourceId); + if (!res) { + throw Exception("invalid resource id {}", resourceId); } - return m_thingTypes[category][id]; + + return res->getThingTypes(category); } -ThingType* ThingTypeManager::getRawThingType(uint16_t id, ThingCategory category) { - if (category >= ThingLastCategory || id >= m_thingTypes[category].size()) { - g_logger.error("invalid thing type client id {} in category {}", id, static_cast(category)); +AssetResourcePtr ThingTypeManager::getResourceById(const uint16_t resourceId) const +{ + if (resourceId >= m_assetResources.size()) + return nullptr; + + return m_assetResources[resourceId]; +} + +SpriteManagerPtr ThingTypeManager::getSpriteManagerById(const uint16_t resourceId) const +{ + if (resourceId >= m_spriteManagers.size()) + return nullptr; + + return m_spriteManagers[resourceId]; +} + +uint32_t ThingTypeManager::getSprSignature(const uint16_t resourceId) const +{ + auto res = getSpriteManagerById(resourceId); + return res ? res->getSignature() : 0; +} + +uint32_t ThingTypeManager::getDatSignature(const uint16_t resourceId) const +{ + auto res = getResourceById(resourceId); + return res ? res->getDatSignature() : 0; +} + +uint16_t ThingTypeManager::getContentRevision(const uint16_t resourceId) const +{ + auto res = getResourceById(resourceId); + return res ? res->getContentRevision() : 0; +} + +ImagePtr ThingTypeManager::getSpriteImage(int id, uint16_t resourceId, bool& isLoading) +{ + auto res = getSpriteManagerById(resourceId); + if (!res) return nullptr; + + return res->getSpriteImage(id, isLoading); +} + +bool ThingTypeManager::isDatLoaded() +{ + // return the state of the first resource encountered + for (const auto& resource : m_assetResources) { + if (resource) { + return m_assetResources.front()->isDatLoaded(); + } } - return m_thingTypes[category][id].get(); + + // no resources allocated + return false; +} + +bool ThingTypeManager::isValidDatId(const uint16_t id, const ThingCategory category, const uint16_t resourceId) const +{ + auto res = getResourceById(resourceId); + return res ? res->isValidDatId(id, category) : false; +} + +void ThingTypeManager::reloadSprites() +{ + for (const auto& sprManager : m_spriteManagers) + if (sprManager) + sprManager->reload(); +} + +bool ThingTypeManager::isSprLoaded(uint16_t resourceId) +{ + auto res = getSpriteManagerById(resourceId); + if (!res) + return false; + + return res->isLoaded(); +} + +bool ThingTypeManager::isUsingProtobuf(uint16_t resourceId) +{ + auto res = getSpriteManagerById(resourceId); + if (!res) + return false; + + return res->isProtobuf(); +} + +const ThingTypePtr& ThingTypeManager::getThingType(const uint16_t id, const ThingCategory category, const uint16_t resourceId) const +{ + auto res = getResourceById(resourceId); + if (!res) { + g_logger.error("failed to get raw thing type {} in category {}: resource {} not loaded", id, static_cast(category), resourceId); + return getNullThingType(); + } + + return res->getThingType(id, category); +} + +ThingType* ThingTypeManager::getRawThingType(uint16_t id, ThingCategory category, uint16_t resourceId) const +{ + auto res = getResourceById(resourceId); + if (!res) { + g_logger.error("failed to get raw thing type {} in category {}: resource {} not loaded", id, static_cast(category), resourceId); + return nullptr; + } + + return res->getRawThingType(id, category); } ThingTypeList ThingTypeManager::findThingTypeByAttr(const ThingAttr attr, const ThingCategory category) { ThingTypeList ret; - for (const auto& type : m_thingTypes[category]) - if (type->hasAttr(attr)) - ret.emplace_back(type); + + // read items from all resources + // (this is for displaying them in market or cyclopedia) + for (const auto& resource : m_assetResources) { + if (!resource) + continue; + + resource->findThingTypesByAttr(attr, category, ret); + } + return ret; } @@ -474,37 +551,19 @@ ItemTypeList ThingTypeManager::findItemTypeByCategory(ItemCategory category) return ret; } -void ThingTypeManager::saveDat(const std::string& fileName) +void ThingTypeManager::saveDat(const std::string& fileName, uint16_t resourceId) { - if (!m_datLoaded) - throw Exception("failed to save, dat is not loaded"); - - try { - const auto& fin = g_resources.createFile(fileName); - if (!fin) - throw Exception("failed to open file '{}' for write", fileName); - - fin->cache(); + auto res = g_things.getResourceById(resourceId); + if (!res) + throw Exception("failed to save, resource not found"); - fin->addU32(m_datSignature); - - for (const auto& m_thingType : m_thingTypes) - fin->addU16(m_thingType.size() - 1); - - for (int category = 0; category < ThingLastCategory; ++category) { - uint16_t firstId = 1; - if (category == ThingCategoryItem) - firstId = 100; - - for (uint16_t id = firstId; id < m_thingTypes[category].size(); ++id) - m_thingTypes[category][id]->serialize(fin); - } + res->saveDat(fileName); +} - fin->flush(); - fin->close(); - } catch (const std::exception& e) { - g_logger.error("Failed to save '{}': {}", fileName, e.what()); - } +void ThingTypeManager::saveSpr(const std::string& fileName, uint16_t resourceId) +{ + if (auto res = g_things.getSpriteManagerById(resourceId)) + res->saveSpr(fileName); } void ThingTypeManager::loadOtb(const std::string& file) @@ -609,4 +668,217 @@ void ThingTypeManager::loadXml(const std::string& file) #endif +bool AssetResource::loadDat(const std::string& file) +{ + if (m_datLoaded) { + g_logger.error("Failed to read dat '{}': Resource already loaded!", file); + return false; + } + + try { + auto fin = g_resources.openFile(file); + fin->cache(true); + + m_datSignature = fin->getU32(); + m_contentRevision = static_cast(m_datSignature); + + for (auto& thingTypeList : m_thingTypes) { + const uint16_t count = fin->getU16() + 1; + thingTypeList.clear(); + thingTypeList.resize(count); + } + + for (int category = 0; category < ThingLastCategory; ++category) { + const uint16_t firstId = (category == ThingCategoryItem ? 100 : 1); + auto& thingList = m_thingTypes[category]; + + for (uint16_t id = firstId; id < thingList.size(); ++id) { + auto type = std::make_shared(); + type->unserialize(id, m_resourceId, static_cast(category), fin); + thingList[id] = std::move(type); + } + } + + m_datLoaded = true; + return true; + + } catch (const stdext::exception& e) { + g_logger.error("Failed to read dat '{}': {}", file, e.what()); + return false; + } +} + +#ifdef FRAMEWORK_EDITOR +void AssetResource::saveDat(const std::string& file) +{ + try { + if (!isDatLoaded()) { + throw Exception("failed to save {}, dat is not loaded", file); + } + + const auto& fin = g_resources.createFile(file); + if (!fin) + throw Exception("failed to open file '{}' for write", file); + + fin->cache(); + + fin->addU32(m_datSignature); + + for (const auto& m_thingType : m_thingTypes) + fin->addU16(m_thingType.size() - 1); + + for (int category = 0; category < ThingLastCategory; ++category) { + uint16_t firstId = 1; + if (category == ThingCategoryItem) + firstId = 100; + + for (uint16_t id = firstId; id < m_thingTypes[category].size(); ++id) + m_thingTypes[category][id]->serialize(fin); + } + + fin->flush(); + fin->close(); + } catch (const std::exception& e) { + g_logger.error("Failed to save '{}': {}", file, e.what()); + } +} +#endif + +SpriteManagerPtr AssetResource::loadAppearances(const std::string& file) +{ + if (m_datLoaded) { + g_logger.error("Failed to read '{}': Resource already loaded!", file); + return nullptr; + } + + try { + int spritesCount = 0; + std::string appearancesFile; + + json document = json::parse( + g_resources.readFileContents( + g_resources.resolvePath( + g_resources.guessFilePath(file + "catalog-content", "json") + ) + ) + ); + + auto protoSprites = std::make_shared(); + + for (const auto& obj : document) { + const auto& type = obj["type"]; + + if (type == "appearances") { + appearancesFile = obj["file"]; + } else if (type == "sprite") { + int lastSpriteId = obj["lastspriteid"].get(); + auto sheet = std::make_shared( + obj["firstspriteid"].get(), + lastSpriteId, + static_cast(obj["spritetype"].get()), + obj["file"].get() + ); + + const int maxSpriteId = sheet->firstId + sheet->getSpritesPerSheet() - 1; + if (lastSpriteId > maxSpriteId) { + lastSpriteId = maxSpriteId; + sheet->lastId = maxSpriteId; + } + + protoSprites->addSpriteSheet(sheet); + spritesCount = std::max(spritesCount, lastSpriteId); + } + } + + protoSprites->setSpritesCount(spritesCount + 1); + protoSprites->setPath(file); + + // load appearances.dat + std::stringstream fin; + g_resources.readFileStream( + g_resources.resolvePath(fmt::format("{}{}", file, appearancesFile)), + fin + ); + + appearances::Appearances appearancesLib; + if (!appearancesLib.ParseFromIstream(&fin)) + throw stdext::exception("Couldn't parse appearances lib."); + + auto& nullThing = g_things.getNullThingType(); + for (int category = ThingCategoryItem; category < ThingLastCategory; ++category) { + const google::protobuf::RepeatedPtrField* appearances = nullptr; + switch (category) { + case ThingCategoryItem: appearances = &appearancesLib.object(); break; + case ThingCategoryCreature: appearances = &appearancesLib.outfit(); break; + case ThingCategoryEffect: appearances = &appearancesLib.effect(); break; + case ThingCategoryMissile: appearances = &appearancesLib.missile(); break; + default: return nullptr; + } + // fix for custom assets, in which the ids are not sorted. + uint32_t lastAppearanceId = 0; + for (const auto& appearance : *appearances) { + if (appearance.id() > lastAppearanceId) + lastAppearanceId = appearance.id(); + } + auto& things = m_thingTypes[category]; + things.clear(); + things.resize(lastAppearanceId + 1, nullThing); + for (const auto& appearance : *appearances) { + const auto& type = std::make_shared(); + const uint16_t id = appearance.id(); + type->unserializeAppearance(id, m_resourceId, protoSprites, static_cast(category), appearance); + m_thingTypes[category][id] = type; + } + } + + m_datLoaded = true; + return protoSprites; + } catch (const std::exception& e) { + g_logger.error("Failed to load appearances '{}': {}", file, e.what()); + return nullptr; + } +} + +const ThingTypeList& AssetResource::getThingTypes(const ThingCategory category) +{ + if (category < ThingLastCategory) + return m_thingTypes[category]; + + throw Exception("invalid thing type category {}", category); +} + +const ThingTypePtr& AssetResource::getThingType(const uint16_t id, const ThingCategory category) +{ + if (category >= ThingLastCategory || id >= m_thingTypes[category].size()) { + g_logger.error("invalid thing type client id {} in category {}", id, static_cast(category)); + return g_things.getNullThingType(); + } + return m_thingTypes[category][id]; +} + +ThingType* AssetResource::getRawThingType(uint16_t id, ThingCategory category) +{ + if (category >= ThingLastCategory || id >= m_thingTypes[category].size()) { + g_logger.error("invalid thing type client id {} in category {}", id, static_cast(category)); + return nullptr; + } + return m_thingTypes[category][id].get(); +} + +void AssetResource::findThingTypesByAttr(ThingAttr attr, ThingCategory category, ThingTypeList& out) const +{ + if (!m_datLoaded || category >= ThingLastCategory) + return; + + const auto& nullThing = g_things.getNullThingType(); + + for (const auto& type : m_thingTypes[category]) { + if (!type || type == nullThing) + continue; + + if (type->hasAttr(attr)) + out.emplace_back(type); + } +} + /* vim: set ts=4 sw=4 et: */ \ No newline at end of file diff --git a/src/client/thingtypemanager.h b/src/client/thingtypemanager.h index a072511dfb..0561670f18 100644 --- a/src/client/thingtypemanager.h +++ b/src/client/thingtypemanager.h @@ -23,32 +23,50 @@ #pragma once #include "staticdata.h" +#include "spritemanager.h" using RaceList = std::vector; static const RaceType emptyRaceType{}; +struct AssetResourceInfo +{ + uint16_t resourceId{ 0 }; + int clientVersionId{ 0 }; + std::string dir; +}; + class ThingTypeManager { public: + ThingTypeManager() = default; + ~ThingTypeManager() = default; + + // non-copyable + ThingTypeManager(const ThingTypeManager&) = delete; + ThingTypeManager& operator=(const ThingTypeManager&) = delete; + void init(); void terminate(); - bool loadDat(std::string file); - bool loadOtml(std::string file); - bool loadAppearances(const std::string& file); + bool loadDat(const std::string& file, const uint16_t resourceId); + bool loadSpr(const std::string& file, const uint16_t resourceId); + bool loadOtml(std::string file, uint16_t resourceId); + bool loadAppearances(const std::string& file, const uint16_t resourceId); bool loadStaticData(const std::string& file); + PackInfoResourceList decodePackInfo(const std::string& file); #ifdef FRAMEWORK_EDITOR void parseItemType(uint16_t id, pugi::xml_node node); void loadOtb(const std::string& file); void loadXml(const std::string& file); - void saveDat(const std::string& fileName); - uint32_t getOtbMajorVersion() { return m_otbMajorVersion; } - uint32_t getOtbMinorVersion() { return m_otbMinorVersion; } - bool isXmlLoaded() { return m_xmlLoaded; } - bool isOtbLoaded() { return m_otbLoaded; } - bool isValidOtbId(uint16_t id) { return id >= 1 && id < m_itemTypes.size(); } - const ItemTypeList& getItemTypes() { return m_itemTypes; } + void saveDat(const std::string& fileName, uint16_t resourceId = 0); + void saveSpr(const std::string& fileName, uint16_t resourceId = 0); + uint32_t getOtbMajorVersion() const { return m_otbMajorVersion; } + uint32_t getOtbMinorVersion() const { return m_otbMinorVersion; } + bool isXmlLoaded() const { return m_xmlLoaded; } + bool isOtbLoaded() const { return m_otbLoaded; } + bool isValidOtbId(uint16_t id) const { return id >= 1 && id < m_itemTypes.size(); } + const ItemTypeList& getItemTypes() const { return m_itemTypes; } void addItemType(const ItemTypePtr& itemType); const ItemTypePtr& findItemTypeByClientId(uint16_t id); const ItemTypePtr& findItemTypeByName(const std::string& name); @@ -65,29 +83,45 @@ class ThingTypeManager const RaceType& getRaceData(uint32_t raceId); RaceList getRacesByName(const std::string& searchString); - const ThingTypePtr& getNullThingType() { return m_nullThingType; } + const ThingTypePtr& getNullThingType() const { return m_nullThingType; } - const ThingTypePtr& getThingType(uint16_t id, ThingCategory category); - ThingType* getRawThingType(uint16_t id, ThingCategory category); + const ThingTypePtr& getThingType(uint16_t id, ThingCategory category, const uint16_t resourceId) const; + ThingType* getRawThingType(uint16_t id, ThingCategory category, uint16_t resourceId) const; - const ThingTypeList& getThingTypes(ThingCategory category); + const ThingTypeList& getThingTypes(ThingCategory category, uint16_t resourceId = 0); - uint32_t getDatSignature() { return m_datSignature; } - uint16_t getContentRevision() { return m_contentRevision; } + AssetResourcePtr getResourceById(const uint16_t resourceId) const; + SpriteManagerPtr getSpriteManagerById(const uint16_t resourceId) const; + size_t getResourcesCount() const { return m_assetResources.size(); } + uint32_t getSprSignature(const uint16_t resourceId = 0) const; + uint32_t getDatSignature(const uint16_t resourceId = 0) const; + uint16_t getContentRevision(const uint16_t resourceId = 0) const; - bool isDatLoaded() { return m_datLoaded; } - bool isValidDatId(const uint16_t id, const ThingCategory category) const { return category < ThingLastCategory && id >= 1 && id < m_thingTypes[category].size(); } + ImagePtr getSpriteImage(int id, uint16_t resourceId, bool& isLoading); + + bool isDatLoaded(); + bool isValidDatId(const uint16_t id, const ThingCategory category, const uint16_t resourceId) const; + + void reloadSprites(); + bool isSprLoaded(uint16_t resourceId); + bool isUsingProtobuf(uint16_t resourceId); private: - ThingTypeList m_thingTypes[ThingLastCategory]; - RaceList m_monsterRaces; - ThingTypePtr m_nullThingType; + // loaded spr/dat/assets storage + // resources 0 .. n + AssetResourceList m_assetResources; - bool m_datLoaded{ false }; + // loaded sprite managers + // contains dedicated sprite manager for each loaded resource + // if assets: uses ProtobufSpriteManager object + // if spr/dat: uses LegacySpriteManager object + SpriteManagerList m_spriteManagers; - uint32_t m_datSignature{ 0 }; - uint16_t m_contentRevision{ 0 }; + ThingTypePtr m_nullThingType; + + // to do: m_resourceId support + RaceList m_monsterRaces; #ifdef FRAMEWORK_EDITOR ItemTypePtr m_nullItemType; @@ -98,8 +132,60 @@ class ThingTypeManager bool m_xmlLoaded{ false }; bool m_otbLoaded{ false }; #endif - - friend class GarbageCollection; }; extern ThingTypeManager g_things; + +class AssetResource : public std::enable_shared_from_this +{ +public: + // AssetResource::Create(resourceId) + static std::shared_ptr Create(uint16_t resourceId) { + // Using 'new' here intentionally due to private constructor + return std::shared_ptr(new AssetResource(resourceId)); + } + + ~AssetResource() = default; + + // non-copyable + AssetResource(const AssetResource&) = delete; + AssetResource& operator=(const AssetResource&) = delete; + + uint16_t getId() const { return m_resourceId; } + + uint32_t getDatSignature() const { return m_datSignature; } + uint16_t getContentRevision() const { return m_contentRevision; } + + const ThingTypeList& getThingTypes(const ThingCategory category); + + const ThingTypePtr& getThingType(uint16_t id, ThingCategory category); + ThingType* getRawThingType(uint16_t id, ThingCategory category); + + void findThingTypesByAttr(ThingAttr attr, ThingCategory category, ThingTypeList& out) const; + + // spr/dat + bool loadDat(const std::string& file); + bool isDatLoaded() const { return m_datLoaded; } + bool isValidDatId(const uint16_t id, const ThingCategory category) const { return category < ThingLastCategory && id >= 1 && id < m_thingTypes[category].size(); } + +#ifdef FRAMEWORK_EDITOR + void saveDat(const std::string& file); +#endif + + // protobuf assets + SpriteManagerPtr loadAppearances(const std::string& file); + +private: + explicit AssetResource(uint16_t resourceId) : m_resourceId(resourceId) {} + + ThingTypeList m_thingTypes[ThingLastCategory]; + + uint32_t m_datSignature{ 0 }; + uint16_t m_contentRevision{ 0 }; + uint16_t m_clientVersion{ 0 }; + uint16_t m_resourceId{ 0 }; + + bool m_datLoaded{ false }; + + friend class GarbageCollection; +}; diff --git a/src/client/uicreature.cpp b/src/client/uicreature.cpp index 8d29f95cbe..c281e62493 100644 --- a/src/client/uicreature.cpp +++ b/src/client/uicreature.cpp @@ -81,28 +81,47 @@ Outfit UICreature::getOutfit() { if (!m_creature) setOutfit({}); return m_creatu void UICreature::onStyleApply(const std::string_view styleName, const OTMLNodePtr& styleNode) { + ColorOutfit base; + bool needUpdateOutfit = false; + + auto outfit = getOutfit(); for (const auto& node : styleNode->children()) { - if (node->tag() == "creature-center") { + const std::string tag = node->tag(); + if (tag == "creature-center") { m_center = node->value(); - } else if (node->tag() == "creature-size") { + } else if (tag == "creature-size") { setCreatureSize(node->value()); - } else if (node->tag() == "outfit-id") { - auto outfit = getOutfit(); + } else if (tag == "outfit-id") { outfit.setCategory(ThingCategoryCreature); - outfit.setId(node->value()); - setOutfit(outfit); - } else if (node->tag() == "outfit-head") { - getOutfit().setHead(node->value()); - } else if (node->tag() == "outfit-body") { - getOutfit().setBody(node->value()); - } else if (node->tag() == "outfit-legs") { - getOutfit().setLegs(node->value()); - } else if (node->tag() == "outfit-feet") { - getOutfit().setFeet(node->value()); - } else if (node->tag() == "outfit-direction") { + base.type = node->value(); + needUpdateOutfit = true; + } else if (tag == "outfit-resource-id") { + base.resourceId = node->value(); + needUpdateOutfit = true; + } else if (tag == "outfit-head") { + base.head = node->value(); + needUpdateOutfit = true; + } else if (tag == "outfit-body") { + base.body = node->value(); + needUpdateOutfit = true; + } else if (tag == "outfit-legs") { + base.legs = node->value(); + needUpdateOutfit = true; + } else if (tag == "outfit-feet") { + base.feet = node->value(); + needUpdateOutfit = true; + } else if (tag == "outfit-direction") { m_direction = static_cast(node->value()); + needUpdateOutfit = true; } } + + if (needUpdateOutfit) { + base.applyColors(); + outfit.applyOutfit(base); + setOutfit(outfit); + } + UIWidget::onStyleApply(styleName, styleNode); } diff --git a/src/client/uieffect.cpp b/src/client/uieffect.cpp index d8671bfc53..539ddc7ea3 100644 --- a/src/client/uieffect.cpp +++ b/src/client/uieffect.cpp @@ -56,14 +56,14 @@ void UIEffect::drawSelf(const DrawPoolType drawPane) drawText(m_rect); } -void UIEffect::setEffectId(const int id) +void UIEffect::setEffectId(const int id, uint16_t resourceId) { if (id == 0) m_effect = nullptr; else { if (!m_effect) m_effect = std::make_shared(); - m_effect->setId(id); + m_effect->setId(id, resourceId); if (m_effect) m_effect->setShader(m_shaderName); } @@ -76,17 +76,29 @@ void UIEffect::setEffect(const EffectPtr& e) void UIEffect::onStyleApply(const std::string_view styleName, const OTMLNodePtr& styleNode) { + uint16_t effectId = 0; + uint16_t effectResourceId = 0; + bool needUpdate = false; + for (const auto& node : styleNode->children()) { - if (node->tag() == "effect-id") - setEffectId(node->value()); - else if (node->tag() == "effect-visible") + const std::string tag = node->tag(); + if (tag == "effect-id") { + effectId = node->value(); + needUpdate = true; + } else if (tag == "effect-resource-id") { + effectResourceId = node->value(); + needUpdate = true; + } else if (tag == "effect-visible") setEffectVisible(node->value()); - else if (node->tag() == "virtual") + else if (tag == "virtual") setVirtual(node->value()); - else if (node->tag() == "show-id") + else if (tag == "show-id") m_showId = node->value(); } + if (needUpdate) + setEffectId(effectId, effectResourceId); + UIWidget::onStyleApply(styleName, styleNode); } diff --git a/src/client/uieffect.h b/src/client/uieffect.h index 3ee69901f7..f2a6f529d8 100644 --- a/src/client/uieffect.h +++ b/src/client/uieffect.h @@ -31,7 +31,7 @@ class UIEffect final : public UIWidget UIEffect(); void drawSelf(DrawPoolType drawPane) override; - void setEffectId(int id); + void setEffectId(int id, uint16_t resourceId = 0); void setEffectVisible(const bool visible) { m_effectVisible = visible; } void setEffect(const EffectPtr& effect); void setVirtual(const bool virt) { m_virtual = virt; } diff --git a/src/client/uiitem.cpp b/src/client/uiitem.cpp index e9bbb03d8d..283888e4cf 100644 --- a/src/client/uiitem.cpp +++ b/src/client/uiitem.cpp @@ -44,8 +44,8 @@ void UIItem::drawSelf(const DrawPoolType drawPane) drawImage(m_rect); if (m_itemVisible && m_item) { - if (m_item->getClientId() != m_itemId) { - m_item->setId(m_itemId); + if (m_item->getClientId() != m_itemId || m_item->getResourceId() != m_resourceId) { + m_item->setId(m_itemId, m_resourceId); } const int exactSize = std::max(g_gameConfig.getSpriteSize(), m_item->getExactSize()); @@ -73,16 +73,17 @@ void UIItem::drawSelf(const DrawPoolType drawPane) drawText(m_rect); } -void UIItem::setItemId(const int id) +void UIItem::setItemId(const int id, uint16_t resourceId) { m_itemId = id; + m_resourceId = resourceId; if (id == 0) m_item = nullptr; else if (m_item) - m_item->setId(id); + m_item->setId(id, resourceId); else - m_item = Item::create(id); + m_item = Item::create(id, resourceId); if (m_item) m_item->setShader(m_shaderName); @@ -115,21 +116,33 @@ void UIItem::setItem(const ItemPtr& item) void UIItem::onStyleApply(const std::string_view styleName, const OTMLNodePtr& styleNode) { + uint16_t itemId = 0; + uint16_t resourceId = 0; + bool needUpdateItem = false; + for (const auto& node : styleNode->children()) { - if (node->tag() == "item-id") - setItemId(node->value()); - else if (node->tag() == "item-count") + const std::string tag = node->tag(); + if (tag == "item-id") { + itemId = node->value(); + needUpdateItem = true; + } else if (tag == "item-resource-id") { + resourceId = node->value(); + needUpdateItem = true; + } else if (tag == "item-count") setItemCount(node->value()); - else if (node->tag() == "item-visible") + else if (tag == "item-visible") setItemVisible(node->value()); - else if (node->tag() == "virtual") + else if (tag == "virtual") setVirtual(node->value()); - else if (node->tag() == "show-id") + else if (tag == "show-id") m_showId = node->value(); - else if (node->tag() == "always-show-count") + else if (tag == "always-show-count") m_alwaysShowCount = node->value(); } + if (needUpdateItem) + setItemId(itemId, resourceId); + UIWidget::onStyleApply(styleName, styleNode); } diff --git a/src/client/uiitem.h b/src/client/uiitem.h index a13b08e5b9..cb0c0299e0 100644 --- a/src/client/uiitem.h +++ b/src/client/uiitem.h @@ -31,7 +31,7 @@ class UIItem final : public UIWidget UIItem(); void drawSelf(DrawPoolType drawPane) override; - void setItemId(int id); + void setItemId(int id, uint16_t resourceId = 0); void setItemCount(int count); void setItemSubType(int subType); void setItemVisible(const bool visible) { m_itemVisible = visible; } @@ -57,6 +57,7 @@ class UIItem final : public UIWidget std::string m_shaderName; ItemPtr m_item; uint32_t m_itemId{ 0 }; + uint16_t m_resourceId{ 0 }; bool m_virtual{ false }; bool m_showId{ false }; bool m_itemVisible{ true }; diff --git a/src/client/uimissile.cpp b/src/client/uimissile.cpp index 08e787a218..eedb6b0ca7 100644 --- a/src/client/uimissile.cpp +++ b/src/client/uimissile.cpp @@ -57,14 +57,14 @@ void UIMissile::drawSelf(const DrawPoolType drawPane) drawText(m_rect); } -void UIMissile::setMissileId(const int id) +void UIMissile::setMissileId(const int id, uint16_t resourceId) { if (id == 0) m_missile = nullptr; else { if (!m_missile) m_missile = std::make_shared(); - m_missile->setId(id); + m_missile->setId(id, resourceId); m_missile->setDirection(Otc::South); if (m_missile) @@ -83,18 +83,31 @@ void UIMissile::setMissile(const MissilePtr& e) void UIMissile::onStyleApply(const std::string_view styleName, const OTMLNodePtr& styleNode) { + uint16_t missileId = 0; + uint16_t missileResourceId = 0; + bool needUpdate = false; + for (const auto& node : styleNode->children()) { - if (node->tag() == "missile-id") - setMissileId(node->value()); - else if (node->tag() == "missile-visible") + const std::string tag = node->tag(); + if (tag == "missile-id") { + missileId = node->value(); + needUpdate = true; + } else if (tag == "missile-resource-id") { + missileResourceId = node->value(); + needUpdate = true; + } else if (tag == "missile-visible") setMissileVisible(node->value()); - else if (node->tag() == "virtual") + else if (tag == "virtual") setVirtual(node->value()); - else if (node->tag() == "show-id") + else if (tag == "show-id") m_showId = node->value(); - else if (node->tag() == "direction") + else if (tag == "direction") setDirection(static_cast(node->value())); } + + if (needUpdate) + setMissileId(missileId, missileResourceId); + UIWidget::onStyleApply(styleName, styleNode); } diff --git a/src/client/uimissile.h b/src/client/uimissile.h index b5baa826da..fa9ff800ed 100644 --- a/src/client/uimissile.h +++ b/src/client/uimissile.h @@ -31,7 +31,7 @@ class UIMissile final : public UIWidget UIMissile(); void drawSelf(DrawPoolType drawPane) override; - void setMissileId(int id); + void setMissileId(int id, uint16_t resourceId = 0); void setMissileVisible(const bool visible) { m_missileVisible = visible; } void setMissile(const MissilePtr& missile); void setVirtual(const bool virt) { m_virtual = virt; } diff --git a/src/client/uisprite.cpp b/src/client/uisprite.cpp index 5ef95b3aa7..60cd109a10 100644 --- a/src/client/uisprite.cpp +++ b/src/client/uisprite.cpp @@ -21,7 +21,7 @@ */ #include "uisprite.h" -#include +#include #include "framework/graphics/drawpoolmanager.h" #include "framework/otml/otmlnode.h" @@ -48,9 +48,9 @@ void UISprite::drawSelf(const DrawPoolType drawPane) drawText(m_rect); } -void UISprite::setSpriteId(const int id) +void UISprite::setSpriteId(const int id, const uint16_t resourceId) { - if (!g_sprites.isLoaded()) + if (!g_things.isSprLoaded(resourceId)) return; m_spriteId = id; @@ -60,7 +60,8 @@ void UISprite::setSpriteId(const int id) } m_sprite = nullptr; - if (const auto& image = g_sprites.getSpriteImage(id)) { + bool isLoading = false; + if (const auto& image = g_things.getSpriteImage(id, resourceId, isLoading)) { m_sprite = std::make_shared(image); m_sprite->allowAtlasCache(); } @@ -70,12 +71,24 @@ void UISprite::onStyleApply(const std::string_view styleName, const OTMLNodePtr& { UIWidget::onStyleApply(styleName, styleNode); + uint16_t spriteId = 0; + uint16_t spriteResourceId = 0; + bool needUpdate = false; + for (const auto& node : styleNode->children()) { - if (node->tag() == "sprite-id") - setSpriteId(node->value()); - else if (node->tag() == "sprite-visible") + const std::string tag = node->tag(); + if (tag == "sprite-id") { + spriteId = node->value(); + needUpdate = true; + } else if (tag == "sprite-resource-id") { + spriteResourceId = node->value(); + needUpdate = true; + } else if (tag == "sprite-visible") setSpriteVisible(node->value()); - else if (node->tag() == "sprite-color") + else if (tag == "sprite-color") setSpriteColor(node->value()); } -} \ No newline at end of file + + if (needUpdate) + setSpriteId(spriteId, spriteResourceId); +} diff --git a/src/client/uisprite.h b/src/client/uisprite.h index 6d59d2a8b1..1564abeb0c 100644 --- a/src/client/uisprite.h +++ b/src/client/uisprite.h @@ -29,7 +29,7 @@ class UISprite final : public UIWidget public: void drawSelf(DrawPoolType drawPane) override; - void setSpriteId(int id); + void setSpriteId(int id, uint16_t resourceId = 0); int getSpriteId() { return m_spriteId; } void clearSprite() { setSpriteId(0); } @@ -45,6 +45,7 @@ class UISprite final : public UIWidget TexturePtr m_sprite; uint16_t m_spriteId{ 0 }; + uint16_t m_resourceId{ 0 }; Color m_spriteColor{ Color::white }; bool m_spriteVisible{ true }; diff --git a/src/framework/core/garbagecollection.cpp b/src/framework/core/garbagecollection.cpp index a5e98437d3..6fe838ee6a 100644 --- a/src/framework/core/garbagecollection.cpp +++ b/src/framework/core/garbagecollection.cpp @@ -23,7 +23,6 @@ #include "garbagecollection.h" #include "client/const.h" -#include "client/thingtype.h" #include "client/thingtypemanager.h" #include "framework/graphics/declarations.h" #include "framework/graphics/texture.h" @@ -66,29 +65,74 @@ void GarbageCollection::texture() { } void GarbageCollection::thingType() { - static constexpr uint16_t - IDLE_TIME = 60 * 1000, // Maximum time it can be idle, default 60 seconds. - AMOUNT_PER_CHECK = 500; // maximum number of objects to be checked. + static constexpr uint16_t AMOUNT_PER_CHECK = 500; // maximum number of objects to be checked. + + static uint8_t category{ 0 }; + + static size_t thingId = 0; + static size_t resourceId = 0; + size_t scanned = 0; + + while (scanned < AMOUNT_PER_CHECK) { + // no more resources to scan + // wrap around and finish + if (resourceId >= g_things.getResourcesCount()) { + resourceId = 0; + category = ThingCategoryItem; + thingId = 0; + break; + } + + // get resource by id + auto res = g_things.getResourceById(resourceId); + if (!res) { + // no resource found + // move to the next resource + ++resourceId; + category = ThingCategoryItem; + thingId = 0; + continue; + } - static uint8_t category{ ThingLastCategory }; - static size_t index = 0; + // this is unlikely to happen + // but it stops sonar from flagging this function as a "blocker" + if (category >= res->m_thingTypes->size()) { + throw std::out_of_range("GarbageCollection: resource category is out of range"); + } - if (category == ThingLastCategory) - category = ThingCategoryItem; + auto& things = res->m_thingTypes[category]; + if (things.empty()) { + ++category; + thingId = 0; + if (category == ThingLastCategory) { + category = ThingCategoryItem; + ++resourceId; + } + continue; + } - const auto& thingTypes = g_things.m_thingTypes[category]; - const size_t limit = std::min(index + AMOUNT_PER_CHECK, thingTypes.size()); + processThingsInCategory(things, thingId, scanned, AMOUNT_PER_CHECK); - while (index < limit) { - auto& thing = thingTypes[index]; - if (thing->hasTexture() && thing->getLastTimeUsage().ticksElapsed() > IDLE_TIME) { - thing->unload(); + if (thingId == things.size() - 1) { + thingId = 0; + ++category; + if (category == ThingLastCategory) { + category = ThingCategoryItem; + ++resourceId; + } } - ++index; } +} + +void GarbageCollection::processThingsInCategory(ThingTypeList& things, size_t& thingId, size_t& scanned, size_t maxAmount) { + static constexpr uint16_t IDLE_TIME = 60 * 1000; // Maximum time it can be idle, default 60 seconds. + + const size_t limit = std::min(thingId + (maxAmount - scanned), things.size() - 1); + for (; thingId < limit; ++thingId, ++scanned) { + const auto& thing = things[thingId]; + if (!thing) continue; - if (limit == thingTypes.size()) { - index = 0; - ++category; + if (thing->hasTexture() && thing->getLastTimeUsage().ticksElapsed() > IDLE_TIME) + thing->unload(); } -} \ No newline at end of file +} diff --git a/src/framework/core/garbagecollection.h b/src/framework/core/garbagecollection.h index abdabcc925..37becf2bd6 100644 --- a/src/framework/core/garbagecollection.h +++ b/src/framework/core/garbagecollection.h @@ -23,6 +23,7 @@ #pragma once #include "timer.h" +#include "client/thingtype.h" #include class GarbageCollection @@ -34,6 +35,8 @@ class GarbageCollection static void thingType(); private: + static void processThingsInCategory(ThingTypeList& things, size_t& thingId, size_t& scanned, size_t maxAmount); + static bool canCheck(Timer& timer, const uint32_t delay) { if (timer.ticksElapsed() < delay) return false; diff --git a/src/framework/graphics/drawpoolmanager.cpp b/src/framework/graphics/drawpoolmanager.cpp index 7ff5fde5ca..e26bc5e732 100644 --- a/src/framework/graphics/drawpoolmanager.cpp +++ b/src/framework/graphics/drawpoolmanager.cpp @@ -247,7 +247,7 @@ void DrawPoolManager::drawObjects(DrawPool* pool) { void DrawPoolManager::drawPool(const DrawPoolType type) { const auto pool = get(type); - if (!pool->isEnabled()) + if (!pool || !pool->isEnabled()) return; drawObjects(pool); diff --git a/src/framework/graphics/image.cpp b/src/framework/graphics/image.cpp index 4c08302ffe..c6a527d8ff 100644 --- a/src/framework/graphics/image.cpp +++ b/src/framework/graphics/image.cpp @@ -256,6 +256,24 @@ void Image::reverseChannels() } } +void Image::checkTransparentPixels() +{ + // The image must be more than 4 pixels transparent to be considered transparent. + uint8_t cntTrans = 0; + const auto& buf = getPixels(); + for (size_t i = 3, n = buf.size(); i < n; i += 4) { + // do not simplify + // collapsing this to "buf[i] == 0x00 && ++cntTrans > 4" causes sonar to flag this as a blocker + if (buf[i] == 0x00) { + ++cntTrans; + if (cntTrans > 4) { + setTransparentPixel(true); + break; + } + } + } +} + ImagePtr Image::fromQRCode(const std::string& code, const int border) { try { diff --git a/src/framework/graphics/image.h b/src/framework/graphics/image.h index a492428f5a..6ef4aea875 100644 --- a/src/framework/graphics/image.h +++ b/src/framework/graphics/image.h @@ -71,6 +71,7 @@ class Image uint8_t* getPixel(const int x, const int y) { return &m_pixels[static_cast(y * m_size.width() + x) * m_bpp]; } bool hasTransparentPixel() const { return m_transparentPixel; } + void checkTransparentPixels(); void setTransparentPixel(const bool value) { m_transparentPixel = value; } private: diff --git a/src/tools/datdump.cpp b/src/tools/datdump.cpp index 67996b51e4..68d5b75c7f 100644 --- a/src/tools/datdump.cpp +++ b/src/tools/datdump.cpp @@ -22,6 +22,7 @@ #include "tools/datdump.h" +#ifdef FRAMEWORK_EDITOR #include "client/game.h" #include "client/thingtype.h" #include "client/thingtypemanager.h" @@ -212,7 +213,7 @@ namespace datdump { if (version >= 1057) g_game.enableFeature(Otc::GameIdleAnimations); - if (!g_things.loadDat(request.datPath)) { + if (!g_things.loadDat(request.datPath, 0)) { // to do: multispr throw std::runtime_error("unable to load DAT file: " + request.datPath); } @@ -259,4 +260,5 @@ namespace datdump { g_lua.terminate(); return success; } -} // namespace datdump \ No newline at end of file +} // namespace datdump +#endif diff --git a/src/tools/datdump.h b/src/tools/datdump.h index e107ea658d..15caa966b7 100644 --- a/src/tools/datdump.h +++ b/src/tools/datdump.h @@ -22,6 +22,7 @@ #pragma once +#ifdef FRAMEWORK_EDITOR #include #include #include @@ -40,3 +41,4 @@ std::optional parseRequest(std::vector& args); bool run(const Request& request); } // namespace datdump +#endif \ No newline at end of file diff --git a/vc17/otclient.vcxproj b/vc17/otclient.vcxproj index 039e6f9b53..a843e3c35e 100644 --- a/vc17/otclient.vcxproj +++ b/vc17/otclient.vcxproj @@ -349,7 +349,6 @@ - @@ -518,7 +517,6 @@ -