Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions modules/phoenix/cpp/cosmetic_wardrobe.cpp
Original file line number Diff line number Diff line change
@@ -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<bool>("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<uint16>(GP_CLI_COMMAND_ITEM_MOVE::packetId))
{
return false;
}

if (!enabled)
{
return false;
}

// Rearranging within the wardrobe is not storage.
const auto* packet = data.as<GP_CLI_COMMAND_ITEM_MOVE>();
if (static_cast<CONTAINER_ID>(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<GP_SERV_COMMAND_ITEM_ATTR>(PItem, static_cast<CONTAINER_ID>(packet->Category1), packet->ItemIndex1);
PChar->pushPacket<GP_SERV_COMMAND_ITEM_SAME>(PChar);
PChar->pushPacket<GP_SERV_COMMAND_CHAT_STD>(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<sol::optional<sol::table>>();
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);
56 changes: 56 additions & 0 deletions modules/phoenix/lua/custom/cosmetic_wardrobe.lua
Original file line number Diff line number Diff line change
@@ -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)
165 changes: 165 additions & 0 deletions modules/phoenix/lua/data/cosmetic_wardrobe_items.lua
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions settings/default/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading