From b2372f8afd379f09696356c44130ecd77dea50ea Mon Sep 17 00:00:00 2001 From: sruon Date: Mon, 23 Mar 2026 19:01:43 -0600 Subject: [PATCH] Revamp exdata handling - Save modified exdata at tick end - Expose actual exdata structs - Interface with lua through tables with named keys Co-Authored-By: atom0s --- scripts/enum/item.lua | 1 + scripts/items/tredecim_scythe.lua | 6 +- scripts/specs/core/CBaseEntity.lua | 1 + scripts/specs/core/CItem.lua | 16 ++- scripts/specs/core/Exdata.lua | 12 ++ scripts/tests/systems/exdata.lua | 126 +++++++++++++++++++ src/map/entities/charentity.cpp | 2 + src/map/enums/exdata.h | 98 +++++++++++++++ src/map/inventory_sync_state.cpp | 31 +++++ src/map/inventory_sync_state.h | 3 + src/map/items.h | 2 + src/map/items/CMakeLists.txt | 7 ++ src/map/items/exdata.cpp | 88 +++++++++++++ src/map/items/exdata.h | 39 ++++++ src/map/items/exdata/base.h | 62 +++++++++ src/map/items/exdata/legion_pass.cpp | 40 ++++++ src/map/items/exdata/legion_pass.h | 40 ++++++ src/map/items/exdata/perpetual_hourglass.cpp | 38 ++++++ src/map/items/exdata/perpetual_hourglass.h | 44 +++++++ src/map/items/item.cpp | 10 ++ src/map/items/item.h | 20 ++- src/map/lua/lua_baseentity.cpp | 39 +++--- src/map/lua/lua_baseentity.h | 2 +- src/map/lua/lua_item.cpp | 84 ++++++++++++- src/map/lua/lua_item.h | 6 +- 25 files changed, 783 insertions(+), 34 deletions(-) create mode 100644 scripts/specs/core/Exdata.lua create mode 100644 scripts/tests/systems/exdata.lua create mode 100644 src/map/enums/exdata.h create mode 100644 src/map/items/exdata.cpp create mode 100644 src/map/items/exdata.h create mode 100644 src/map/items/exdata/base.h create mode 100644 src/map/items/exdata/legion_pass.cpp create mode 100644 src/map/items/exdata/legion_pass.h create mode 100644 src/map/items/exdata/perpetual_hourglass.cpp create mode 100644 src/map/items/exdata/perpetual_hourglass.h diff --git a/scripts/enum/item.lua b/scripts/enum/item.lua index e3bb23d45a0..033756ab5ab 100644 --- a/scripts/enum/item.lua +++ b/scripts/enum/item.lua @@ -2244,6 +2244,7 @@ xi.item = LEVIATITE = 3525, CARBITE = 3526, FENRITE = 3527, + LEGION_PASS = 3528, MOG_KUPON_A_SAP = 3533, MOG_KUPON_A_JAD = 3534, MOG_KUPON_A_RUB = 3537, diff --git a/scripts/items/tredecim_scythe.lua b/scripts/items/tredecim_scythe.lua index d63f8ab469b..0e5a61b5615 100644 --- a/scripts/items/tredecim_scythe.lua +++ b/scripts/items/tredecim_scythe.lua @@ -9,16 +9,14 @@ itemObject.onItemEquip = function(target, item) target:addListener('MELEE_SWING_HIT', 'TREDECIM_MELEE_SWING_HIT', function(playerArg, targetArg, attackArg) local mainWeapon = playerArg:getEquippedItem(xi.slot.MAIN) if mainWeapon and mainWeapon:getID() == xi.item.TREDECIM_SCYTHE then - local exData = mainWeapon:getExData() + local exData = mainWeapon:getExDataRaw() local count = exData[0] if count % 13 == 12 then attackArg:setCritical(true) end - exData[0] = (count + 1) % 13 - - mainWeapon:setExData(exData) + mainWeapon:setExDataRaw({ [0] = (count + 1) % 13 }) end end) end diff --git a/scripts/specs/core/CBaseEntity.lua b/scripts/specs/core/CBaseEntity.lua index 1d855b54795..c3f70ef87e6 100644 --- a/scripts/specs/core/CBaseEntity.lua +++ b/scripts/specs/core/CBaseEntity.lua @@ -951,6 +951,7 @@ end -- TODO: This one is going to be really messy, might be better to create multiple definitions -- for readability. +---@return CItem? function CBaseEntity:addItem(...) end diff --git a/scripts/specs/core/CItem.lua b/scripts/specs/core/CItem.lua index 3914ce15a8c..565497bf315 100644 --- a/scripts/specs/core/CItem.lua +++ b/scripts/specs/core/CItem.lua @@ -195,11 +195,19 @@ function CItem:getSoulPlateData() end ---@nodiscard ----@return table +---@return ExdataLegionPass|ExdataPerpetualHourglass function CItem:getExData() end ----@param newData table ----@return nil -function CItem:setExData(newData) +---@param data ExdataLegionPass|ExdataPerpetualHourglass +function CItem:setExData(data) +end + +---@nodiscard +---@return table # 0-indexed raw exdata bytes +function CItem:getExDataRaw() +end + +---@param data table # 0-indexed raw exdata bytes +function CItem:setExDataRaw(data) end diff --git a/scripts/specs/core/Exdata.lua b/scripts/specs/core/Exdata.lua new file mode 100644 index 00000000000..385d1f03596 --- /dev/null +++ b/scripts/specs/core/Exdata.lua @@ -0,0 +1,12 @@ +---@meta + +---@class ExdataLegionPass +---@field timestamp integer # How long until the pass expires. Usually 5 minutes from creation. +---@field title xi.legion.title # Legion chamber +---@field signature string # 12 characters pass owner name + +---@class ExdataPerpetualHourglass +---@field flags integer # Undocumented flags. Changes Hourglass text color to denote status. +---@field startTime integer # Reservation start time +---@field endTime integer # Reservation end time +---@field zoneId xi.zone # Zone reserved by Hourglass diff --git a/scripts/tests/systems/exdata.lua b/scripts/tests/systems/exdata.lua new file mode 100644 index 00000000000..d04fb53ff97 --- /dev/null +++ b/scripts/tests/systems/exdata.lua @@ -0,0 +1,126 @@ +describe('Exdata', function() + ---@type CClientEntityPair + local player + + before_each(function() + player = xi.test.world:spawnPlayer( + { + zone = xi.zone.SOUTHERN_SAN_DORIA, + }) + end) + + it('can get and set Legion Pass exdata', function() + local item = player:addItem({ id = xi.item.LEGION_PASS, quantity = 1 }) + assert(item) + + local now = GetSystemTime() + item:setExData( + { + timestamp = now + 300, + title = xi.legion.title.HALL_OF_AN_36, + signature = player:getName(), + }) + + local ex = item:getExData() + assert(ex.timestamp == now + 300) + assert(ex.title == xi.legion.title.HALL_OF_AN_36) + assert(ex.signature == player:getName()) + end) + + it('can get and set Perpetual Hourglass exdata', function() + local item = player:addItem({ id = xi.item.PERPETUAL_HOURGLASS, quantity = 1 }) + assert(item) + + local now = GetSystemTime() + item:setExData( + { + flags = 0x01, + startTime = now, + endTime = now + 1800, + zoneId = xi.zone.DYNAMIS_SAN_DORIA, + }) + + local ex = item:getExData() + assert(ex.flags == 0x01) + assert(ex.startTime == now) + assert(ex.endTime == now + 1800) + assert(ex.zoneId == xi.zone.DYNAMIS_SAN_DORIA) + end) + + it('preserves unchanged fields on write-back', function() + local item = player:addItem({ id = xi.item.LEGION_PASS, quantity = 1 }) + assert(item) + + item:setExData( + { + timestamp = 1000, + title = xi.legion.title.HALL_OF_KI_18, + signature = 'TestPlayer', + }) + + local ex = item:getExData() + ex.title = xi.legion.title.HALL_OF_MURU_36 + item:setExData(ex) + + local ex2 = item:getExData() + assert(ex2.timestamp == 1000) + assert(ex2.title == xi.legion.title.HALL_OF_MURU_36) + assert(ex2.signature == 'TestPlayer') + end) + + it('addItem accepts exdata table', function() + local now = GetSystemTime() + local item = player:addItem( + { + id = xi.item.LEGION_PASS, + quantity = 1, + exdata = + { + timestamp = now + 300, + title = xi.legion.title.HALL_OF_AN_36, + signature = 'AddItemTest', + }, + }) + assert(item) + + local ex = item:getExData() + assert(ex.timestamp == now + 300) + assert(ex.title == xi.legion.title.HALL_OF_AN_36) + assert(ex.signature == 'AddItemTest') + end) + + it('addItem accepts raw exdata bytes', function() + local item = player:addItem( + { + id = xi.item.FIRE_CRYSTAL, + quantity = 1, + exdata = { [0] = 0x42, [5] = 0xCD }, + }) + assert(item) + + local ex = item:getExDataRaw() + assert(ex[0] == 0x42) + assert(ex[5] == 0xCD) + end) + + it('raw functions bypass typed dispatch', function() + local item = player:addItem({ id = xi.item.LEGION_PASS, quantity = 1 }) + assert(item) + + item:setExDataRaw({ [0] = 0xF4, [1] = 0x01 }) + + local ex = item:getExData() + assert(ex.timestamp == 500) + end) + + it('unhandled items fall back to raw bytes', function() + local item = player:addItem({ id = xi.item.FIRE_CRYSTAL, quantity = 1 }) + assert(item) + + item:setExDataRaw({ [0] = 0xAB, [5] = 0xCD }) + + local ex = item:getExData() + assert(ex[0] == 0xAB) + assert(ex[5] == 0xCD) + end) +end) diff --git a/src/map/entities/charentity.cpp b/src/map/entities/charentity.cpp index c7c690cffe7..a5eab1ebb07 100644 --- a/src/map/entities/charentity.cpp +++ b/src/map/entities/charentity.cpp @@ -1147,6 +1147,8 @@ void CCharEntity::PostTick() sendServerStatus_ = false; updatemask = 0; } + + inventorySyncState_.flushDirtyItems(this); } // Flush all pending equipment changes at end of network cycle after all SmallPackets have been processed diff --git a/src/map/enums/exdata.h b/src/map/enums/exdata.h new file mode 100644 index 00000000000..5b4b89d1507 --- /dev/null +++ b/src/map/enums/exdata.h @@ -0,0 +1,98 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include +#include + +namespace Exdata +{ + +enum class AugmentKindFlags : uint8_t +{ + HasAugments = 0x02, // Standard/trial/serialized/crafting shield/evolith + Bundled = 0x03, // Odyssey, Dyna-D JSE necks +}; + +// AugmentSubKind (byte 1 of augment exdata) +// When Kind=2: bitflags select the augment format +enum class AugmentSubKindFlags : uint8_t +{ + Standard = 0x03, // Base flags for standard augments + Escutcheon = 0x08, // Crafting shields + Serialized = 0x10, // Serialized number + server name (Lu Shang +1, Ebisu +1) + Mezzotint = 0x20, // Mezzotinting (Geas Fete, Delve) + Trial = 0x40, // Magian trial + Evolith = 0x80, +}; +} // namespace Exdata + +template <> +struct magic_enum::customize::enum_range +{ + static constexpr bool is_flags = true; +}; + +template <> +struct magic_enum::customize::enum_range +{ + static constexpr bool is_flags = true; +}; + +using namespace magic_enum::bitwise_operators; + +namespace Exdata +{ + +// Numbers are meaningless, new types can be added at the tail end. +enum class Type : uint8_t +{ + None = 0, + Augment = 1, + Usable = 2, + Mannequin = 3, + Furniture = 4, + FlowerPot = 5, + Linkshell = 6, + Fish = 7, + BettingSlip = 8, + SoulPlate = 9, + SoulReflector = 10, + AssaultLog = 11, + LotteryTicket = 12, + Tabula = 13, + Evolith = 14, + CraftingSet = 15, + BrennerBook = 16, + GlowingLamp = 17, + LegionPass = 18, + Serialized = 19, + PerpetualHourglass = 20, + ChocoboEgg = 21, + ChocoboCard = 22, + Escutcheon = 23, + RaceCertificate = 24, + MeebleGrimoire = 25, + HoneymoonTicket = 26, + WeaponUnlock = 27, +}; +} // namespace Exdata diff --git a/src/map/inventory_sync_state.cpp b/src/map/inventory_sync_state.cpp index eda066f9be0..0420c907d0e 100644 --- a/src/map/inventory_sync_state.cpp +++ b/src/map/inventory_sync_state.cpp @@ -20,7 +20,11 @@ */ #include "inventory_sync_state.h" + +#include "common/database.h" +#include "entities/charentity.h" #include "items/item.h" +#include "packets/s2c/0x020_item_attr.h" // Marks a given container as having been entirely streamed to the client void InventorySyncState::markSynced(const CONTAINER_ID id) @@ -70,3 +74,30 @@ auto InventorySyncState::dirtyContainers() const -> const std::set { return dirtyContainers_; } + +void InventorySyncState::flushDirtyItems(CCharEntity* PChar) +{ + for (uint8 loc = 0; loc < MAX_CONTAINER_ID; ++loc) + { + auto* PContainer = PChar->getStorage(loc); + if (!PContainer) + { + continue; + } + + for (uint8 slot = 0; slot <= PContainer->GetSize(); ++slot) + { + auto* PItem = PContainer->GetItem(slot); + if (PItem && PItem->isDirty()) + { + db::preparedStmt("UPDATE char_inventory SET extra = ? WHERE charid = ? AND location = ? AND slot = ? LIMIT 1", + PItem->m_extra, + PChar->id, + loc, + slot); + PChar->pushPacket(PItem, static_cast(loc), slot); + PItem->setDirty(false); + } + } + } +} diff --git a/src/map/inventory_sync_state.h b/src/map/inventory_sync_state.h index 2ce447443d9..9efd7a57aa7 100644 --- a/src/map/inventory_sync_state.h +++ b/src/map/inventory_sync_state.h @@ -58,6 +58,9 @@ class InventorySyncState auto pendingEquipChanges() const -> const std::vector&; auto dirtyContainers() const -> const std::set&; + // Dirty item exdata flush + void flushDirtyItems(class CCharEntity* PChar); + private: xi::bitset syncedContainers_{}; std::vector pendingEquipChanges_; diff --git a/src/map/items.h b/src/map/items.h index e204c088104..f1f3c205902 100644 --- a/src/map/items.h +++ b/src/map/items.h @@ -72,6 +72,7 @@ enum ITEMID : uint16 LEVIATITE = 3525, CARBITE = 3526, FENRITE = 3527, + LEGION_PASS = 3528, FIRE_CRYSTAL = 4096, ICE_CRYSTAL = 4097, WIND_CRYSTAL = 4098, @@ -81,6 +82,7 @@ enum ITEMID : uint16 LIGHT_CRYSTAL = 4102, DARK_CRYSTAL = 4103, DARK_CLUSTER = 4111, + PERPETUAL_HOURGLASS = 4237, INFERNO_CRYSTAL = 4238, GLACIER_CRYSTAL = 4239, CYCLONE_CRYSTAL = 4240, diff --git a/src/map/items/CMakeLists.txt b/src/map/items/CMakeLists.txt index 3226fb51a4f..abc17ffdb2b 100644 --- a/src/map/items/CMakeLists.txt +++ b/src/map/items/CMakeLists.txt @@ -1,4 +1,11 @@ set(ITEM_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/exdata.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/exdata.h + ${CMAKE_CURRENT_SOURCE_DIR}/exdata/base.h + ${CMAKE_CURRENT_SOURCE_DIR}/exdata/legion_pass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/exdata/legion_pass.h + ${CMAKE_CURRENT_SOURCE_DIR}/exdata/perpetual_hourglass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/exdata/perpetual_hourglass.h ${CMAKE_CURRENT_SOURCE_DIR}/item_currency.cpp ${CMAKE_CURRENT_SOURCE_DIR}/item_currency.h ${CMAKE_CURRENT_SOURCE_DIR}/item_equipment.cpp diff --git a/src/map/items/exdata.cpp b/src/map/items/exdata.cpp new file mode 100644 index 00000000000..3b8e2a5ddcc --- /dev/null +++ b/src/map/items/exdata.cpp @@ -0,0 +1,88 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#include "exdata.h" + +#include "item.h" + +#include "items.h" +#include + +namespace Exdata +{ + +// Returns Exdata type for an item based on type or item ID. +// This loosely follows client logic for rendering exdata. +auto getType(const CItem* item) -> Type +{ + if (!item) + { + return Type::None; + } + + const auto itemId = item->getID(); + + if (itemId == LEGION_PASS) + { + return Type::LegionPass; + } + + if (itemId == PERPETUAL_HOURGLASS) + { + return Type::PerpetualHourglass; + } + + return Type::None; +} + +// Fills the table with appropriate keys according to exdata type +auto toTable(const CItem* item, sol::table& table) -> bool +{ + switch (Exdata::getType(item)) + { + case Type::LegionPass: + item->exdata().toTable(table); + return true; + case Type::PerpetualHourglass: + item->exdata().toTable(table); + return true; + default: + return false; + } +} + +// Updates item exdata using values from passed table matching exdata type +auto fromTable(CItem* item, const sol::table& data) -> bool +{ + switch (Exdata::getType(item)) + { + case Type::LegionPass: + item->exdata().fromTable(data); + return true; + case Type::PerpetualHourglass: + item->exdata().fromTable(data); + return true; + default: + return false; + } +} + +} // namespace Exdata diff --git a/src/map/items/exdata.h b/src/map/items/exdata.h new file mode 100644 index 00000000000..f71038cd949 --- /dev/null +++ b/src/map/items/exdata.h @@ -0,0 +1,39 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "enums/exdata.h" + +#include "exdata/base.h" + +#include "exdata/legion_pass.h" +#include "exdata/perpetual_hourglass.h" + +class CItem; + +namespace Exdata +{ +auto getType(const CItem* item) -> Type; + +auto toTable(const CItem* item, sol::table& table) -> bool; +auto fromTable(CItem* item, const sol::table& data) -> bool; +} // namespace Exdata diff --git a/src/map/items/exdata/base.h b/src/map/items/exdata/base.h new file mode 100644 index 00000000000..488483f7f28 --- /dev/null +++ b/src/map/items/exdata/base.h @@ -0,0 +1,62 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "common/cbasetypes.h" +#include "common/utils.h" + +#include +#include +#include + +namespace Exdata +{ +template +auto decodeSignature(const uint8_t (&sig)[N]) -> std::string +{ + const std::string raw(reinterpret_cast(sig), N); + char decoded[DecodeStringLength] = {}; + DecodeStringSignature(raw, decoded); + return std::string(decoded); +} + +template +void encodeSignature(const std::string& str, uint8_t (&sig)[N]) +{ + char encoded[SignatureStringLength] = {}; + EncodeStringSignature(str, encoded); + std::memcpy(sig, encoded, N); +} + +// sol2's get_or is ambiguous when the fallback comes from a bitfield. +template +auto get_or(Table&& tbl, Key&& key, T fallback) -> T +{ + return std::forward(tbl).template get>(std::forward(key)).value_or(std::move(fallback)); +} + +template +auto get_or(Proxy&& proxy, T fallback) -> T +{ + return std::forward(proxy).template get>().value_or(std::move(fallback)); +} +} // namespace Exdata diff --git a/src/map/items/exdata/legion_pass.cpp b/src/map/items/exdata/legion_pass.cpp new file mode 100644 index 00000000000..2643933036a --- /dev/null +++ b/src/map/items/exdata/legion_pass.cpp @@ -0,0 +1,40 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#include "legion_pass.h" + +void Exdata::LegionPass::toTable(sol::table& table) const +{ + table["timestamp"] = this->Timestamp; + table["title"] = this->Title; + table["signature"] = Exdata::decodeSignature(this->Signature); +} + +void Exdata::LegionPass::fromTable(const sol::table& data) +{ + this->Timestamp = Exdata::get_or(data, "timestamp", this->Timestamp); + this->Title = Exdata::get_or(data, "title", this->Title); + + if (sol::optional sig = data["signature"]) + { + Exdata::encodeSignature(*sig, this->Signature); + } +} diff --git a/src/map/items/exdata/legion_pass.h b/src/map/items/exdata/legion_pass.h new file mode 100644 index 00000000000..e55baa7d99e --- /dev/null +++ b/src/map/items/exdata/legion_pass.h @@ -0,0 +1,40 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "base.h" + +namespace Exdata +{ +#pragma pack(push, 1) +struct LegionPass +{ + uint32_t Timestamp; + uint32_t Title; + uint8_t padding00[4]; // byte 0 is a server-side counter; bytes 1-3 unknown + uint8_t Signature[12]; + + void toTable(sol::table& table) const; + void fromTable(const sol::table& data); +}; +#pragma pack(pop) +} // namespace Exdata diff --git a/src/map/items/exdata/perpetual_hourglass.cpp b/src/map/items/exdata/perpetual_hourglass.cpp new file mode 100644 index 00000000000..fb7eedc0c5d --- /dev/null +++ b/src/map/items/exdata/perpetual_hourglass.cpp @@ -0,0 +1,38 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#include "perpetual_hourglass.h" + +void Exdata::PerpetualHourglass::toTable(sol::table& table) const +{ + table["flags"] = this->Flags; + table["startTime"] = this->StartTime; + table["endTime"] = this->EndTime; + table["zoneId"] = this->ZoneId; +} + +void Exdata::PerpetualHourglass::fromTable(const sol::table& data) +{ + this->Flags = Exdata::get_or(data, "flags", this->Flags); + this->StartTime = Exdata::get_or(data, "startTime", this->StartTime); + this->EndTime = Exdata::get_or(data, "endTime", this->EndTime); + this->ZoneId = Exdata::get_or(data, "zoneId", this->ZoneId); +} diff --git a/src/map/items/exdata/perpetual_hourglass.h b/src/map/items/exdata/perpetual_hourglass.h new file mode 100644 index 00000000000..133b43c5737 --- /dev/null +++ b/src/map/items/exdata/perpetual_hourglass.h @@ -0,0 +1,44 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "base.h" + +namespace Exdata +{ +#pragma pack(push, 1) +struct PerpetualHourglass +{ + uint16_t padding00; + uint8_t Flags : 3; + uint8_t padding01 : 5; + uint8_t padding02[5]; + uint32_t StartTime; + uint32_t EndTime; + uint16_t ZoneId; + uint8_t padding03[6]; + + void toTable(sol::table& table) const; + void fromTable(const sol::table& data); +}; +#pragma pack(pop) +} // namespace Exdata diff --git a/src/map/items/item.cpp b/src/map/items/item.cpp index 0568ea92f75..bae9e8befb3 100644 --- a/src/map/items/item.cpp +++ b/src/map/items/item.cpp @@ -373,6 +373,16 @@ bool CItem::isStorageSlip() const return m_id < 29340 && m_id > 29311; } +auto CItem::isDirty() const -> bool +{ + return dirty_; +} + +void CItem::setDirty(const bool dirty) +{ + dirty_ = dirty; +} + bool CItem::isSoultrapper() const { return m_id == 18721 || m_id == 18724; diff --git a/src/map/items/item.h b/src/map/items/item.h index 089236854d2..838568e8fba 100644 --- a/src/map/items/item.h +++ b/src/map/items/item.h @@ -121,6 +121,9 @@ class CItem virtual const std::string getSignature(); virtual void setSignature(const std::string& signature); + auto isDirty() const -> bool; + void setDirty(bool dirty); + bool isSoultrapper() const; void setSoulPlateData(const std::string& name, uint32 interestData, uint8 zeni, uint16 skillIndex, uint8 fp); auto getSoulPlateData() -> std::tuple; @@ -128,7 +131,21 @@ class CItem bool isMannequin() const; static constexpr uint32_t extra_size = 0x18; - uint8 m_extra[extra_size]{}; // any extra data pertaining to item (augments, furniture location, etc) + uint8 m_extra[extra_size]{}; + + template + auto exdata() -> T& + { + static_assert(sizeof(T) == extra_size, "Exdata struct must be 24 bytes"); + return *reinterpret_cast(m_extra); + } + + template + auto exdata() const -> const T& + { + static_assert(sizeof(T) == extra_size, "Exdata struct must be 24 bytes"); + return *reinterpret_cast(m_extra); + } protected: void setType(uint8); @@ -150,6 +167,7 @@ class CItem uint8 m_locationID; // storage number bool m_sent; + bool dirty_{}; std::string m_name; std::string m_send; diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 33651c5ad22..114b9785ce4 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -98,6 +98,7 @@ #include "enums/automaton.h" #include "enums/chat_message_area.h" #include "enums/item_lockflg.h" +#include "items/exdata.h" #include "items/item_furnishing.h" #include "items/item_linkshell.h" @@ -4111,17 +4112,18 @@ uint32 CLuaBaseEntity::getItemCount(uint16 itemID) * Notes : See format and variable options below ************************************************************************/ -bool CLuaBaseEntity::addItem(sol::variadic_args va) +auto CLuaBaseEntity::addItem(sol::variadic_args va) const -> CItem* { if (m_PBaseEntity->objtype != TYPE_PC) { ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); - return false; + return nullptr; } - uint8 SlotID = ERROR_SLOTID; + uint8 SlotID = ERROR_SLOTID; + CItem* AddedItem = nullptr; - CCharEntity* PChar = (CCharEntity*)m_PBaseEntity; + auto* PChar = static_cast(m_PBaseEntity); /* FORMAT 1: player:addItem({ id = itemID, quantity = quantity }) -- add quantity of itemID @@ -4138,7 +4140,7 @@ bool CLuaBaseEntity::addItem(sol::variadic_args va) if (!table["id"].valid()) { ShowError("AddItem: id is nil"); - return false; + return nullptr; } uint16 id = table.get("id"); @@ -4204,18 +4206,21 @@ bool CLuaBaseEntity::addItem(sol::variadic_args va) if (exdataObj.is()) { auto exdataTable = exdataObj.as(); - for (const auto& entryPair : exdataTable) + if (!Exdata::fromTable(PItem, exdataTable)) { - uint8 index = entryPair.first.as(); - uint8 value = entryPair.second.as(); - - if (index < CItem::extra_size) + for (const auto& [keyObj, valObj] : exdataTable) { - PItem->m_extra[index] = value; - } - else - { - ShowWarning("AddItem: Trying to write to invalid exdata index: <%i>", index); + uint8 index = keyObj.as(); + uint8 value = valObj.as(); + + if (index < CItem::extra_size) + { + PItem->m_extra[index] = value; + } + else + { + ShowWarning("AddItem: Trying to write to invalid exdata index: <%i>", index); + } } } } @@ -4225,6 +4230,7 @@ bool CLuaBaseEntity::addItem(sol::variadic_args va) { break; } + AddedItem = PItem; } else { @@ -4307,6 +4313,7 @@ bool CLuaBaseEntity::addItem(sol::variadic_args va) { break; } + AddedItem = PItem; } else { @@ -4316,7 +4323,7 @@ bool CLuaBaseEntity::addItem(sol::variadic_args va) } } - return SlotID != ERROR_SLOTID; + return AddedItem; } /************************************************************************ diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index d8d0c7bdfc8..8501446cf99 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -243,7 +243,7 @@ class CLuaBaseEntity bool hasEquipped(uint16 equipmentID); // Returns true if item is equipped in any slot bool hasItem(uint16 itemID, const sol::object& location); uint32 getItemCount(uint16 itemID); - bool addItem(sol::variadic_args va); + auto addItem(sol::variadic_args va) const -> CItem*; bool delItem(uint16 itemID, int32 quantity, const sol::object& containerID); bool delItemAt(uint16 itemID, int32 quantity, uint8 containerId, uint8 slotId); bool delContainerItems(const sol::object& containerID); diff --git a/src/map/lua/lua_item.cpp b/src/map/lua/lua_item.cpp index a33bd39045a..e817eb2d384 100644 --- a/src/map/lua/lua_item.cpp +++ b/src/map/lua/lua_item.cpp @@ -22,10 +22,15 @@ #include "lua_item.h" #include "common/logging.h" +#include "items/exdata.h" #include "items/item.h" #include "items/item_equipment.h" +#include "items/item_fish.h" +#include "items/item_flowerpot.h" #include "items/item_furnishing.h" #include "items/item_general.h" +#include "items/item_linkshell.h" +#include "items/item_usable.h" #include "items/item_weapon.h" #include "utils/itemutils.h" @@ -365,9 +370,20 @@ auto CLuaItem::getSoulPlateData() -> sol::table return table; } -auto CLuaItem::getExData() -> sol::table +/************************************************************************ + * Function: getExData() + * Purpose : Returns the item's extra data as a typed table. + * Example : item:getExData() -- typed table (e.g. ExdataLegionPass) + ************************************************************************/ +auto CLuaItem::getExData() const -> sol::table { sol::table table = lua.create_table(); + + if (Exdata::toTable(m_PLuaItem, table)) + { + return table; + } + for (std::size_t idx = 0; idx < m_PLuaItem->extra_size; ++idx) { table[idx] = m_PLuaItem->m_extra[idx]; @@ -375,21 +391,75 @@ auto CLuaItem::getExData() -> sol::table return table; } -void CLuaItem::setExData(const sol::table& newData) +/************************************************************************ + * Function: setExData() + * Purpose : Writes the item's extra data from a typed table. + * Example : item:setExData({ timestamp = t, title = 1 }) + ************************************************************************/ +void CLuaItem::setExData(const sol::table& data) const { - for (const auto& [keyObj, valObj] : newData) + if (Exdata::fromTable(m_PLuaItem, data)) + { + m_PLuaItem->setDirty(true); + return; + } + + for (const auto& [keyObj, valObj] : data) { - uint8 key = keyObj.as(); - uint8 val = valObj.as(); + uint8 key = keyObj.as(); + const uint8 val = valObj.as(); if (key >= CItem::extra_size) { - ShowWarning("Tried to write to key too large for item exdata array: %s[%i]", m_PLuaItem->getName(), key); + ShowWarning("setExData: key too large for exdata array: %s[%i]", m_PLuaItem->getName(), key); continue; } m_PLuaItem->m_extra[key] = val; } + + m_PLuaItem->setDirty(true); +} + +/************************************************************************ + * Function: getExDataRaw() + * Purpose : Returns the item's extra data as a 0-indexed byte table. + * Example : item:getExDataRaw() + * Notes : Keys are 0-indexed to be in line with the underlying C++ data. + ************************************************************************/ +auto CLuaItem::getExDataRaw() const -> sol::table +{ + sol::table table = lua.create_table(); + for (std::size_t idx = 0; idx < m_PLuaItem->extra_size; ++idx) + { + table[idx] = m_PLuaItem->m_extra[idx]; + } + return table; +} + +/************************************************************************ + * Function: setExDataRaw() + * Purpose : Writes the item's extra data from a 0-indexed byte table. + * Example : item:setExDataRaw({ [0] = 5, [1] = 10 }) + * Notes : Keys are 0-indexed byte offsets into m_extra. + ************************************************************************/ +void CLuaItem::setExDataRaw(const sol::table& data) const +{ + for (const auto& [keyObj, valObj] : data) + { + uint8 key = keyObj.as(); + const uint8 val = valObj.as(); + + if (key >= CItem::extra_size) + { + ShowWarning("setExDataRaw: key too large for exdata array: %s[%i]", m_PLuaItem->getName(), key); + continue; + } + + m_PLuaItem->m_extra[key] = val; + } + + m_PLuaItem->setDirty(true); } //==========================================================// @@ -437,6 +507,8 @@ void CLuaItem::Register() SOL_REGISTER("getSoulPlateData", CLuaItem::getSoulPlateData); SOL_REGISTER("getExData", CLuaItem::getExData); SOL_REGISTER("setExData", CLuaItem::setExData); + SOL_REGISTER("getExDataRaw", CLuaItem::getExDataRaw); + SOL_REGISTER("setExDataRaw", CLuaItem::setExDataRaw); } std::ostream& operator<<(std::ostream& os, const CLuaItem& item) diff --git a/src/map/lua/lua_item.h b/src/map/lua/lua_item.h index f5df96cf9f2..655baf5723f 100644 --- a/src/map/lua/lua_item.h +++ b/src/map/lua/lua_item.h @@ -95,8 +95,10 @@ class CLuaItem void setSoulPlateData(const std::string& name, uint32 interestData, uint8 zeni, uint16 skillIndex, uint8 fp); auto getSoulPlateData() -> sol::table; - auto getExData() -> sol::table; // NOTE: This is 0-indexed, to be in line with the underlying C++ data - void setExData(const sol::table& newData); // NOTE: This is 0-indexed, to be in line with the underlying C++ data + auto getExData() const -> sol::table; + void setExData(const sol::table& data) const; + auto getExDataRaw() const -> sol::table; // NOTE: 0-indexed, to be in line with the underlying C++ data + void setExDataRaw(const sol::table& data) const; // NOTE: 0-indexed, to be in line with the underlying C++ data bool operator==(const CLuaItem& other) const {