diff --git a/modules/phoenix/cpp/cosmetic_wardrobe.cpp b/modules/phoenix/cpp/cosmetic_wardrobe.cpp new file mode 100644 index 00000000000..10f326ac3df --- /dev/null +++ b/modules/phoenix/cpp/cosmetic_wardrobe.cpp @@ -0,0 +1,102 @@ +/************************************************************************ + * Cosmetic Wardrobe + * + * Blocks equipment moves into Mog Wardrobe 8 unless the item is on the cosmetic list. + * The list is published to xi.phoenix.cosmeticWardrobe by + * modules/phoenix/lua/custom/cosmetic_wardrobe.lua, which also opens the + * wardrobe to its full 80 slots on login. Setup instructions live in that + * file's header. + * + * A blocked move never happens server side. The client draws item moves + * before the server answers, so the rejection resends the source slot and + * the item snaps back where it was. + ************************************************************************/ +#include "common/logging.h" +#include "common/settings.h" +#include "map/entities/char_entity.h" +#include "map/enums/chat_message_type.h" +#include "map/item_container.h" +#include "map/items/item.h" +#include "map/packets/basic.h" +#include "map/packets/c2s/0x029_item_move.h" +#include "map/packets/s2c/0x017_chat_std.h" +#include "map/packets/s2c/0x01d_item_same.h" +#include "map/packets/s2c/0x020_item_attr.h" +#include "map/utils/moduleutils.h" + +class CosmeticWardrobe : public CPPModule +{ + void OnInit() override + { + // Read once at boot, the same lifetime as the Lua gate. False means pass + // through, so disabling restores stock wardrobe behavior on restart even + // where Wardrobe 8 is already open. + enabled = settings::get("main.ENABLE_COSMETIC_WARDROBE"); + } + + auto OnIncomingPacket(MapSession* PSession, CCharEntity* PChar, CBasicPacket& data) -> bool override + { + // Return if the packet isn't an item move. + if (data.getType() != static_cast(GP_CLI_COMMAND_ITEM_MOVE::packetId)) + { + return false; + } + + if (!enabled) + { + return false; + } + + // Rearranging within the wardrobe is not storage. + const auto* packet = data.as(); + if (static_cast(packet->Category2) != LOC_WARDROBE8 || packet->Category1 == packet->Category2) + { + return false; + } + + // Validation bounds the containers before this hook runs. Kept anyway, it costs nothing. + if (packet->Category1 >= MAX_CONTAINER_ID) + { + return false; + } + + CItem* PItem = PChar->getStorage(packet->Category1)->GetItem(packet->ItemIndex1); + if (!PItem || isAllowedItem(PItem->getID())) + { + return false; + } + + // The client has already drawn the move. Resend the source slot so the item + // snaps back, tell the player why, and consume the packet. + PChar->pushPacket(PItem, static_cast(packet->Category1), packet->ItemIndex1); + PChar->pushPacket(PChar); + PChar->pushPacket(PChar, MESSAGE_SYSTEM_3, "Only event and cosmetic items can be stored in this wardrobe."); + + return true; + } + + // The list lives Lua side so it can be edited without a rebuild. A missing + // list while enabled means the Lua half broke, and nothing gets in. + auto isAllowedItem(const uint16 itemId) -> bool + { + const auto maybeList = lua["xi"]["phoenix"]["cosmeticWardrobe"].get>(); + if (!maybeList) + { + // One log line, not one per attempt. + if (!warnedMissingList) + { + warnedMissingList = true; + ShowError("CosmeticWardrobe: xi.phoenix.cosmeticWardrobe is not loaded"); + } + + return false; + } + + return maybeList->get_or(itemId, false); + } + + bool enabled = false; + bool warnedMissingList = false; +}; + +REGISTER_CPP_MODULE(CosmeticWardrobe); diff --git a/modules/phoenix/lua/custom/cosmetic_wardrobe.lua b/modules/phoenix/lua/custom/cosmetic_wardrobe.lua new file mode 100644 index 00000000000..950578df60a --- /dev/null +++ b/modules/phoenix/lua/custom/cosmetic_wardrobe.lua @@ -0,0 +1,56 @@ +----------------------------------- +-- Cosmetic Wardrobe +-- Mog Wardrobe 8 opens at the full 80 slots and only accepts event and cosmetic items. +-- Launch leaves the wardrobes closed and the unlock system reopens 1-4 through content. +-- Wardrobe 8 is the cosmetic exception and is free for everyone. +-- +-- This file opens the wardrobe and publishes the item list. The cpp half, +-- modules/phoenix/cpp/cosmetic_wardrobe.cpp, rejects any move into Wardrobe 8 that is +-- not on the list. A rejected item snaps back to its original slot with a system +-- message. The server never moves it. Items already inside stay usable, and moving +-- items out is never restricted. The list itself lives in +-- modules/phoenix/lua/data/cosmetic_wardrobe_items.lua. +-- +-- Both halves read the setting at boot. Setting it false restores stock wardrobe +-- behavior on the next restart; wardrobe space already granted stays granted. A +-- missing list while enabled blocks everything, so a broken data file fails closed. +-- Enable by setting ENABLE_COSMETIC_WARDROBE = true in settings/main.lua, then run a +-- fresh cmake configure, rebuild xi_map, and restart. +----------------------------------- +require('modules/module_utils') +----------------------------------- +local m = Module:new('cosmetic_wardrobe', xi.settings.main.ENABLE_COSMETIC_WARDROBE == true) + +local cosmeticItems = require('modules/phoenix/lua/data/cosmetic_wardrobe_items') + +-- Publish the list as a set. The cpp filter reads xi.phoenix.cosmeticWardrobe on +-- every move into Wardrobe 8 and blocks everything if it is missing. +m:addOverride('xi.server.onServerStart', function() + local allowed = {} + for _, itemId in ipairs(cosmeticItems) do + -- A mistyped id fails silently at the filter. Flag it at boot and leave it out. + if GetReadOnlyItem(itemId) == nil then + print('cosmetic_wardrobe: item id ' .. itemId .. ' does not exist') + else + allowed[itemId] = true + end + end + + xi.phoenix = xi.phoenix or {} + xi.phoenix.cosmeticWardrobe = allowed + + -- The publish sits above this call on purpose. An error from another module's + -- override must not leave the filter running with no list. + super() +end) + +-- Wardrobe 8 is closed at character creation. Open it to the full 80 slots on every +-- login so new and existing characters both get it. +m:addOverride('xi.player.onGameIn', function(player, firstLogin, zoning) + super(player, firstLogin, zoning) + + local size = player:getContainerSize(xi.inv.WARDROBE8) + if size < 80 then + player:changeContainerSize(xi.inv.WARDROBE8, 80 - size) + end +end) diff --git a/modules/phoenix/lua/data/cosmetic_wardrobe_items.lua b/modules/phoenix/lua/data/cosmetic_wardrobe_items.lua new file mode 100644 index 00000000000..50955521b15 --- /dev/null +++ b/modules/phoenix/lua/data/cosmetic_wardrobe_items.lua @@ -0,0 +1,165 @@ +----------------------------------- +-- Cosmetic Wardrobe Items +-- The items Mog Wardrobe 8 accepts. +-- Everything here comes from an event or a promotion and was obtainable on retail +-- before the June 2010 Abyssea release, except the hand picked items at the bottom +-- which are Beta Tester rewards. +-- Much of the older event gear has no xi.item enum entry. The list uses raw ids +-- throughout so every section reads the same. +-- To allow a new item, append its item_basic id with a name comment. +-- Event furnishings are not listed. The server only lets equipment and weapons into +-- a wardrobe, so furniture never reaches this check. +----------------------------------- +local cosmeticItems = +{ + -- Event and seasonal gear from storage slip 11. + 15297, -- rabbit_belt + 15298, -- worm_belt + 15299, -- mandragora_belt + 15919, -- drovers_belt + 15921, -- detonator_belt + 18166, -- happy_egg + 18167, -- fortune_egg + 18256, -- orphic_egg + 13216, -- gold_moogle_belt + 13217, -- silver_moogle_belt + 13218, -- bronze_moogle_belt + 15455, -- red_sash + 15456, -- dash_sash + 18863, -- dream_bell + 18864, -- dream_bell_+1 + 15178, -- dream_hat + 14519, -- dream_robe + 15752, -- dream_boots + 15179, -- dream_hat_+1 + 14520, -- dream_robe_+1 + 15753, -- dream_boots_+1 + + -- Older event gear. Festival weapons, seasonal hats, yukatas, and the swimsuit sets. + 18846, -- battledore + 18399, -- charm_wand + 18400, -- charm_wand_+1 + 18844, -- miracle_wand + 18845, -- miracle_wand_+1 + 17830, -- wooden_katana + 17831, -- hardwood_katana + 18436, -- lotus_katana + 18441, -- shinai + 17748, -- ibushi_shinai + 17749, -- ibushi_shinai_+1 + 17565, -- trick_staff + 17566, -- treat_staff + 17588, -- treat_staff_ii + 18102, -- pitchfork + 18103, -- pitchfork_+1 + 17074, -- chocobo_wand + 18401, -- moogle_rod + 16182, -- town_moogle_shield + 16183, -- nomad_moogle_shield + 16109, -- egg_helm + 13916, -- pumpkin_head + 13917, -- horror_head + 15176, -- pumpkin_head_ii + 15177, -- horror_head_ii + 16075, -- witch_hat + 16076, -- coven_hat + 11490, -- snow_bunny_hat + 11491, -- snow_bunny_hat_+1 + 15198, -- sprout_beret + 15199, -- guide_beret + 15204, -- mandragora_beret + 16144, -- sol_cap + 16145, -- lunar_cap + 16118, -- moogle_cap + 16119, -- nomad_cap + 11500, -- chocobo_beret + 16120, -- redeyes + 13819, -- onoko_yukata + 13821, -- lords_yukata + 13820, -- omina_yukata + 13822, -- ladys_yukata + 14532, -- otoko_yukata + 14534, -- otokogimi_yukata + 14533, -- onago_yukata + 14535, -- onnagimi_yukata + 11316, -- otokogusa_yukata + 11318, -- otokoeshi_yukata + 11317, -- onnagusa_yukata + 11319, -- ominaeshi_yukata + 14450, -- hume_gilet + 14457, -- hume_gilet_+1 + 14451, -- hume_top + 14458, -- hume_top_+1 + 14452, -- elvaan_gilet + 14459, -- elvaan_gilet_+1 + 14453, -- elvaan_top + 14460, -- elvaan_top_+1 + 14454, -- tarutaru_maillot + 14461, -- tarutaru_maillot_+1 + 14471, -- tarutaru_top + 14472, -- tarutaru_top_+1 + 14455, -- mithra_top + 14462, -- mithra_top_+1 + 14456, -- galka_gilet + 14463, -- galka_gilet_+1 + 11265, -- custom_gilet + 11273, -- custom_gilet_+1 + 11266, -- custom_top + 11274, -- custom_top_+1 + 11267, -- magna_gilet + 11275, -- magna_gilet_+1 + 11268, -- magna_top + 11276, -- magna_top_+1 + 11269, -- wonder_maillot + 11277, -- wonder_maillot_+1 + 11270, -- wonder_top + 11278, -- wonder_top_+1 + 11271, -- savage_top + 11279, -- savage_top_+1 + 11272, -- elder_gilet + 11280, -- elder_gilet_+1 + 15408, -- hume_trunks + 15415, -- hume_trunks_+1 + 15409, -- hume_shorts + 15416, -- hume_shorts_+1 + 15410, -- elvaan_trunks + 15417, -- elvaan_trunks_+1 + 15411, -- elvaan_shorts + 15418, -- elvaan_shorts_+1 + 15412, -- tarutaru_trunks + 15419, -- tarutaru_trunks_+1 + 15423, -- tarutaru_shorts + 15424, -- tarutaru_shorts_+1 + 15413, -- mithra_shorts + 15420, -- mithra_shorts_+1 + 15414, -- galka_trunks + 15421, -- galka_trunks_+1 + 16321, -- custom_trunks + 16329, -- custom_trunks_+1 + 16322, -- custom_shorts + 16330, -- custom_shorts_+1 + 16323, -- magna_trunks + 16331, -- magna_trunks_+1 + 16324, -- magna_shorts + 16332, -- magna_shorts_+1 + 16325, -- wonder_trunks + 16333, -- wonder_trunks_+1 + 16326, -- wonder_shorts + 16334, -- wonder_shorts_+1 + 16327, -- savage_shorts + 16335, -- savage_shorts_+1 + 16328, -- elder_trunks + 16336, -- elder_trunks_+1 + 11300, -- eerie_cloak + 11301, -- eerie_cloak_+1 + 11355, -- dinner_jacket + 16378, -- dinner_hose + 11290, -- tidal_talisman + 16243, -- drovers_mantle + 17587, -- trick_staff_ii + + -- Beta Tester rewards + 26730, -- celeste_cap +} + +return cosmeticItems diff --git a/settings/default/main.lua b/settings/default/main.lua index 3122b51dee9..c1309efc456 100644 --- a/settings/default/main.lua +++ b/settings/default/main.lua @@ -336,6 +336,7 @@ xi.settings.main = REGIME_REWARD_THRESHOLD = 15, -- If the player is more than N levels below the minimum suggested range, do not award experience. PERSIST_SEAL_TIMERS = false, -- Persist seal (Beastmen/Kindred) recast timers across zone changes and logout. GUILD_SHOP_HOLIDAYS = false, -- true/false. Close each guild shop on its weekly holiday. + ENABLE_COSMETIC_WARDROBE = false, -- true/false. Mog Wardrobe 8 opens at 80 slots and only accepts the cosmetic item list. Requires the cosmetic_wardrobe modules. -- SYSTEM DISABLE_INACTIVITY_WATCHDOG = false, -- true/false. If this is enabled, the watchdog which detects if the main loop isn't being ticked will no longer be able to kill the process.