diff --git a/config.lua.dist b/config.lua.dist
index 3bb7a1b3b7..bf56a2aec1 100644
--- a/config.lua.dist
+++ b/config.lua.dist
@@ -89,7 +89,6 @@ mysqlSock = ""
-- intervals regardless of other actions such as item (potion) use. This setting
-- may cause high CPU usage with many players and potentially affect performance!
-- checkDuplicateStorageKeys checks the values stored in the variables for duplicates.
-allowChangeOutfit = true
freePremium = false
maxMessageBuffer = 4
emoteSpells = false
diff --git a/data/XML/mounts.xml b/data/XML/mounts.xml
deleted file mode 100644
index b50f8e885d..0000000000
--- a/data/XML/mounts.xml
+++ /dev/null
@@ -1,212 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/data/XML/outfits.xml b/data/XML/outfits.xml
deleted file mode 100644
index 0902c44aa3..0000000000
--- a/data/XML/outfits.xml
+++ /dev/null
@@ -1,226 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/data/cpplinter.lua b/data/cpplinter.lua
index 6d63776399..56fadcafad 100644
--- a/data/cpplinter.lua
+++ b/data/cpplinter.lua
@@ -63,12 +63,13 @@ configManager = {}
---@field getBestiary fun(): table
---@field getCurrencyItems fun(): table
---@field getItemTypeByClientId fun(clientId: number): ItemType
----@field getMountIdByLookType fun(lookType: number): number
---@field getParties fun(): table
---@field getTowns fun(): table
---@field getHouses fun(): table
---@field getOutfits fun(sex: number): table
+---@field getOutfitByLookType fun(lookType: number): Outfit_t
---@field getMounts fun(): table
+---@field getMountByLookType fun(lookType: number): table
---@field getVocations fun(): table
---@field getGameState fun(): string
---@field setGameState fun(state: string): boolean
@@ -422,12 +423,23 @@ Creature = {}
---@field removeOutfitAddon fun(self: Player, outfitId: number, addonId: number)
---@field hasOutfit fun(self: Player, outfitId: number, addon?: number): boolean
---@field canWearOutfit fun(self: Player, outfitId: number, addonId?: number): boolean
+---@field getCurrentOutfit fun(self: Player): Outfit
+---@field setCurrentOutfit fun(self: Player, outfit: Outfit)
+---@field getDefaultOutfit fun(self: Player): Outfit
+---@field setDefaultOutfit fun(self: Player, outfit: Outfit)
---@field sendOutfitWindow fun(self: Player)
----@field sendEditPodium fun(self: Player, item: Item)
+---@field getRandomizeMount fun(self: Player): boolean
+---@field setRandomizeMount fun(self: Player, randomize: boolean)
+---@field getLastMountToggle fun(self: Player): number
+---@field setLastMountToggle fun(self: Player, timestamp: number)
+---@field getCurrentMount fun(self: Player): number
+---@field mount fun(self: Player, mountId: number)
+---@field dismount fun(self: Player)
+---@field sendPodiumWindow fun(self: Player, item: Item)
---@field addMount fun(self: Player, mountId: number)
---@field removeMount fun(self: Player, mountId: number)
---@field hasMount fun(self: Player, mountId: number): boolean
----@field toggleMount fun(self: Player, active: boolean)
+---@field toggleMount fun(self: Player, active: boolean): boolean
---@field getPremiumEndsAt fun(self: Player): number
---@field setPremiumEndsAt fun(self: Player, timestamp: number)
---@field hasBlessing fun(self: Player, blessingId: number): boolean
@@ -1855,14 +1867,13 @@ RELOAD_TYPE_GLOBAL = 5
RELOAD_TYPE_GLOBALEVENTS = 6
RELOAD_TYPE_ITEMS = 7
RELOAD_TYPE_MONSTERS = 8
-RELOAD_TYPE_MOUNTS = 9
-RELOAD_TYPE_MOVEMENTS = 10
-RELOAD_TYPE_NPCS = 11
-RELOAD_TYPE_QUESTS = 12
-RELOAD_TYPE_SCRIPTS = 13
-RELOAD_TYPE_SPELLS = 14
-RELOAD_TYPE_TALKACTIONS = 15
-RELOAD_TYPE_WEAPONS = 16
+RELOAD_TYPE_MOVEMENTS = 9
+RELOAD_TYPE_NPCS = 10
+RELOAD_TYPE_QUESTS = 11
+RELOAD_TYPE_SCRIPTS = 12
+RELOAD_TYPE_SPELLS = 13
+RELOAD_TYPE_TALKACTIONS = 14
+RELOAD_TYPE_WEAPONS = 15
PlayerFlag_CannotUseCombat = 1 * 2 ^ 0
PlayerFlag_CannotAttackPlayer = 1 * 2 ^ 1
@@ -2039,44 +2050,43 @@ SKILL_LAST = SKILL_FISHING
---@type table
configKeys = {
-- ConfigKeysBoolean
- ALLOW_CHANGEOUTFIT = 0,
- ONE_PLAYER_ON_ACCOUNT = 1,
- AIMBOT_HOTKEY_ENABLED = 2,
- REMOVE_RUNE_CHARGES = 3,
- REMOVE_WEAPON_AMMO = 4,
- REMOVE_WEAPON_CHARGES = 5,
- REMOVE_POTION_CHARGES = 6,
- EXPERIENCE_FROM_PLAYERS = 7,
- FREE_PREMIUM = 8,
- REPLACE_KICK_ON_LOGIN = 9,
- ALLOW_CLONES = 10,
- ALLOW_WALKTHROUGH = 11,
- BIND_ONLY_GLOBAL_ADDRESS = 12,
- OPTIMIZE_DATABASE = 13,
- MARKET_PREMIUM = 14,
- EMOTE_SPELLS = 15,
- STAMINA_SYSTEM = 16,
- WARN_UNSAFE_SCRIPTS = 17,
- CONVERT_UNSAFE_SCRIPTS = 18,
- CLASSIC_EQUIPMENT_SLOTS = 19,
- CLASSIC_ATTACK_SPEED = 20,
- SCRIPTS_CONSOLE_LOGS = 21,
- SERVER_SAVE_NOTIFY_MESSAGE = 22,
- SERVER_SAVE_CLEAN_MAP = 23,
- SERVER_SAVE_CLOSE = 24,
- SERVER_SAVE_SHUTDOWN = 25,
- ONLINE_OFFLINE_CHARLIST = 26,
- YELL_ALLOW_PREMIUM = 27,
- PREMIUM_TO_SEND_PRIVATE = 28,
- HOUSE_OWNED_BY_ACCOUNT = 29,
- CLEAN_PROTECTION_ZONES = 30,
- HOUSE_DOOR_SHOW_PRICE = 31,
- ONLY_INVITED_CAN_MOVE_HOUSE_ITEMS = 32,
- REMOVE_ON_DESPAWN = 33,
- TWO_FACTOR_AUTH = 34,
- MANASHIELD_BREAKABLE = 35,
- CHECK_DUPLICATE_STORAGE_KEYS = 36,
- MONSTER_OVERSPAWN = 37,
+ ONE_PLAYER_ON_ACCOUNT = 0,
+ AIMBOT_HOTKEY_ENABLED = 1,
+ REMOVE_RUNE_CHARGES = 2,
+ REMOVE_WEAPON_AMMO = 3,
+ REMOVE_WEAPON_CHARGES = 4,
+ REMOVE_POTION_CHARGES = 5,
+ EXPERIENCE_FROM_PLAYERS = 6,
+ FREE_PREMIUM = 7,
+ REPLACE_KICK_ON_LOGIN = 8,
+ ALLOW_CLONES = 9,
+ ALLOW_WALKTHROUGH = 10,
+ BIND_ONLY_GLOBAL_ADDRESS = 11,
+ OPTIMIZE_DATABASE = 12,
+ MARKET_PREMIUM = 13,
+ EMOTE_SPELLS = 14,
+ STAMINA_SYSTEM = 15,
+ WARN_UNSAFE_SCRIPTS = 16,
+ CONVERT_UNSAFE_SCRIPTS = 17,
+ CLASSIC_EQUIPMENT_SLOTS = 18,
+ CLASSIC_ATTACK_SPEED = 19,
+ SCRIPTS_CONSOLE_LOGS = 20,
+ SERVER_SAVE_NOTIFY_MESSAGE = 21,
+ SERVER_SAVE_CLEAN_MAP = 22,
+ SERVER_SAVE_CLOSE = 23,
+ SERVER_SAVE_SHUTDOWN = 24,
+ ONLINE_OFFLINE_CHARLIST = 25,
+ YELL_ALLOW_PREMIUM = 26,
+ PREMIUM_TO_SEND_PRIVATE = 27,
+ HOUSE_OWNED_BY_ACCOUNT = 28,
+ CLEAN_PROTECTION_ZONES = 29,
+ HOUSE_DOOR_SHOW_PRICE = 30,
+ ONLY_INVITED_CAN_MOVE_HOUSE_ITEMS = 31,
+ REMOVE_ON_DESPAWN = 32,
+ TWO_FACTOR_AUTH = 33,
+ MANASHIELD_BREAKABLE = 34,
+ CHECK_DUPLICATE_STORAGE_KEYS = 35,
+ MONSTER_OVERSPAWN = 36,
-- ConfigKeysString
MAP_NAME = 0,
diff --git a/data/lib/compat/compat.lua b/data/lib/compat/compat.lua
index 05a97d4e6f..3fac9bdc93 100644
--- a/data/lib/compat/compat.lua
+++ b/data/lib/compat/compat.lua
@@ -1518,17 +1518,6 @@ do
end
end
-do
- local mounts = {}
- for _, mountData in pairs(Game.getMounts()) do
- mounts[mountData.clientId] = mountData.name
- end
-
- function getMountNameByLookType(lookType)
- return mounts[lookType]
- end
-end
-
function indexToCombatType(idx)
return 1 << idx
end
diff --git a/data/lib/core/network_message.lua b/data/lib/core/network_message.lua
index 7cb867c44b..5f5d074425 100644
--- a/data/lib/core/network_message.lua
+++ b/data/lib/core/network_message.lua
@@ -9,3 +9,21 @@ end
function NetworkMessage:addBool(value)
self:addByte(value and 1 or 0)
end
+
+function NetworkMessage:addItemId(itemId)
+ local it = ItemType(itemId)
+ self:addU16(it:getClientId())
+end
+
+function NetworkMessage:addOutfit(outfit)
+ self:addU16(outfit.lookType)
+ if outfit.lookType ~= 0 then
+ self:addByte(outfit.lookHead)
+ self:addByte(outfit.lookBody)
+ self:addByte(outfit.lookLegs)
+ self:addByte(outfit.lookFeet)
+ self:addByte(outfit.lookAddons)
+ else
+ self:addItemId(outfit.lookTypeEx)
+ end
+end
diff --git a/data/lib/core/player.lua b/data/lib/core/player.lua
index 86c1468b8f..ccc8339ee9 100644
--- a/data/lib/core/player.lua
+++ b/data/lib/core/player.lua
@@ -337,22 +337,6 @@ function Player.getTotalMoney(self)
return self:getMoney() + self:getBankBalance()
end
-function Player.addAddonToAllOutfits(self, addon)
- for sex = 0, 1 do
- local outfits = Game.getOutfits(sex)
- for outfit = 1, #outfits do
- self:addOutfitAddon(outfits[outfit].lookType, addon)
- end
- end
-end
-
-function Player.addAllMounts(self)
- local mounts = Game.getMounts()
- for mount = 1, #mounts do
- self:addMount(mounts[mount].id)
- end
-end
-
function Player.setSpecialContainersAvailable(self, available)
local msg = NetworkMessage()
msg:addByte(0x2A)
diff --git a/data/lib/core/storages.lua b/data/lib/core/storages.lua
index 0f260029d5..31efe320cb 100644
--- a/data/lib/core/storages.lua
+++ b/data/lib/core/storages.lua
@@ -2,7 +2,6 @@
Reserved player storage ranges:
- 300000 to 301000+ reserved for achievements
- 20000 to 21000+ reserved for achievement progress
-- 10000000 to 20000000 reserved for outfits and mounts on source
]]--
AccountStorageKeys = {
@@ -49,4 +48,10 @@ PlayerStorageKeys = {
-- Bestiary:
bestiaryKillsBase = 400000,
bestiaryTrackerBase = 500000,
+
+ -- Outfits and mounts:
+ currentMount = 60000,
+ randomizeMount = 60001,
+ outfitsBase = 600000,
+ mountsBase = 610000,
}
diff --git a/data/migrations/37.lua b/data/migrations/37.lua
index d0ffd9c0cb..eea226154e 100644
--- a/data/migrations/37.lua
+++ b/data/migrations/37.lua
@@ -1,3 +1,89 @@
+-- must match PlayerStorageKeys in storages.lua, change accordingly if modified
+local CURRENT_MOUNT = 60000
+local RANDOMIZE_MOUNT = 60001
+local OUTFITS_BASE = 600000
+local MOUNTS_BASE = 610000
+
function onUpdateDatabase()
- return false
+ print("> Updating database to version 38 (revert outfits/mounts to storages)")
+
+ local tx = DBTransaction()
+ if not tx.begin() then
+ return false
+ end
+
+ local query = DBInsert("INSERT INTO `player_storage` (`player_id`, `key`, `value`) VALUES ")
+
+ do
+ local resultId = db.storeQuery("SELECT `player_id`, `outfit_id`, `addons` FROM `player_outfits`")
+ if resultId then
+ repeat
+ local playerId = result.getNumber(resultId, "player_id")
+ local outfitId = result.getNumber(resultId, "outfit_id")
+ local addons = result.getNumber(resultId, "addons")
+
+ local storageKey = OUTFITS_BASE + outfitId
+ query:addRow(string.format("%d, %d, %d", playerId, storageKey, addons))
+ until not result.next(resultId)
+ result.free(resultId)
+ end
+ end
+
+ do
+ local resultId = db.storeQuery("SELECT `player_id`, `mount_id` FROM `player_mounts`")
+ if resultId then
+ repeat
+ local playerId = result.getNumber(resultId, "player_id")
+ local mountId = result.getNumber(resultId, "mount_id")
+
+ local storageKey = MOUNTS_BASE + mountId
+ query:addRow(string.format("%d, %d, %d", playerId, storageKey, 1))
+ until not result.next(resultId)
+ result.free(resultId)
+ end
+ end
+
+ -- Migrate currentmount and randomizemount from players table
+ do
+ local resultId = db.storeQuery(
+ "SELECT `id`, `currentmount`, `randomizemount` FROM `players` WHERE `currentmount` > 0 OR `randomizemount` > 0")
+ if resultId then
+ repeat
+ local playerId = result.getNumber(resultId, "id")
+ local currentMount = result.getNumber(resultId, "currentmount")
+ local randomizeMount = result.getNumber(resultId, "randomizemount")
+
+ if currentMount > 0 then
+ query:addRow(string.format("%d, %d, %d", playerId, CURRENT_MOUNT, currentMount))
+ end
+
+ if randomizeMount > 0 then
+ query:addRow(string.format("%d, %d, %d", playerId, RANDOMIZE_MOUNT, randomizeMount))
+ end
+ until not result.next(resultId)
+ result.free(resultId)
+ end
+ end
+
+ if not query:execute() then
+ tx.rollback()
+ return false
+ end
+
+ if not db.query("DROP TABLE IF EXISTS `player_outfits`") then
+ tx.rollback()
+ return false
+ end
+
+ if not db.query("DROP TABLE IF EXISTS `player_mounts`") then
+ tx.rollback()
+ return false
+ end
+
+ if not db.query("ALTER TABLE `players` DROP COLUMN `currentmount`, DROP COLUMN `randomizemount`") then
+ tx.rollback()
+ return false
+ end
+
+ return tx.commit()
end
diff --git a/data/migrations/38.lua b/data/migrations/38.lua
new file mode 100644
index 0000000000..d0ffd9c0cb
--- /dev/null
+++ b/data/migrations/38.lua
@@ -0,0 +1,3 @@
+function onUpdateDatabase()
+ return false
+end
diff --git a/data/scripts/events/player.lua b/data/scripts/events/player.lua
index 42ea256ef3..b9d91d401d 100644
--- a/data/scripts/events/player.lua
+++ b/data/scripts/events/player.lua
@@ -100,7 +100,7 @@ function Player:onPodiumRequest(item)
return
end
- self:sendEditPodium(item)
+ self:sendPodiumWindow(item)
end
function Player:onPodiumEdit(item, outfit, direction, isVisible)
@@ -122,8 +122,8 @@ function Player:onPodiumEdit(item, outfit, direction, isVisible)
end
-- reset mount if unable to ride
- local mount = Game.getMountIdByLookType(outfit.lookMount)
- if not (mount and self:hasMount(mount)) then
+ local mount = Game.getMountByLookType(outfit.lookMount)
+ if not mount or not self:hasMount(mount.lookType) then
outfit.lookMount = 0
end
end
diff --git a/data/scripts/network/outfit.lua b/data/scripts/network/outfit.lua
deleted file mode 100644
index 37571e511c..0000000000
--- a/data/scripts/network/outfit.lua
+++ /dev/null
@@ -1,11 +0,0 @@
-local handler = PacketHandler(0xD2)
-
-function handler.onReceive(player, msg)
- if not configManager.getBoolean(configKeys.ALLOW_CHANGEOUTFIT) then
- return
- end
-
- player:sendOutfitWindow()
-end
-
-handler:register()
diff --git a/data/scripts/network/toggle_mount.lua b/data/scripts/network/toggle_mount.lua
deleted file mode 100644
index 36da2c6045..0000000000
--- a/data/scripts/network/toggle_mount.lua
+++ /dev/null
@@ -1,8 +0,0 @@
-local handler = PacketHandler(0xD4)
-
-function handler.onReceive(player, msg)
- local mount = msg:getByte() ~= 0
- player:toggleMount(mount)
-end
-
-handler:register()
diff --git a/data/scripts/systems/outfits/commands/add_addon.lua b/data/scripts/systems/outfits/commands/add_addon.lua
new file mode 100644
index 0000000000..4e22bcab46
--- /dev/null
+++ b/data/scripts/systems/outfits/commands/add_addon.lua
@@ -0,0 +1,58 @@
+-- /addaddon , ,
+-- Adds addon 1 or 2 to an owned outfit for the target player.
+local talkaction = TalkAction("/addaddon")
+
+function talkaction.onSay(player, words, param)
+ local split = param:splitTrimmed(",")
+ if #split < 3 then
+ player:sendCancelMessage("Insufficient parameters.")
+ return false
+ end
+
+ local target = Player(split[1])
+ if not target then
+ player:sendCancelMessage("A player with that name is not online.")
+ return false
+ end
+
+ local outfit = Game.getOutfit(split[2], target:getSex())
+ if not outfit then
+ player:sendCancelMessage("Outfit " .. split[2] .. " does not exist.")
+ return false
+ end
+
+ if not target:hasOutfit(outfit.lookType) then
+ player:sendCancelMessage("Target does not have this outfit.")
+ return false
+ end
+
+ local addon = tonumber(split[3])
+ if addon ~= 1 and addon ~= 2 then
+ player:sendCancelMessage("Invalid addon value.")
+ return false
+ end
+
+ if target:hasOutfitAddon(outfit.lookType, addon) then
+ player:sendCancelMessage("Target already has this outfit with this addon.")
+ return false
+ end
+
+ if not target:addOutfitAddon(outfit.lookType, addon) then
+ player:sendCancelMessage("Failed to add addon to the outfit.")
+ return false
+ end
+
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "You have added addon " .. addon .. " for outfit " .. outfit.name ..
+ " to " .. target:getName() .. ".")
+
+ if Outfits.PrintCommandsToConsole then
+ print(player:getName() .. " has granted addon " .. addon .. " for outfit " .. outfit.name .. " to " ..
+ target:getName() .. ".")
+ end
+
+ return true
+end
+
+talkaction:separator(" ")
+talkaction:accountType(ACCOUNT_TYPE_GAMEMASTER)
+talkaction:register()
diff --git a/data/scripts/systems/outfits/commands/add_mount.lua b/data/scripts/systems/outfits/commands/add_mount.lua
new file mode 100644
index 0000000000..0f8f0ee645
--- /dev/null
+++ b/data/scripts/systems/outfits/commands/add_mount.lua
@@ -0,0 +1,46 @@
+-- /addmount ,
+-- Grants the mount to the target player if not already owned.
+local talkaction = TalkAction("/addmount")
+
+function talkaction.onSay(player, words, param)
+ local split = param:splitTrimmed(",")
+ if #split < 2 then
+ player:sendCancelMessage("Insufficient parameters.")
+ return false
+ end
+
+ local target = Player(split[1])
+ if not target then
+ player:sendCancelMessage("A player with that name is not online.")
+ return false
+ end
+
+ local mount = Game.getMount(split[2])
+ if not mount then
+ player:sendCancelMessage("Mount " .. split[2] .. " does not exist.")
+ return false
+ end
+
+ if target:hasMount(mount.lookType) then
+ player:sendCancelMessage("Target already has this mount.")
+ return false
+ end
+
+ if not target:addMount(mount.lookType) then
+ player:sendCancelMessage("Failed to add the mount.")
+ return false
+ end
+
+ player:sendTextMessage(MESSAGE_INFO_DESCR,
+ "You have granted mount " .. mount.name .. " to " .. target:getName() .. ".")
+
+ if Outfits.PrintCommandsToConsole then
+ print(player:getName() .. " has granted mount " .. mount.name .. " to " .. target:getName() .. ".")
+ end
+
+ return true
+end
+
+talkaction:separator(" ")
+talkaction:accountType(ACCOUNT_TYPE_GAMEMASTER)
+talkaction:register()
diff --git a/data/scripts/systems/outfits/commands/add_outfit.lua b/data/scripts/systems/outfits/commands/add_outfit.lua
new file mode 100644
index 0000000000..46f4c765fc
--- /dev/null
+++ b/data/scripts/systems/outfits/commands/add_outfit.lua
@@ -0,0 +1,46 @@
+-- /addoutfit ,
+-- Grants the outfit to the target player if not already owned.
+local talkaction = TalkAction("/addoutfit")
+
+function talkaction.onSay(player, words, param)
+ local split = param:splitTrimmed(",")
+ if #split < 2 then
+ player:sendCancelMessage("Insufficient parameters.")
+ return false
+ end
+
+ local target = Player(split[1])
+ if not target then
+ player:sendCancelMessage("A player with that name is not online.")
+ return false
+ end
+
+ local outfit = Game.getOutfit(split[2], target:getSex())
+ if not outfit then
+ player:sendCancelMessage("Outfit " .. split[2] .. " does not exist.")
+ return false
+ end
+
+ if target:hasOutfit(outfit.lookType) then
+ player:sendCancelMessage("Target already has this outfit.")
+ return false
+ end
+
+ if not target:addOutfit(outfit.lookType) then
+ player:sendCancelMessage("Failed to add the outfit.")
+ return false
+ end
+
+ player:sendTextMessage(MESSAGE_INFO_DESCR,
+ "You have granted outfit " .. outfit.name .. " to " .. target:getName() .. ".")
+
+ if Outfits.PrintCommandsToConsole then
+ print(player:getName() .. " has granted outfit " .. outfit.name .. " to " .. target:getName() .. ".")
+ end
+
+ return true
+end
+
+talkaction:separator(" ")
+talkaction:accountType(ACCOUNT_TYPE_GAMEMASTER)
+talkaction:register()
diff --git a/data/scripts/systems/outfits/commands/remove_addon.lua b/data/scripts/systems/outfits/commands/remove_addon.lua
new file mode 100644
index 0000000000..84354c6a51
--- /dev/null
+++ b/data/scripts/systems/outfits/commands/remove_addon.lua
@@ -0,0 +1,58 @@
+-- /removeaddon , ,
+-- Removes addon 1 or 2 from an owned outfit for the target player.
+local talkaction = TalkAction("/removeaddon")
+
+function talkaction.onSay(player, words, param)
+ local split = param:splitTrimmed(",")
+ if #split < 3 then
+ player:sendCancelMessage("Insufficient parameters.")
+ return false
+ end
+
+ local target = Player(split[1])
+ if not target then
+ player:sendCancelMessage("A player with that name is not online.")
+ return false
+ end
+
+ local outfit = Game.getOutfit(split[2], target:getSex())
+ if not outfit then
+ player:sendCancelMessage("Outfit " .. split[2] .. " does not exist.")
+ return false
+ end
+
+ if not target:hasOutfit(outfit.lookType) then
+ player:sendCancelMessage("Target does not have this outfit.")
+ return false
+ end
+
+ local addon = tonumber(split[3])
+ if addon ~= 1 and addon ~= 2 then
+ player:sendCancelMessage("Invalid addon value.")
+ return false
+ end
+
+ if not target:hasOutfitAddon(outfit.lookType, addon) then
+ player:sendCancelMessage("Target does not have this outfit with this addon.")
+ return false
+ end
+
+ if not target:removeOutfitAddon(outfit.lookType, addon) then
+ player:sendCancelMessage("Failed to remove the addon from the outfit.")
+ return false
+ end
+
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "You have removed addon " .. addon .. " for outfit " .. outfit.name ..
+ " from " .. target:getName() .. ".")
+
+ if Outfits.PrintCommandsToConsole then
+ print(player:getName() .. " has removed addon " .. addon .. " for outfit " .. outfit.name .. " from " ..
+ target:getName() .. ".")
+ end
+
+ return true
+end
+
+talkaction:separator(" ")
+talkaction:accountType(ACCOUNT_TYPE_GAMEMASTER)
+talkaction:register()
diff --git a/data/scripts/systems/outfits/commands/remove_mount.lua b/data/scripts/systems/outfits/commands/remove_mount.lua
new file mode 100644
index 0000000000..bb58fc1a2c
--- /dev/null
+++ b/data/scripts/systems/outfits/commands/remove_mount.lua
@@ -0,0 +1,46 @@
+-- /removemount ,
+-- Removes the mount from the target player; dismounts if currently used.
+local talkaction = TalkAction("/removemount")
+
+function talkaction.onSay(player, words, param)
+ local split = param:splitTrimmed(",")
+ if #split < 2 then
+ player:sendCancelMessage("Insufficient parameters.")
+ return false
+ end
+
+ local target = Player(split[1])
+ if not target then
+ player:sendCancelMessage("A player with that name is not online.")
+ return false
+ end
+
+ local mount = Game.getMount(split[2])
+ if not mount then
+ player:sendCancelMessage("Mount " .. split[2] .. " does not exist.")
+ return false
+ end
+
+ if not target:hasMount(mount.lookType) then
+ player:sendCancelMessage("Target does not have this mount.")
+ return false
+ end
+
+ if not target:removeMount(mount.lookType) then
+ player:sendCancelMessage("Failed to remove the mount.")
+ return false
+ end
+
+ player:sendTextMessage(MESSAGE_INFO_DESCR,
+ "You have removed mount " .. mount.name .. " from " .. target:getName() .. ".")
+
+ if Outfits.PrintCommandsToConsole then
+ print(player:getName() .. " has removed mount " .. mount.name .. " from " .. target:getName() .. ".")
+ end
+
+ return true
+end
+
+talkaction:separator(" ")
+talkaction:accountType(ACCOUNT_TYPE_GAMEMASTER)
+talkaction:register()
diff --git a/data/scripts/systems/outfits/commands/remove_outfit.lua b/data/scripts/systems/outfits/commands/remove_outfit.lua
new file mode 100644
index 0000000000..dce2f8c77f
--- /dev/null
+++ b/data/scripts/systems/outfits/commands/remove_outfit.lua
@@ -0,0 +1,46 @@
+-- /removeoutfit ,
+-- Removes the owned outfit from the target player.
+local talkaction = TalkAction("/removeoutfit")
+
+function talkaction.onSay(player, words, param)
+ local split = param:splitTrimmed(",")
+ if #split < 2 then
+ player:sendCancelMessage("Insufficient parameters.")
+ return false
+ end
+
+ local target = Player(split[1])
+ if not target then
+ player:sendCancelMessage("A player with that name is not online.")
+ return false
+ end
+
+ local outfit = Game.getOutfit(split[2], target:getSex())
+ if not outfit then
+ player:sendCancelMessage("Outfit " .. split[2] .. " does not exist.")
+ return false
+ end
+
+ if not target:hasOutfit(outfit.lookType) then
+ player:sendCancelMessage("Target does not have this outfit.")
+ return false
+ end
+
+ if not target:removeOutfit(outfit.lookType) then
+ player:sendCancelMessage("Failed to remove the outfit.")
+ return false
+ end
+
+ player:sendTextMessage(MESSAGE_INFO_DESCR,
+ "You have removed outfit " .. outfit.name .. " from " .. target:getName() .. ".")
+
+ if Outfits.PrintCommandsToConsole then
+ print(player:getName() .. " has removed outfit " .. outfit.name .. " from " .. target:getName() .. ".")
+ end
+
+ return true
+end
+
+talkaction:separator(" ")
+talkaction:accountType(ACCOUNT_TYPE_GAMEMASTER)
+talkaction:register()
diff --git a/data/scripts/systems/outfits/config.lua b/data/scripts/systems/outfits/config.lua
new file mode 100644
index 0000000000..e8ae3abacc
--- /dev/null
+++ b/data/scripts/systems/outfits/config.lua
@@ -0,0 +1,24 @@
+-- Outfits & Mounts module configuration.
+--
+-- Networking entry points:
+-- - 0xD2: systems/outfits/network/request_outfit_window.lua
+-- - 0xD3: systems/outfits/network/set_outfit.lua
+-- - 0xD4: systems/outfits/network/toggle_mount.lua
+--
+-- Core behavior:
+-- - Mount selection/toggling: systems/outfits/core/mounts.lua
+-- - Outfit/podium windows: systems/outfits/core/windows.lua
+-- - Outfit ownership/addons: systems/outfits/core/outfits.lua
+Outfits = {
+ -- Set false to block outfit changes via incoming packets (0xD2/0xD3).
+ AllowChangeOutfit = true,
+
+ -- Set false to block mount toggles via incoming packets (0xD4).
+ AllowToggleMount = true,
+
+ -- Cooldown (ms) enforced for manual mount toggles (Ctrl+R). Forced zone toggles may bypass this.
+ ToggleMountCooldown = 3000,
+
+ -- Print outfit/mount command usage to console.
+ PrintCommandsToConsole = true
+}
diff --git a/data/scripts/systems/outfits/core/mounts.lua b/data/scripts/systems/outfits/core/mounts.lua
new file mode 100644
index 0000000000..8b37e0fe55
--- /dev/null
+++ b/data/scripts/systems/outfits/core/mounts.lua
@@ -0,0 +1,239 @@
+-- Mount ownership, selection, and toggling.
+--
+-- Persistent state:
+-- - PlayerStorageKeys.mountsBase + lookType: mount ownership
+-- - PlayerStorageKeys.currentMount: selected mount lookType (used when mounting)
+-- - PlayerStorageKeys.randomizeMount: whether to select a random owned mount when mounting
+--
+-- Session-only state:
+-- - lastMountToggle[playerId]: used for Outfits.ToggleMountCooldown
+-- - wasMounted[playerId]: remembers intent while forcibly dismounted in protection zones
+do
+ local lastMountToggle = {}
+ function Player.getLastMountToggle(self)
+ return lastMountToggle[self:getId()] or 0
+ end
+
+ function Player.setLastMountToggle(self, time)
+ lastMountToggle[self:getId()] = time
+ end
+end
+
+do
+ local wasMounted = {}
+ function Player.getWasMounted(self)
+ return wasMounted[self:getId()] or false
+ end
+
+ function Player.setWasMounted(self, mounted)
+ wasMounted[self:getId()] = mounted or nil
+ end
+end
+
+function Player.addMount(self, mountId)
+ return self:setStorageValue(PlayerStorageKeys.mountsBase + mountId, 1)
+end
+
+function Player.addAllMounts(self)
+ local mounts = Game.getMounts()
+ for _, mount in ipairs(mounts) do
+ self:addMount(mount.lookType)
+ end
+end
+
+function Player.hasMount(self, mountId)
+ local value = self:getStorageValue(PlayerStorageKeys.mountsBase + mountId)
+ return value ~= nil and value ~= -1
+end
+
+function Player.removeMount(self, mountId)
+ local value = self:removeStorageValue(PlayerStorageKeys.mountsBase + mountId)
+ if self:getCurrentMount() == mountId and self:isMounted() then
+ self:dismount()
+ end
+ return value
+end
+
+function Player.removeAllMounts(self)
+ local mounts = Game.getMounts()
+ for _, mount in ipairs(mounts) do
+ self:removeMount(mount.lookType)
+ end
+
+ if self:isMounted() then
+ self:dismount()
+ end
+end
+
+-- Returns the selected mount lookType stored in PlayerStorageKeys.currentMount, or nil if unset.
+function Player.getCurrentMount(self)
+ local value = self:getStorageValue(PlayerStorageKeys.currentMount)
+ if value == nil or value == -1 then
+ return nil
+ end
+ return value
+end
+
+-- Sets the selected mount lookType (nil clears). For non-staff players, the mount must be owned.
+function Player.setCurrentMount(self, mountId)
+ if mountId == nil then
+ return self:removeStorageValue(PlayerStorageKeys.currentMount)
+ end
+
+ if not self:getGroup():getAccess() and not self:hasMount(mountId) then
+ return false
+ end
+
+ return self:setStorageValue(PlayerStorageKeys.currentMount, mountId)
+end
+
+function Player.getRandomizeMount(self)
+ local randomizeMount = self:getStorageValue(PlayerStorageKeys.randomizeMount)
+ return randomizeMount ~= nil and randomizeMount ~= -1
+end
+
+function Player.setRandomizeMount(self, randomize)
+ if randomize then
+ return self:setStorageValue(PlayerStorageKeys.randomizeMount, 1)
+ end
+ return self:removeStorageValue(PlayerStorageKeys.randomizeMount)
+end
+
+-- Returns whether the player can ride the given mount lookType.
+function Player.canRideMount(self, mountId)
+ if self:getGroup():getAccess() then
+ return true
+ end
+
+ local mount = Game.getMountByLookType(mountId)
+ if not mount then
+ return false
+ end
+
+ if mount.premium and not self:isPremium() then
+ return false
+ end
+
+ return self:hasMount(mount.lookType)
+end
+
+function Player.isMounted(self)
+ return self:getOutfit().lookMount ~= 0
+end
+
+-- Mounts the given mount object (from Game.getMountByLookType): updates outfit + speed.
+function Player.mount(self, mount)
+ if mount == nil or mount.lookType == nil or mount.speed == nil then
+ return false
+ end
+
+ local outfit = self:getDefaultOutfit()
+ outfit.lookMount = mount.lookType
+ self:setOutfit(outfit)
+ self:changeSpeed(mount.speed)
+ return true
+end
+
+-- Dismounts the current mount: clears outfit mount + removes speed bonus.
+function Player.dismount(self)
+ local outfit = self:getDefaultOutfit()
+ local lookMount = outfit.lookMount
+ outfit.lookMount = 0
+ self:setOutfit(outfit)
+
+ local mount = Game.getMountByLookType(lookMount)
+ if mount ~= nil then
+ self:changeSpeed(-mount.speed)
+ end
+end
+
+local function getRandomMount(player)
+ local mounts = Game.getMounts()
+
+ local availableMounts = {}
+ for _, mount in ipairs(mounts) do
+ if player:hasMount(mount.lookType) then
+ table.insert(availableMounts, mount.lookType)
+ end
+ end
+
+ if #availableMounts == 0 then
+ return nil
+ end
+
+ local idx = math.random(1, #availableMounts)
+ return availableMounts[idx]
+end
+
+-- player:toggleMount(mounted)
+-- Behavior:
+-- - When mounted is true: mounts using the selected mount (or a random owned mount if randomize is enabled).
+-- - When mounted is false: dismounts.
+-- Enforces cooldown when mounting, protection-zone restriction, premium/ownership rules, and CONDITION_OUTFIT.
+function Player.toggleMount(self, mounted)
+ if mounted then
+ if not self:getGroup():getAccess() and self:getWasMounted() then
+ local lastMountToggle = self:getLastMountToggle()
+ if os.mtime() - lastMountToggle < Outfits.ToggleMountCooldown then
+ return false
+ end
+ end
+
+ if self:isMounted() then
+ return false
+ end
+
+ local tile = self:getTile()
+ if not self:getGroup():getAccess() and tile:hasFlag(TILESTATE_PROTECTIONZONE) then
+ self:sendCancelMessage(RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE)
+ return false
+ end
+
+ local lookMount = self:getCurrentMount()
+ if not lookMount then
+ self:sendOutfitWindow()
+ return false
+ end
+
+ if self:getRandomizeMount() then
+ lookMount = getRandomMount(self)
+ if not lookMount then
+ self:sendOutfitWindow()
+ return false
+ end
+ end
+
+ local currentMount = Game.getMountByLookType(lookMount)
+ if not currentMount then
+ return false
+ end
+
+ if not self:getGroup():getAccess() and currentMount.premium and not self:isPremium() then
+ self:sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT)
+ return false
+ end
+
+ if self:hasCondition(CONDITION_OUTFIT) then
+ self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE)
+ return false
+ end
+
+ self:mount(currentMount)
+ else
+ if not self:isMounted() then
+ return false
+ end
+
+ self:dismount()
+ end
+
+ self:setOutfit(self:getDefaultOutfit())
+ self:setLastMountToggle(os.mtime())
+ return true
+end
+
+-- Deprecated helper; mount lookType is already the identifier.
+function Game.getMountIdByLookType(lookType)
+ print("Warning: Game.getMountIdByLookType is deprecated. Mounts are now identified by client ID.")
+ return lookType
+end
diff --git a/data/scripts/systems/outfits/core/outfits.lua b/data/scripts/systems/outfits/core/outfits.lua
new file mode 100644
index 0000000000..e1a9472a18
--- /dev/null
+++ b/data/scripts/systems/outfits/core/outfits.lua
@@ -0,0 +1,145 @@
+-- Outfit ownership and addon helpers.
+--
+-- This file manages outfit storage state using PlayerStorageKeys.outfitsBase.
+-- Wearability checks (premium/unlocked/addons) are implemented in Player.canWearOutfit.
+--
+-- Addon representation:
+-- - Storage value is a bitmask of unlocked addons for a given lookType.
+-- - bit 0 => addon 1
+-- - bit 1 => addon 2
+-- - Functions that take `addon` expect an addon index (1 or 2), not a bitmask.
+-- - Functions that take `addons` expect a bitmask (e.g. 1, 2, or 3).
+-- player:addOutfit(lookType)
+-- Grants outfit ownership for the given lookType.
+function Player.addOutfit(self, lookType)
+ local addons = self:getStorageValue(PlayerStorageKeys.outfitsBase + lookType)
+ if addons ~= nil and addons ~= -1 then
+ return true
+ end
+ return self:setStorageValue(PlayerStorageKeys.outfitsBase + lookType, 0)
+end
+
+-- player:addAllOutfits()
+-- Grants all outfits available for the player's sex.
+-- This grants ownership only (addon mask = 0); it does not grant addons.
+function Player.addAllOutfits(self)
+ local outfits = Game.getOutfits(self:getSex())
+ for _, outfit in ipairs(outfits) do
+ self:addOutfit(outfit.lookType)
+ end
+end
+
+-- player:addOutfitAddon(lookType, addon)
+-- Adds addon 1 or 2 to an already-owned outfit.
+--
+-- Note: `addon` is an index (1 or 2). Internally it sets the corresponding bit in storage.
+function Player.addOutfitAddon(self, lookType, addon)
+ local addons = self:getStorageValue(PlayerStorageKeys.outfitsBase + lookType)
+ if addons == nil or addons == -1 then
+ return false
+ end
+
+ addons = addons | (1 << (addon - 1))
+ return self:setStorageValue(PlayerStorageKeys.outfitsBase + lookType, addons)
+end
+
+-- player:addAddonToAllOutfits(addon)
+-- Adds addon 1 or 2 to all outfits for both sexes.
+-- This function also grants missing outfits before setting the addon bit.
+function Player.addAddonToAllOutfits(self, addon)
+ for sex = 0, 1 do
+ local outfits = Game.getOutfits(sex)
+ for _, outfit in ipairs(outfits) do
+ self:addOutfit(outfit.lookType)
+ self:addOutfitAddon(outfit.lookType, addon)
+ end
+ end
+end
+
+-- player:getOutfitAddons(lookType) -> number
+-- Returns the stored addon bitmask for an owned outfit.
+-- Returns 0 when the player does not own the outfit or has no addons.
+function Player.getOutfitAddons(self, lookType)
+ local outfitAddons = self:getStorageValue(PlayerStorageKeys.outfitsBase + lookType)
+ if outfitAddons == nil or outfitAddons == -1 then
+ return 0
+ end
+ return outfitAddons
+end
+
+-- player:hasOutfit(lookType) -> boolean
+-- Returns true if the player owns the outfit (regardless of addons).
+function Player.hasOutfit(self, lookType)
+ local addons = self:getStorageValue(PlayerStorageKeys.outfitsBase + lookType)
+ return addons ~= nil and addons ~= -1
+end
+
+-- player:hasOutfitAddons(lookType, addons) -> boolean
+-- Returns true if the player owns the outfit and has all addons in the given bitmask.
+-- Example: addons == 3 means addon 1 and addon 2.
+function Player.hasOutfitAddons(self, lookType, addons)
+ local currentAddons = self:getStorageValue(PlayerStorageKeys.outfitsBase + lookType)
+ if currentAddons == nil or currentAddons == -1 then
+ return false
+ end
+ return (currentAddons & addons) == addons
+end
+
+-- player:hasOutfitAddon(lookType, addon) -> boolean
+-- Returns true if the player has a specific addon (1 or 2) for the outfit.
+function Player.hasOutfitAddon(self, lookType, addon)
+ local addons = self:getOutfitAddons(lookType)
+ return addons & (1 << (addon - 1)) ~= 0
+end
+
+-- player:removeOutfit(lookType)
+-- Removes outfit ownership (and all addon bits) for the given lookType.
+function Player.removeOutfit(self, lookType)
+ return self:removeStorageValue(PlayerStorageKeys.outfitsBase + lookType)
+end
+
+-- player:removeOutfitAddon(lookType, addon)
+-- Removes addon 1 or 2 from an owned outfit by clearing the corresponding bit.
+function Player.removeOutfitAddon(self, lookType, addon)
+ local addons = self:getOutfitAddons(lookType)
+ addons = addons & ~(1 << (addon - 1))
+ return self:setStorageValue(PlayerStorageKeys.outfitsBase + lookType, addons)
+end
+
+-- player:canWearOutfit(lookType[, addons]) -> boolean
+-- Returns whether the player is allowed to wear an outfit with the requested addons.
+--
+-- Parameters:
+-- - lookType: outfit lookType (client id)
+-- - addons: addon bitmask (0, 1, 2, or 3). This is not an addon index.
+--
+-- Checks:
+-- - staff bypass
+-- - valid outfit
+-- - premium requirement
+-- - ownership requirement for locked outfits
+-- - addon bitmask subset requirement
+function Player.canWearOutfit(self, lookType, addons)
+ if self:getGroup():getAccess() then
+ return true
+ end
+
+ local outfit = Game.getOutfitByLookType(lookType)
+ if not outfit then
+ return false
+ end
+
+ if outfit.premium and not self:isPremium() then
+ return false
+ end
+
+ if not outfit.unlocked and not self:hasOutfit(lookType) then
+ return false
+ end
+
+ if addons == nil or addons == 0 then
+ return true
+ end
+
+ return self:getOutfitAddons(lookType) & addons == addons
+end
diff --git a/data/scripts/systems/outfits/core/windows.lua b/data/scripts/systems/outfits/core/windows.lua
new file mode 100644
index 0000000000..da37cccd80
--- /dev/null
+++ b/data/scripts/systems/outfits/core/windows.lua
@@ -0,0 +1,260 @@
+-- Outfit and podium window builders.
+--
+-- This file builds outgoing packets:
+-- - 0xC8: Outfit Window (Player.sendOutfitWindow)
+-- - 0xD8: Podium Window (Player.sendPodiumWindow)
+local function canWearOutfit(player, outfit)
+ if not outfit.unlocked then
+ return player:hasOutfit(outfit.lookType)
+ end
+
+ return not outfit.premium or player:isPremium()
+end
+
+local function getAvailableOutfits(player)
+ local availableOutfits = {}
+
+ local isAccessPlayer = player:getGroup():getAccess()
+ if isAccessPlayer then
+ -- Add GM outfit for staff members.
+ local gamemaster = {
+ name = "Gamemaster",
+ lookType = 75,
+ addons = 0
+ }
+ table.insert(availableOutfits, gamemaster)
+ end
+
+ local outfits = Game.getOutfits(player:getSex())
+ for _, outfit in ipairs(outfits) do
+ if isAccessPlayer then
+ table.insert(availableOutfits, {
+ name = outfit.name,
+ lookType = outfit.lookType,
+ addons = 3
+ })
+ elseif canWearOutfit(player, outfit) then
+ local addons = player:getOutfitAddons(outfit.lookType)
+ table.insert(availableOutfits, {
+ name = outfit.name,
+ lookType = outfit.lookType,
+ addons = addons
+ })
+ end
+ end
+
+ return availableOutfits
+end
+
+local function getAvailableMounts(player)
+ local mounts = Game.getMounts()
+
+ local isAccessPlayer = player:getGroup():getAccess()
+
+ local availableMounts = {}
+ for _, mount in ipairs(mounts) do
+ if isAccessPlayer or player:hasMount(mount.lookType) then
+ table.insert(availableMounts, {
+ lookType = mount.lookType,
+ name = mount.name
+ })
+ end
+ end
+ return availableMounts
+end
+
+local function getAvailableFamiliars(player)
+ return {}
+end
+
+-- player:sendOutfitWindow()
+-- Builds and sends packet 0xC8 (Outfit Window).
+-- Structure:
+-- - 0xC8
+-- - currentOutfit via msg:addOutfit
+-- - if lookMount == 0: four mount color bytes (head/body/legs/feet)
+-- - currentFamiliarLookType:u16 (sent as 0)
+-- - outfits: count:u16 then {lookType:u16, name:string, addons:byte, mode:byte}
+-- - mounts: count:u16 then {lookType:u16, name:string, mode:byte}
+-- - familiars: count:u16 (currently 0)
+-- - tryOutfitMode:byte (0)
+-- - mounted:bool (uses getWasMounted to preserve intent while forcibly dismounted in PZ)
+-- - randomizeMount:bool
+function Player.sendOutfitWindow(self)
+ local availableOutfits = getAvailableOutfits(self)
+ if #availableOutfits == 0 then
+ self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE)
+ return
+ end
+
+ local currentOutfit = self:getDefaultOutfit()
+ if currentOutfit.lookType == 0 then
+ currentOutfit.lookType = availableOutfits[1].lookType
+ end
+
+ -- If the player was forcibly dismounted (e.g. PZ), keep the checkbox state coherent.
+ local mounted = self:isMounted() or self:getWasMounted()
+
+ local availableMounts = getAvailableMounts(self)
+ local availableFamiliars = getAvailableFamiliars(self)
+
+ local msg = NetworkMessage()
+ msg:addByte(0xC8)
+
+ msg:addOutfit(currentOutfit)
+
+ msg:addU16(currentOutfit.lookMount)
+ msg:addByte(currentOutfit.lookMountHead)
+ msg:addByte(currentOutfit.lookMountBody)
+ msg:addByte(currentOutfit.lookMountLegs)
+ msg:addByte(currentOutfit.lookMountFeet)
+
+ msg:addU16(0) -- current familiar looktype
+
+ msg:addU16(#availableOutfits)
+ for _, outfit in ipairs(availableOutfits) do
+ msg:addU16(outfit.lookType)
+ msg:addString(outfit.name)
+ msg:addByte(outfit.addons)
+ msg:addByte(0) -- mode: 0x00 - available, 0x01 store (requires U32 store offerId), 0x02 golden outfit tooltip (hardcoded)
+ end
+
+ msg:addU16(#availableMounts)
+ for _, mount in ipairs(availableMounts) do
+ msg:addU16(mount.lookType)
+ msg:addString(mount.name)
+ msg:addByte(0) -- mode: 0x00 - available, 0x01 store (requires U32 store offerlookType)
+ end
+
+ msg:addU16(#availableFamiliars)
+ for _, familiar in ipairs(availableFamiliars) do
+ msg:addU16(familiar.lookType)
+ msg:addString(familiar.name)
+ msg:addByte(0) -- mode: 0x00 - available, 0x01 store (requires U32 store offerId)
+ end
+
+ msg:addByte(0x00) -- try outfit mode (?)
+ msg:addBool(mounted)
+ msg:addBool(self:getRandomizeMount())
+ msg:sendToPlayer(self)
+ msg:delete()
+end
+
+-- player:sendPodiumWindow(item)
+-- Builds and sends packet 0xD8 (Podium Window).
+-- Structure:
+-- - 0xD8
+-- - currentPodiumOutfit via msg:addOutfit
+-- - currentMount block: lookMount:u16 + four mount color bytes
+-- - currentFamiliarLookType:u16 (0)
+-- - outfits list (same layout as 0xC8)
+-- - mounts list (same layout as 0xC8)
+-- - familiar count:u16 (0)
+-- - windowMode:byte (5)
+-- - showMountCheckbox:bool
+-- - unknown:u16 (0)
+-- - position:Position + itemClientId:u16 + stackpos:byte
+-- - showPlatform:bool + outfitCheckbox:bool (ignored by client) + direction:byte
+function Player.sendPodiumWindow(self, item)
+ local podium = item:getPodium()
+ if not podium then
+ return
+ end
+
+ local tile = item:getTile()
+ if not tile then
+ return
+ end
+
+ local it = ItemType(item:getId())
+ if not it then
+ return
+ end
+
+ local availableOutfits = getAvailableOutfits(self)
+ if #availableOutfits == 0 then
+ self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE)
+ return
+ end
+
+ local stackpos = tile:getThingIndex(item)
+
+ local podiumOutfit = podium:getOutfit()
+ local playerOutfit = self:getDefaultOutfit()
+ local isEmpty = podiumOutfit.lookType == 0 and podiumOutfit.lookMount == 0
+
+ if podiumOutfit.lookType == 0 then
+ -- Copy player outfit.
+ podiumOutfit.lookType = playerOutfit.lookType
+ podiumOutfit.lookHead = playerOutfit.lookHead
+ podiumOutfit.lookBody = playerOutfit.lookBody
+ podiumOutfit.lookLegs = playerOutfit.lookLegs
+ podiumOutfit.lookFeet = playerOutfit.lookFeet
+ podiumOutfit.lookAddons = playerOutfit.lookAddons
+ end
+
+ if podiumOutfit.lookMount == 0 then
+ -- Copy player mount.
+ podiumOutfit.lookMount = playerOutfit.lookMount
+ podiumOutfit.lookMountHead = playerOutfit.lookMountHead
+ podiumOutfit.lookMountBody = playerOutfit.lookMountBody
+ podiumOutfit.lookMountLegs = playerOutfit.lookMountLegs
+ podiumOutfit.lookMountFeet = playerOutfit.lookMountFeet
+ end
+
+ if not self:canWearOutfit(podiumOutfit.lookType) then
+ -- select first outfit available when the one from podium is not unlocked
+ podiumOutfit.lookType = availableOutfits[1].lookType
+ end
+
+ local availableMounts = getAvailableMounts(self)
+
+ local msg = NetworkMessage()
+ msg:addByte(0xD8)
+
+ -- current outfit
+ msg:addOutfit(podiumOutfit)
+
+ -- current mount
+ msg:addU16(podiumOutfit.lookMount)
+ msg:addByte(podiumOutfit.lookMountHead)
+ msg:addByte(podiumOutfit.lookMountBody)
+ msg:addByte(podiumOutfit.lookMountLegs)
+ msg:addByte(podiumOutfit.lookMountFeet)
+
+ -- current familiar (not used in podium)
+ msg:addU16(0)
+
+ -- available outfits
+ msg:addU16(#availableOutfits)
+ for _, outfit in ipairs(availableOutfits) do
+ msg:addU16(outfit.lookType)
+ msg:addString(outfit.name)
+ msg:addByte(outfit.addons)
+ msg:addByte(0) -- mode: 0x00 - available, 0x01 store (requires U32 store offerId), 0x02 golden outfit tooltip (hardcoded)
+ end
+
+ -- available mounts
+ msg:addU16(#availableMounts)
+ for _, mount in ipairs(availableMounts) do
+ msg:addU16(mount.lookType)
+ msg:addString(mount.name)
+ msg:addByte(0) -- mode: 0x00 - available, 0x01 store (requires U32 store offerId)
+ end
+
+ -- available familiars (not used in podium)
+ msg:addU16(0)
+
+ msg:addByte(0x05) -- "set outfit" window mode (5 = podium)
+ msg:addBool((isEmpty and playerOutfit.lookMount ~= 0) or podium:hasFlag(PODIUM_SHOW_MOUNT)) -- "mount" checkbox
+ msg:addU16(0) -- unknown
+ msg:addPosition(item:getPosition())
+ msg:addU16(it:getClientId())
+ msg:addByte(stackpos)
+
+ msg:addBool(podium:hasFlag(PODIUM_SHOW_PLATFORM)) -- is platform visible
+ msg:addBool(true) -- "outfit" checkbox, ignored by the client
+ msg:addByte(podium:getDirection())
+ msg:sendToPlayer(self)
+ msg:delete()
+end
diff --git a/data/scripts/systems/outfits/data/mounts.lua b/data/scripts/systems/outfits/data/mounts.lua
new file mode 100644
index 0000000000..9c8c83154a
--- /dev/null
+++ b/data/scripts/systems/outfits/data/mounts.lua
@@ -0,0 +1,1112 @@
+-- Mount data source for the Outfits & Mounts module.
+--
+-- This file overrides the Game mount lookup helpers used throughout the module:
+-- - Game.getMounts()
+-- - Game.getMountByLookType(lookType)
+-- - Game.getMountByName(name)
+-- - Game.getMount(param)
+--
+-- Each entry is keyed by lookType and includes:
+-- - name (string)
+-- - speed (number)
+-- - premium (bool)
+local mounts = {
+ [368] = {
+ name = "Widow Queen",
+ speed = 20,
+ premium = true
+ },
+ [369] = {
+ name = "Racing Bird",
+ speed = 20,
+ premium = true
+ },
+ [370] = {
+ name = "War Bear",
+ speed = 20,
+ premium = true
+ },
+ [371] = {
+ name = "Black Sheep",
+ speed = 20,
+ premium = true
+ },
+ [372] = {
+ name = "Midnight Panther",
+ speed = 20,
+ premium = true
+ },
+ [373] = {
+ name = "Draptor",
+ speed = 20,
+ premium = true
+ },
+ [374] = {
+ name = "Titanica",
+ speed = 20,
+ premium = true
+ },
+ [375] = {
+ name = "Tin Lizzard",
+ speed = 20,
+ premium = true
+ },
+ [376] = {
+ name = "Blazebringer",
+ speed = 20,
+ premium = true
+ },
+ [377] = {
+ name = "Rapid Boar",
+ speed = 20,
+ premium = true
+ },
+ [378] = {
+ name = "Stampor",
+ speed = 20,
+ premium = true
+ },
+ [379] = {
+ name = "Undead Cavebear",
+ speed = 20,
+ premium = true
+ },
+ [387] = {
+ name = "Donkey",
+ speed = 20,
+ premium = true
+ },
+ [388] = {
+ name = "Tiger Slug",
+ speed = 20,
+ premium = true
+ },
+ [389] = {
+ name = "Uniwheel",
+ speed = 20,
+ premium = true
+ },
+ [390] = {
+ name = "Crystal Wolf",
+ speed = 20,
+ premium = true
+ },
+ [392] = {
+ name = "War Horse",
+ speed = 20,
+ premium = true
+ },
+ [401] = {
+ name = "Kingly Deer",
+ speed = 20,
+ premium = true
+ },
+ [402] = {
+ name = "Tamed Panda",
+ speed = 20,
+ premium = true
+ },
+ [405] = {
+ name = "Dromedary",
+ speed = 20,
+ premium = true
+ },
+ [406] = {
+ name = "Scorpion King",
+ speed = 20,
+ premium = true
+ },
+ [421] = {
+ name = "Rented Horse",
+ speed = 20,
+ premium = false
+ },
+ [426] = {
+ name = "Armoured War Horse",
+ speed = 20,
+ premium = false
+ },
+ [427] = {
+ name = "Shadow Draptor",
+ speed = 20,
+ premium = false
+ },
+ [437] = {
+ name = "Rented Horse",
+ speed = 20,
+ premium = false
+ },
+ [438] = {
+ name = "Rented Horse",
+ speed = 20,
+ premium = false
+ },
+ [447] = {
+ name = "Lady Bug",
+ speed = 20,
+ premium = true
+ },
+ [450] = {
+ name = "Manta Ray",
+ speed = 20,
+ premium = true
+ },
+ [502] = {
+ name = "Ironblight",
+ speed = 20,
+ premium = true
+ },
+ [503] = {
+ name = "Magma Crawler",
+ speed = 20,
+ premium = true
+ },
+ [506] = {
+ name = "Dragonling",
+ speed = 20,
+ premium = true
+ },
+ [515] = {
+ name = "Gnarlhound",
+ speed = 20,
+ premium = true
+ },
+ [521] = {
+ name = "Crimson Ray",
+ speed = 20,
+ premium = false
+ },
+ [522] = {
+ name = "Steelbeak",
+ speed = 20,
+ premium = false
+ },
+ [526] = {
+ name = "Water Buffalo",
+ speed = 20,
+ premium = true
+ },
+ [546] = {
+ name = "Tombstinger",
+ speed = 20,
+ premium = false
+ },
+ [547] = {
+ name = "Platesaurian",
+ speed = 20,
+ premium = false
+ },
+ [548] = {
+ name = "Ursagrodon",
+ speed = 20,
+ premium = true
+ },
+ [559] = {
+ name = "The Hellgrip",
+ speed = 20,
+ premium = true
+ },
+ [571] = {
+ name = "Noble Lion",
+ speed = 20,
+ premium = true
+ },
+ [572] = {
+ name = "Desert King",
+ speed = 20,
+ premium = false
+ },
+ [580] = {
+ name = "Shock Head",
+ speed = 20,
+ premium = true
+ },
+ [606] = {
+ name = "Walker",
+ speed = 20,
+ premium = true
+ },
+ [621] = {
+ name = "Azudocus",
+ speed = 20,
+ premium = false
+ },
+ [622] = {
+ name = "Carpacosaurus",
+ speed = 20,
+ premium = false
+ },
+ [624] = {
+ name = "Death Crawler",
+ speed = 20,
+ premium = false
+ },
+ [626] = {
+ name = "Flamesteed",
+ speed = 20,
+ premium = false
+ },
+ [627] = {
+ name = "Jade Lion",
+ speed = 20,
+ premium = false
+ },
+ [628] = {
+ name = "Jade Pincer",
+ speed = 20,
+ premium = false
+ },
+ [629] = {
+ name = "Nethersteed",
+ speed = 20,
+ premium = false
+ },
+ [630] = {
+ name = "Tempest",
+ speed = 20,
+ premium = false
+ },
+ [631] = {
+ name = "Winter King",
+ speed = 20,
+ premium = false
+ },
+ [644] = {
+ name = "Doombringer",
+ speed = 20,
+ premium = false
+ },
+ [647] = {
+ name = "Woodland Prince",
+ speed = 20,
+ premium = false
+ },
+ [648] = {
+ name = "Hailstorm Fury",
+ speed = 20,
+ premium = false
+ },
+ [649] = {
+ name = "Siegebreaker",
+ speed = 20,
+ premium = false
+ },
+ [650] = {
+ name = "Poisonbane",
+ speed = 20,
+ premium = false
+ },
+ [651] = {
+ name = "Blackpelt",
+ speed = 20,
+ premium = false
+ },
+ [669] = {
+ name = "Golden Dragonfly",
+ speed = 20,
+ premium = false
+ },
+ [670] = {
+ name = "Steel Bee",
+ speed = 20,
+ premium = false
+ },
+ [671] = {
+ name = "Copper Fly",
+ speed = 20,
+ premium = false
+ },
+ [672] = {
+ name = "Tundra Rambler",
+ speed = 20,
+ premium = false
+ },
+ [673] = {
+ name = "Highland Yak",
+ speed = 20,
+ premium = false
+ },
+ [674] = {
+ name = "Glacier Vagabond",
+ speed = 20,
+ premium = false
+ },
+ [688] = {
+ name = "Flying Divan",
+ speed = 20,
+ premium = false
+ },
+ [689] = {
+ name = "Magic Carpet",
+ speed = 20,
+ premium = false
+ },
+ [690] = {
+ name = "Floating Kashmir",
+ speed = 20,
+ premium = false
+ },
+ [691] = {
+ name = "Ringtail Waccoon",
+ speed = 20,
+ premium = false
+ },
+ [692] = {
+ name = "Night Waccoon",
+ speed = 20,
+ premium = false
+ },
+ [693] = {
+ name = "Emerald Waccoon",
+ speed = 20,
+ premium = false
+ },
+ [682] = {
+ name = "Glooth Glider",
+ speed = 20,
+ premium = true
+ },
+ [685] = {
+ name = "Shadow Hart",
+ speed = 20,
+ premium = false
+ },
+ [686] = {
+ name = "Black Stag",
+ speed = 20,
+ premium = false
+ },
+ [687] = {
+ name = "Emperor Deer",
+ speed = 20,
+ premium = false
+ },
+ [726] = {
+ name = "Flitterkatzen",
+ speed = 20,
+ premium = false
+ },
+ [727] = {
+ name = "Venompaw",
+ speed = 20,
+ premium = false
+ },
+ [728] = {
+ name = "Batcat",
+ speed = 20,
+ premium = false
+ },
+ [734] = {
+ name = "Sea Devil",
+ speed = 20,
+ premium = false
+ },
+ [735] = {
+ name = "Coralripper",
+ speed = 20,
+ premium = false
+ },
+ [736] = {
+ name = "Plumfish",
+ speed = 20,
+ premium = false
+ },
+ [738] = {
+ name = "Gorongra",
+ speed = 20,
+ premium = false
+ },
+ [739] = {
+ name = "Noctungra",
+ speed = 20,
+ premium = false
+ },
+ [740] = {
+ name = "Silverneck",
+ speed = 20,
+ premium = false
+ },
+ [761] = {
+ name = "Slagsnare",
+ speed = 20,
+ premium = false
+ },
+ [762] = {
+ name = "Nightstinger",
+ speed = 20,
+ premium = false
+ },
+ [763] = {
+ name = "Razorcreep",
+ speed = 20,
+ premium = false
+ },
+ [848] = {
+ name = "Rift Runner",
+ speed = 20,
+ premium = true
+ },
+ [849] = {
+ name = "Nightdweller",
+ speed = 20,
+ premium = false
+ },
+ [850] = {
+ name = "Frostflare",
+ speed = 20,
+ premium = false
+ },
+ [851] = {
+ name = "Cinderhoof",
+ speed = 20,
+ premium = false
+ },
+ [868] = {
+ name = "Mouldpincer",
+ speed = 20,
+ premium = false
+ },
+ [869] = {
+ name = "Bloodcurl",
+ speed = 20,
+ premium = false
+ },
+ [870] = {
+ name = "Leafscuttler",
+ speed = 20,
+ premium = false
+ },
+ [883] = {
+ name = "Sparkion",
+ speed = 20,
+ premium = true
+ },
+ [886] = {
+ name = "Swamp Snapper",
+ speed = 20,
+ premium = false
+ },
+ [887] = {
+ name = "Mould Shell",
+ speed = 20,
+ premium = false
+ },
+ [888] = {
+ name = "Reed Lurker",
+ speed = 20,
+ premium = false
+ },
+ [889] = {
+ name = "Neon Sparkid",
+ speed = 20,
+ premium = true
+ },
+ [890] = {
+ name = "Vortexion",
+ speed = 20,
+ premium = true
+ },
+ [901] = {
+ name = "Ivory Fang",
+ speed = 20,
+ premium = false
+ },
+ [902] = {
+ name = "Shadow Claw",
+ speed = 20,
+ premium = false
+ },
+ [903] = {
+ name = "Snow Pelt",
+ speed = 20,
+ premium = false
+ },
+ [905] = {
+ name = "Jackalope",
+ speed = 20,
+ premium = false
+ },
+ [906] = {
+ name = "Dreadhare",
+ speed = 20,
+ premium = false
+ },
+ [907] = {
+ name = "Wolpertinger",
+ speed = 20,
+ premium = false
+ },
+ [937] = {
+ name = "Stone Rhino",
+ speed = 20,
+ premium = true
+ },
+ [950] = {
+ name = "Gold Sphinx",
+ speed = 20,
+ premium = false
+ },
+ [951] = {
+ name = "Emerald Sphinx",
+ speed = 20,
+ premium = false
+ },
+ [952] = {
+ name = "Shadow Sphinx",
+ speed = 20,
+ premium = false
+ },
+ [959] = {
+ name = "Jungle Saurian",
+ speed = 20,
+ premium = false
+ },
+ [960] = {
+ name = "Ember Saurian",
+ speed = 20,
+ premium = false
+ },
+ [961] = {
+ name = "Lagoon Saurian",
+ speed = 20,
+ premium = false
+ },
+ [1017] = {
+ name = "Blazing Unicorn",
+ speed = 20,
+ premium = false
+ },
+ [1018] = {
+ name = "Arctic Unicorn",
+ speed = 20,
+ premium = false
+ },
+ [1019] = {
+ name = "Prismatic unicorn",
+ speed = 20,
+ premium = false
+ },
+ [1025] = {
+ name = "Cranium Spider",
+ speed = 20,
+ premium = false
+ },
+ [1026] = {
+ name = "Cave Tarantula",
+ speed = 20,
+ premium = false
+ },
+ [1027] = {
+ name = "Gloom Widow",
+ speed = 20,
+ premium = false
+ },
+ [1049] = {
+ name = "Mole",
+ speed = 20,
+ premium = true
+ },
+ [1052] = {
+ name = "Marsh Toad",
+ speed = 20,
+ premium = false
+ },
+ [1053] = {
+ name = "Sanguine Frog",
+ speed = 20,
+ premium = false
+ },
+ [1054] = {
+ name = "Toxic Toad",
+ speed = 20,
+ premium = false
+ },
+ [1091] = {
+ name = "Ebony Tiger",
+ speed = 20,
+ premium = false
+ },
+ [1092] = {
+ name = "Feral Tiger",
+ speed = 20,
+ premium = false
+ },
+ [1093] = {
+ name = "Jungle Tiger",
+ speed = 20,
+ premium = false
+ },
+ [1101] = {
+ name = "Fleeting Knowledge",
+ speed = 20,
+ premium = true
+ },
+ [1104] = {
+ name = "Tawny Owl",
+ speed = 20,
+ premium = false
+ },
+ [1105] = {
+ name = "Snowy Owl",
+ speed = 20,
+ premium = false
+ },
+ [1106] = {
+ name = "Boreal Owl",
+ speed = 20,
+ premium = false
+ },
+ [1150] = {
+ name = "Lacewing Moth",
+ speed = 20,
+ premium = true
+ },
+ [1151] = {
+ name = "Hibernal Moth",
+ speed = 20,
+ premium = true
+ },
+ [1163] = {
+ name = "Cold Percht Sleigh",
+ speed = 20,
+ premium = true
+ },
+ [1164] = {
+ name = "Bright Percht Sleigh",
+ speed = 20,
+ premium = true
+ },
+ [1165] = {
+ name = "Dark Percht Sleigh",
+ speed = 20,
+ premium = true
+ },
+ [1167] = {
+ name = "Festive Snowman",
+ speed = 20,
+ premium = false
+ },
+ [1168] = {
+ name = "Muffled Snowman",
+ speed = 20,
+ premium = false
+ },
+ [1169] = {
+ name = "Caped Snowman",
+ speed = 20,
+ premium = false
+ },
+ [1179] = {
+ name = "Rabbit Rickshaw",
+ speed = 20,
+ premium = false
+ },
+ [1180] = {
+ name = "Bunny Dray",
+ speed = 20,
+ premium = false
+ },
+ [1181] = {
+ name = "Cony Cart",
+ speed = 20,
+ premium = false
+ },
+ [1183] = {
+ name = "River Crocovile",
+ speed = 20,
+ premium = false
+ },
+ [1184] = {
+ name = "Swamp Crocovile",
+ speed = 20,
+ premium = false
+ },
+ [1185] = {
+ name = "Nightmarish Crocovile",
+ speed = 20,
+ premium = false
+ },
+ [1191] = {
+ name = "Gryphon",
+ speed = 20,
+ premium = true
+ },
+ [1208] = {
+ name = "Jousting Eagle",
+ speed = 20,
+ premium = false
+ },
+ [1209] = {
+ name = "Cerberus Champion",
+ speed = 20,
+ premium = false
+ },
+ [1229] = {
+ name = "Cold Percht Sleigh Variant",
+ speed = 20,
+ premium = true
+ },
+ [1230] = {
+ name = "Bright Percht Sleigh Variant",
+ speed = 20,
+ premium = true
+ },
+ [1231] = {
+ name = "Dark Percht Sleigh Variant",
+ speed = 20,
+ premium = true
+ },
+ [1232] = {
+ name = "Cold Percht Sleigh Final",
+ speed = 20,
+ premium = true
+ },
+ [1233] = {
+ name = "Bright Percht Sleigh Final",
+ speed = 20,
+ premium = true
+ },
+ [1234] = {
+ name = "Dark Percht Sleigh Final",
+ speed = 20,
+ premium = true
+ },
+ [1247] = {
+ name = "Battle Badger",
+ speed = 20,
+ premium = false
+ },
+ [1248] = {
+ name = "Ether Badger",
+ speed = 20,
+ premium = false
+ },
+ [1249] = {
+ name = "Zaoan Badger",
+ speed = 20,
+ premium = false
+ },
+ [1257] = {
+ name = "Blue Rolling Barrel",
+ speed = 20,
+ premium = true
+ },
+ [1258] = {
+ name = "Red Rolling Barrel",
+ speed = 20,
+ premium = true
+ },
+ [1259] = {
+ name = "Green Rolling Barrel",
+ speed = 20,
+ premium = true
+ },
+ [1264] = {
+ name = "Floating Sage",
+ speed = 20,
+ premium = false
+ },
+ [1265] = {
+ name = "Floating Scholar",
+ speed = 20,
+ premium = false
+ },
+ [1266] = {
+ name = "Floating Augur",
+ speed = 20,
+ premium = false
+ },
+ [1269] = {
+ name = "Haze",
+ speed = 20,
+ premium = true
+ },
+ [1281] = {
+ name = "Antelope",
+ speed = 20,
+ premium = true
+ },
+ [1284] = {
+ name = "Snow Strider",
+ speed = 20,
+ premium = false
+ },
+ [1285] = {
+ name = "Dusk Pryer",
+ speed = 20,
+ premium = false
+ },
+ [1286] = {
+ name = "Dawn Strayer",
+ speed = 20,
+ premium = false
+ },
+ [1321] = {
+ name = "Phantasmal Jade",
+ speed = 20,
+ premium = true
+ },
+ [1324] = {
+ name = "Savanna Ostrich",
+ speed = 20,
+ premium = true
+ },
+ [1325] = {
+ name = "Coral Rhea",
+ speed = 20,
+ premium = true
+ },
+ [1326] = {
+ name = "Eventide Nandu",
+ speed = 20,
+ premium = true
+ },
+ [1333] = {
+ name = "Voracious Hyaena",
+ speed = 20,
+ premium = false
+ },
+ [1334] = {
+ name = "Cunning Hyaena",
+ speed = 20,
+ premium = false
+ },
+ [1335] = {
+ name = "Scruffy Hyaena",
+ speed = 20,
+ premium = false
+ },
+ [1336] = {
+ name = "White Lion",
+ speed = 20,
+ premium = true
+ },
+ [1363] = {
+ name = "Krakoloss",
+ speed = 20,
+ premium = true
+ },
+ [1379] = {
+ name = "Merry Mammoth",
+ speed = 20,
+ premium = false
+ },
+ [1380] = {
+ name = "Holiday Mammoth",
+ speed = 20,
+ premium = false
+ },
+ [1381] = {
+ name = "Festive Mammoth",
+ speed = 20,
+ premium = false
+ },
+ [1389] = {
+ name = "Void Watcher",
+ speed = 20,
+ premium = false
+ },
+ [1390] = {
+ name = "Rune Watcher",
+ speed = 20,
+ premium = false
+ },
+ [1391] = {
+ name = "Rift Watcher",
+ speed = 20,
+ premium = false
+ },
+ [1417] = {
+ name = "Phant",
+ speed = 20,
+ premium = true
+ },
+ [1430] = {
+ name = "Shellodon",
+ speed = 20,
+ premium = true
+ },
+ [1431] = {
+ name = "Singeing Steed",
+ speed = 20,
+ premium = true
+ },
+ [1439] = {
+ name = "Hyacinth",
+ speed = 20,
+ premium = false
+ },
+ [1440] = {
+ name = "Peony",
+ speed = 20,
+ premium = false
+ },
+ [1441] = {
+ name = "Dandelion",
+ speed = 20,
+ premium = false
+ },
+ [1446] = {
+ name = "Rustwurm",
+ speed = 20,
+ premium = false
+ },
+ [1447] = {
+ name = "Bogwurm",
+ speed = 20,
+ premium = false
+ },
+ [1448] = {
+ name = "Gloomwurm",
+ speed = 20,
+ premium = false
+ },
+ [1453] = {
+ name = "Emerald Raven",
+ speed = 20,
+ premium = false
+ },
+ [1454] = {
+ name = "Mystic Raven",
+ speed = 20,
+ premium = false
+ },
+ [1455] = {
+ name = "Radiant Raven",
+ speed = 20,
+ premium = false
+ },
+ [1459] = {
+ name = "Gloothomotive",
+ speed = 20,
+ premium = true
+ },
+ [1491] = {
+ name = "Topaz Shrine",
+ speed = 20,
+ premium = false
+ },
+ [1492] = {
+ name = "Jade Shrine",
+ speed = 20,
+ premium = false
+ },
+ [1493] = {
+ name = "Obsidian Shrine",
+ speed = 20,
+ premium = false
+ },
+ [1526] = {
+ name = "Poppy Ibex",
+ speed = 20,
+ premium = false
+ },
+ [1527] = {
+ name = "Mint Ibex",
+ speed = 20,
+ premium = false
+ },
+ [1528] = {
+ name = "Cinnamon Ibex",
+ speed = 20,
+ premium = false
+ },
+ [1536] = {
+ name = "Giant Beaver",
+ speed = 20,
+ premium = true
+ },
+ [1577] = {
+ name = "Ripptor",
+ speed = 20,
+ premium = true
+ },
+ [1578] = {
+ name = "Parade Horse",
+ speed = 20,
+ premium = false
+ },
+ [1579] = {
+ name = "Jousting Horse",
+ speed = 20,
+ premium = false
+ },
+ [1580] = {
+ name = "Tourney Horse",
+ speed = 20,
+ premium = false
+ },
+ [1599] = {
+ name = "Mutated Abomination",
+ speed = 20,
+ premium = true
+ },
+ [1608] = {
+ name = "Tangerine Flecked Koi",
+ speed = 20,
+ premium = false
+ },
+ [1609] = {
+ name = "Brass Speckled Koi",
+ speed = 20,
+ premium = false
+ },
+ [1610] = {
+ name = "Ink Spotted Koi",
+ speed = 20,
+ premium = false
+ }
+}
+
+function Game.getMountByLookType(lookType)
+ local mount = mounts[lookType]
+ if not mount then
+ return nil
+ end
+
+ return {
+ lookType = lookType,
+ name = mount.name,
+ speed = mount.speed,
+ premium = mount.premium
+ }
+end
+
+function Game.getMountByName(name)
+ for lookType, mount in pairs(mounts) do
+ if mount.name:lower() == name:lower() then
+ return {
+ lookType = lookType,
+ name = mount.name,
+ speed = mount.speed,
+ premium = mount.premium
+ }
+ end
+ end
+ return nil
+end
+
+function Game.getMount(param)
+ local lookType = tonumber(param)
+ if lookType then
+ return Game.getMountByLookType(lookType)
+ end
+ return Game.getMountByName(param)
+end
+
+do
+ local cachedMounts = {}
+
+ for lookType, mount in pairs(mounts) do
+ table.insert(cachedMounts, {
+ lookType = lookType,
+ name = mount.name,
+ speed = mount.speed,
+ premium = mount.premium
+ })
+ end
+
+ function Game.getMounts()
+ return cachedMounts
+ end
+end
diff --git a/data/scripts/systems/outfits/data/outfits.lua b/data/scripts/systems/outfits/data/outfits.lua
new file mode 100644
index 0000000000..a358a558ee
--- /dev/null
+++ b/data/scripts/systems/outfits/data/outfits.lua
@@ -0,0 +1,1624 @@
+-- Outfit data source for the Outfits & Mounts module.
+--
+-- This file overrides the Game outfit lookup helpers used throughout the module:
+-- - Game.getOutfits(sex)
+-- - Game.getOutfitByLookType(lookType)
+-- - Game.getOutfitByName(name, sex)
+-- - Game.getOutfit(param, sex)
+--
+-- Each entry is keyed by lookType and includes:
+-- - name (string)
+-- - sex (PLAYERSEX_FEMALE / PLAYERSEX_MALE)
+-- - premium (bool)
+-- - unlocked (bool): if false, player must own it to wear
+-- - enabled (bool): if false, excluded from lists
+local outfits = {
+ -- Female outfits
+ [136] = {
+ name = "Citizen",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = true,
+ enabled = true
+ },
+ [137] = {
+ name = "Hunter",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = true,
+ enabled = true
+ },
+ [138] = {
+ name = "Mage",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = true,
+ enabled = true
+ },
+ [139] = {
+ name = "Knight",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = true,
+ enabled = true
+ },
+ [140] = {
+ name = "Noblewoman",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [141] = {
+ name = "Summoner",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [142] = {
+ name = "Warrior",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [147] = {
+ name = "Barbarian",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [148] = {
+ name = "Druid",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [149] = {
+ name = "Wizard",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [150] = {
+ name = "Oriental",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [155] = {
+ name = "Pirate",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [156] = {
+ name = "Assassin",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [157] = {
+ name = "Beggar",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [158] = {
+ name = "Shaman",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [252] = {
+ name = "Norsewoman",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [269] = {
+ name = "Nightmare",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [270] = {
+ name = "Jester",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [279] = {
+ name = "Brotherhood",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [288] = {
+ name = "Demon Hunter",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [324] = {
+ name = "Yalaharian",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [329] = {
+ name = "Newly Wed",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [336] = {
+ name = "Warmaster",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [366] = {
+ name = "Wayfarer",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [431] = {
+ name = "Afflicted",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [433] = {
+ name = "Elementalist",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [464] = {
+ name = "Deepling",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [466] = {
+ name = "Insectoid",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [471] = {
+ name = "Entrepreneur",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [513] = {
+ name = "Crystal Warlord",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [514] = {
+ name = "Soil Guardian",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [542] = {
+ name = "Demon Outfit",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [575] = {
+ name = "Cave Explorer",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [578] = {
+ name = "Dream Warden",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [618] = {
+ name = "Glooth Engineer",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [620] = {
+ name = "Jersey",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [632] = {
+ name = "Champion",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [635] = {
+ name = "Conjurer",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [636] = {
+ name = "Beastmaster",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [664] = {
+ name = "Chaos Acolyte",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [666] = {
+ name = "Death Herald",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [683] = {
+ name = "Ranger",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [694] = {
+ name = "Ceremonial Garb",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [696] = {
+ name = "Puppeteer",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [698] = {
+ name = "Spirit Caller",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [724] = {
+ name = "Evoker",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [732] = {
+ name = "Seaweaver",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [745] = {
+ name = "Recruiter",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [749] = {
+ name = "Sea Dog",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [759] = {
+ name = "Royal Pumpkin",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [845] = {
+ name = "Rift Warrior",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [852] = {
+ name = "Winter Warden",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [874] = {
+ name = "Philosopher",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [885] = {
+ name = "Arena Champion",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [900] = {
+ name = "Lupine Warden",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [909] = {
+ name = "Grove Keeper",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [929] = {
+ name = "Festive Outfit",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [956] = {
+ name = "Pharaoh",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [958] = {
+ name = "Trophy Hunter",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [963] = {
+ name = "Retro Warrior",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [965] = {
+ name = "Retro Summoner",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [967] = {
+ name = "Retro Noblewoman",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [969] = {
+ name = "Retro Mage",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [971] = {
+ name = "Retro Knight",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [973] = {
+ name = "Retro Hunter",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [975] = {
+ name = "Retro Citizen",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1020] = {
+ name = "Herbalist",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1024] = {
+ name = "Sun Priest",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1043] = {
+ name = "Makeshift Warrior",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1050] = {
+ name = "Siege Master",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1057] = {
+ name = "Mercenary",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1070] = {
+ name = "Battle Mage",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1095] = {
+ name = "Discoverer",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1103] = {
+ name = "Sinister Archer",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1128] = {
+ name = "Pumpkin Mummy",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1147] = {
+ name = "Dream Warrior",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1162] = {
+ name = "Percht Raider",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1174] = {
+ name = "Owl Keeper",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1187] = {
+ name = "Guidon Bearer",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1203] = {
+ name = "Void Master",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1205] = {
+ name = "Veteran Paladin",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1207] = {
+ name = "Lion of War",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1211] = {
+ name = "Golden Outfit",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1244] = {
+ name = "Hand of the Inquisition",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1246] = {
+ name = "Breezy Garb",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1252] = {
+ name = "Orcsoberfest Garb",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1271] = {
+ name = "Poltergeist",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1280] = {
+ name = "Herder",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1283] = {
+ name = "Falconer",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1289] = {
+ name = "Dragon Slayer",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1293] = {
+ name = "Trailblazer",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1323] = {
+ name = "Revenant",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1332] = {
+ name = "Jouster",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1339] = {
+ name = "Moth Cape",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1372] = {
+ name = "Rascoohan",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1383] = {
+ name = "Merry Garb",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1385] = {
+ name = "Rune Master",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1387] = {
+ name = "Citizen of Issavi",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1416] = {
+ name = "Forest Warden",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1437] = {
+ name = "Royal Bounacean Advisor",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1445] = {
+ name = "Dragon Knight",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1450] = {
+ name = "Arbalester",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1456] = {
+ name = "Royal Costume",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1461] = {
+ name = "Formal Dress",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1490] = {
+ name = "Ghost Blade",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1501] = {
+ name = "Nordic Chieftain",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1569] = {
+ name = "Fire-Fighter",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1576] = {
+ name = "Fencer",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1582] = {
+ name = "Shadowlotus Disciple",
+ sex = PLAYERSEX_FEMALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1598] = {
+ name = "Ancient Aucar",
+ sex = PLAYERSEX_FEMALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+
+ -- Male outfits
+ [128] = {
+ name = "Citizen",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = true,
+ enabled = true
+ },
+ [129] = {
+ name = "Hunter",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = true,
+ enabled = true
+ },
+ [130] = {
+ name = "Mage",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = true,
+ enabled = true
+ },
+ [131] = {
+ name = "Knight",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = true,
+ enabled = true
+ },
+ [132] = {
+ name = "Nobleman",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [133] = {
+ name = "Summoner",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [134] = {
+ name = "Warrior",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [143] = {
+ name = "Barbarian",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [144] = {
+ name = "Druid",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [145] = {
+ name = "Wizard",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [146] = {
+ name = "Oriental",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = true,
+ enabled = true
+ },
+ [151] = {
+ name = "Pirate",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [152] = {
+ name = "Assassin",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [153] = {
+ name = "Beggar",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [154] = {
+ name = "Shaman",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [251] = {
+ name = "Norseman",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [268] = {
+ name = "Nightmare",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [273] = {
+ name = "Jester",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [278] = {
+ name = "Brotherhood",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [289] = {
+ name = "Demon Hunter",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [325] = {
+ name = "Yalaharian",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [328] = {
+ name = "Newly Wed",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [335] = {
+ name = "Warmaster",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [367] = {
+ name = "Wayfarer",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [430] = {
+ name = "Afflicted",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [432] = {
+ name = "Elementalist",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [463] = {
+ name = "Deepling",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [465] = {
+ name = "Insectoid",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [472] = {
+ name = "Entrepreneur",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [512] = {
+ name = "Crystal Warlord",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [516] = {
+ name = "Soil Guardian",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [541] = {
+ name = "Demon Outfit",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [574] = {
+ name = "Cave Explorer",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [577] = {
+ name = "Dream Warden",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [610] = {
+ name = "Glooth Engineer",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [619] = {
+ name = "Jersey",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [633] = {
+ name = "Champion",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [634] = {
+ name = "Conjurer",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [637] = {
+ name = "Beastmaster",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [665] = {
+ name = "Chaos Acolyte",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [667] = {
+ name = "Death Herald",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [684] = {
+ name = "Ranger",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [695] = {
+ name = "Ceremonial Garb",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [697] = {
+ name = "Puppeteer",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [699] = {
+ name = "Spirit Caller",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [725] = {
+ name = "Evoker",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [733] = {
+ name = "Seaweaver",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [746] = {
+ name = "Recruiter",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [750] = {
+ name = "Sea Dog",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [760] = {
+ name = "Royal Pumpkin",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [846] = {
+ name = "Rift Warrior",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [853] = {
+ name = "Winter Warden",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [873] = {
+ name = "Philosopher",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [884] = {
+ name = "Arena Champion",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [899] = {
+ name = "Lupine Warden",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [908] = {
+ name = "Grove Keeper",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [931] = {
+ name = "Festive Outfit",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [955] = {
+ name = "Pharaoh",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [957] = {
+ name = "Trophy Hunter",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [962] = {
+ name = "Retro Warrior",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [964] = {
+ name = "Retro Summoner",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [966] = {
+ name = "Retro Nobleman",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [968] = {
+ name = "Retro Mage",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [970] = {
+ name = "Retro Knight",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [972] = {
+ name = "Retro Hunter",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [974] = {
+ name = "Retro Citizen",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1021] = {
+ name = "Herbalist",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1023] = {
+ name = "Sun Priest",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1042] = {
+ name = "Makeshift Warrior",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1051] = {
+ name = "Siege Master",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1056] = {
+ name = "Mercenary",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1069] = {
+ name = "Battle Mage",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1094] = {
+ name = "Discoverer",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1102] = {
+ name = "Sinister Archer",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1127] = {
+ name = "Pumpkin Mummy",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1146] = {
+ name = "Dream Warrior",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1161] = {
+ name = "Percht Raider",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1173] = {
+ name = "Owl Keeper",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1186] = {
+ name = "Guidon Bearer",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1202] = {
+ name = "Void Master",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1204] = {
+ name = "Veteran Paladin",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1206] = {
+ name = "Lion of War",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1210] = {
+ name = "Golden Outfit",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1243] = {
+ name = "Hand of the Inquisition",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1245] = {
+ name = "Breezy Garb",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1251] = {
+ name = "Orcsoberfest Garb",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1270] = {
+ name = "Poltergeist",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1279] = {
+ name = "Herder",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1282] = {
+ name = "Falconer",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1288] = {
+ name = "Dragon Slayer",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1292] = {
+ name = "Trailblazer",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1322] = {
+ name = "Revenant",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1331] = {
+ name = "Jouster",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1338] = {
+ name = "Moth Cape",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1371] = {
+ name = "Rascoohan",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1382] = {
+ name = "Merry Garb",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1384] = {
+ name = "Rune Master",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1386] = {
+ name = "Citizen of Issavi",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1415] = {
+ name = "Forest Warden",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1436] = {
+ name = "Royal Bounacean Advisor",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1444] = {
+ name = "Dragon Knight",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1449] = {
+ name = "Arbalester",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1457] = {
+ name = "Royal Costume",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1460] = {
+ name = "Formal Dress",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1489] = {
+ name = "Ghost Blade",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1500] = {
+ name = "Nordic Chieftain",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1568] = {
+ name = "Fire-Fighter",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ },
+ [1575] = {
+ name = "Fencer",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1581] = {
+ name = "Shadowlotus Disciple",
+ sex = PLAYERSEX_MALE,
+ premium = false,
+ unlocked = false,
+ enabled = true
+ },
+ [1597] = {
+ name = "Ancient Aucar",
+ sex = PLAYERSEX_MALE,
+ premium = true,
+ unlocked = false,
+ enabled = true
+ }
+}
+
+function Game.getOutfitByLookType(lookType)
+ local outfit = outfits[lookType]
+ if not outfit then
+ return nil
+ end
+
+ return {
+ lookType = lookType,
+ name = outfit.name,
+ sex = outfit.sex,
+ premium = outfit.premium,
+ unlocked = outfit.unlocked
+ }
+end
+
+function Game.getOutfitByName(name, sex)
+ if type(name) ~= "string" then
+ return nil
+ end
+
+ for lookType, outfit in pairs(outfits) do
+ if outfit.name:lower() == name:lower() and outfit.sex == sex then
+ return {
+ lookType = lookType,
+ name = outfit.name,
+ sex = outfit.sex,
+ premium = outfit.premium,
+ unlocked = outfit.unlocked
+ }
+ end
+ end
+ return nil
+end
+
+function Game.getOutfit(param, sex)
+ local lookType = tonumber(param)
+ if lookType then
+ return Game.getOutfitByLookType(lookType)
+ end
+ return Game.getOutfitByName(param, sex)
+end
+
+do
+ local cachedOutfits = {
+ [PLAYERSEX_FEMALE] = {},
+ [PLAYERSEX_MALE] = {}
+ }
+
+ for lookType, outfit in pairs(outfits) do
+ if outfit.enabled then
+ table.insert(cachedOutfits[outfit.sex], {
+ lookType = lookType,
+ name = outfit.name,
+ sex = outfit.sex,
+ premium = outfit.premium,
+ unlocked = outfit.unlocked
+ })
+ end
+ end
+
+ function Game.getOutfits(sex)
+ return cachedOutfits[sex]
+ end
+end
diff --git a/data/scripts/systems/outfits/events/change_zone.lua b/data/scripts/systems/outfits/events/change_zone.lua
new file mode 100644
index 0000000000..39161f3af8
--- /dev/null
+++ b/data/scripts/systems/outfits/events/change_zone.lua
@@ -0,0 +1,23 @@
+-- Handles auto dismount/remount when crossing protection zones.
+--
+-- The flag wasMounted is used to remember the player's intent to stay mounted while
+-- forcibly dismounted in protection zones, and to bypass the normal toggle cooldown
+-- for these forced transitions.
+local event = Event()
+
+function event.onCreatureChangeZone(self, fromZone, toZone)
+ if not self:isPlayer() then
+ return
+ end
+
+ if toZone == ZONE_PROTECTION and not self:getGroup():getAccess() and self:isMounted() then
+ self:setWasMounted(true)
+ self:toggleMount(false)
+ elseif fromZone == ZONE_PROTECTION and self:getWasMounted() then
+ if self:toggleMount(true) then
+ self:setWasMounted(false)
+ end
+ end
+end
+
+event:register()
diff --git a/data/scripts/systems/outfits/events/cleanup.lua b/data/scripts/systems/outfits/events/cleanup.lua
new file mode 100644
index 0000000000..10e0d9a29c
--- /dev/null
+++ b/data/scripts/systems/outfits/events/cleanup.lua
@@ -0,0 +1,12 @@
+-- Clears per-session mount bookkeeping on logout.
+--
+-- These values are not persisted and should not be carried between sessions.
+local event = Event()
+
+function event.onPlayerLogout(self)
+ self:setLastMountToggle(nil)
+ self:setWasMounted(nil)
+ return true
+end
+
+event:register()
diff --git a/data/scripts/systems/outfits/network/request_outfit_window.lua b/data/scripts/systems/outfits/network/request_outfit_window.lua
new file mode 100644
index 0000000000..8f3b9f700c
--- /dev/null
+++ b/data/scripts/systems/outfits/network/request_outfit_window.lua
@@ -0,0 +1,14 @@
+local handler = PacketHandler(0xD2)
+
+-- 0xD2: Request Outfit Window (client -> server)
+-- Payload: (empty)
+-- Response: 0xC8 (Outfit Window) via Player.sendOutfitWindow in systems/outfits/core/windows.lua
+function handler.onReceive(player, msg)
+ if not Outfits.AllowChangeOutfit then
+ return
+ end
+
+ player:sendOutfitWindow()
+end
+
+handler:register()
diff --git a/data/scripts/systems/outfits/network/set_outfit.lua b/data/scripts/systems/outfits/network/set_outfit.lua
new file mode 100644
index 0000000000..8ed7ca02ff
--- /dev/null
+++ b/data/scripts/systems/outfits/network/set_outfit.lua
@@ -0,0 +1,132 @@
+local handler = PacketHandler(0xD3)
+
+-- 0xD3: Set Outfit (client -> server)
+-- Payload:
+-- - outfitType:byte
+-- - outfit (always):
+-- - lookType:u16
+-- - lookHead:byte, lookBody:byte, lookLegs:byte, lookFeet:byte
+-- - lookAddons:byte
+-- - branches:
+-- - outfitType == 0 (Customize/Set outfit):
+-- - lookMount:u16
+-- - lookMountHead:byte, lookMountBody:byte, lookMountLegs:byte, lookMountFeet:byte
+-- - familiarLookType:u16 (ignored)
+-- - randomizeMount:bool
+-- - outfitType == 1 (Store try-on):
+-- - clears lookMount and reads 4 mount color bytes (client preview)
+-- - outfitType == 2 (Podium edit):
+-- - position:Position
+-- - clientId:u16
+-- - stackpos:byte
+-- - lookMount:u16 + 4 mount color bytes
+-- - direction:byte
+-- - isVisible:bool
+function handler.onReceive(player, msg)
+ if not Outfits.AllowChangeOutfit then
+ return
+ end
+
+ local outfitType = msg:getByte()
+
+ local outfit = player:getCurrentOutfit()
+ outfit.lookType = msg:getU16()
+ outfit.lookHead = msg:getByte()
+ outfit.lookBody = msg:getByte()
+ outfit.lookLegs = msg:getByte()
+ outfit.lookFeet = msg:getByte()
+ outfit.lookAddons = msg:getByte()
+
+ if outfitType == 0 then -- set outfit from customize character window
+ local lookMount = msg:getU16()
+ local lookMountHead = msg:getByte()
+ local lookMountBody = msg:getByte()
+ local lookMountLegs = msg:getByte()
+ local lookMountFeet = msg:getByte()
+
+ outfit.lookMount = lookMount
+ if lookMount ~= 0 then -- only update colors if a mount with colors is selected
+ outfit.lookMountHead = lookMountHead
+ outfit.lookMountBody = lookMountBody
+ outfit.lookMountLegs = lookMountLegs
+ outfit.lookMountFeet = lookMountFeet
+ end
+
+ msg:getU16() -- familiar looktype
+ local randomizeMount = msg:getBool()
+
+ if not player:canWearOutfit(outfit.lookType, outfit.lookAddons) then
+ return
+ end
+
+ if outfit.lookMount ~= 0 then
+ if not player:canRideMount(outfit.lookMount) then
+ player:setCurrentMount(nil)
+ return
+ end
+
+ player:setCurrentMount(outfit.lookMount)
+ else
+ player:setCurrentMount(nil)
+ end
+
+ player:setOutfit(outfit)
+ player:setRandomizeMount(randomizeMount)
+ elseif outfitType == 1 then -- try outfit from store window
+ outfit.lookMount = 0
+ outfit.lookMountHead = msg:getByte()
+ outfit.lookMountBody = msg:getByte()
+ outfit.lookMountLegs = msg:getByte()
+ outfit.lookMountFeet = msg:getByte()
+
+ -- TODO: Implement store outfit preview window
+ elseif outfitType == 2 then -- set podium outfit
+ local position = msg:getPosition()
+ local clientId = msg:getU16()
+ local stackpos = msg:getByte()
+
+ outfit.lookMount = msg:getU16()
+ outfit.lookMountHead = msg:getByte()
+ outfit.lookMountBody = msg:getByte()
+ outfit.lookMountLegs = msg:getByte()
+ outfit.lookMountFeet = msg:getByte()
+
+ local direction = msg:getByte()
+ local isVisible = msg:getBool()
+
+ if not player:canWearOutfit(outfit.lookType, outfit.lookAddons) then
+ return
+ end
+
+ if outfit.lookMount ~= 0 and not player:canRideMount(outfit.lookMount) then
+ return
+ end
+
+ local tile = Tile(position)
+ if not tile then
+ return
+ end
+
+ local item = tile:getTopDownItem()
+ if not item then
+ return
+ end
+
+ -- Verify item is at expected stack position
+ local thingAtPos = tile:getThing(stackpos)
+ if not thingAtPos or thingAtPos ~= item then
+ return
+ end
+
+ local it = Game.getItemTypeByClientId(clientId)
+ if not it or it:getClientId() ~= clientId or it:getId() ~= item:getId() then
+ return
+ end
+
+ player:onPodiumEdit(item, outfit, direction, isVisible)
+ else
+ print("Warning: Unknown outfitType received in set outfit message: " .. outfitType)
+ end
+end
+
+handler:register()
diff --git a/data/scripts/systems/outfits/network/toggle_mount.lua b/data/scripts/systems/outfits/network/toggle_mount.lua
new file mode 100644
index 0000000000..393da585a3
--- /dev/null
+++ b/data/scripts/systems/outfits/network/toggle_mount.lua
@@ -0,0 +1,17 @@
+local handler = PacketHandler(0xD4)
+
+-- 0xD4: Toggle Mount (client -> server)
+-- Payload: mounted:bool (true = mount, false = dismount)
+-- Notes:
+-- - Cooldown is enforced by Player.toggleMount (systems/outfits/core/mounts.lua).
+-- - Mounting from a protection zone is rejected by Player.toggleMount.
+function handler.onReceive(player, msg)
+ if not Outfits.AllowToggleMount then
+ return
+ end
+
+ local mounted = msg:getBool()
+ player:toggleMount(mounted)
+end
+
+handler:register()
diff --git a/data/scripts/talkactions/commands/reload.lua b/data/scripts/talkactions/commands/reload.lua
index 81519d5e4e..8b5922334c 100644
--- a/data/scripts/talkactions/commands/reload.lua
+++ b/data/scripts/talkactions/commands/reload.lua
@@ -25,9 +25,6 @@ local reloadTypes = {
["monster"] = RELOAD_TYPE_MONSTERS,
["monsters"] = RELOAD_TYPE_MONSTERS,
- ["mount"] = RELOAD_TYPE_MOUNTS,
- ["mounts"] = RELOAD_TYPE_MOUNTS,
-
["move"] = RELOAD_TYPE_MOVEMENTS,
["movement"] = RELOAD_TYPE_MOVEMENTS,
["movements"] = RELOAD_TYPE_MOVEMENTS,
diff --git a/schema.sql b/schema.sql
index b41feca8a6..79fe5ef56a 100644
--- a/schema.sql
+++ b/schema.sql
@@ -32,8 +32,6 @@ CREATE TABLE IF NOT EXISTS `players` (
`lookmountbody` int NOT NULL DEFAULT '0',
`lookmountlegs` int NOT NULL DEFAULT '0',
`lookmountfeet` int NOT NULL DEFAULT '0',
- `currentmount` smallint unsigned NOT NULL DEFAULT '0',
- `randomizemount` tinyint NOT NULL DEFAULT '0',
`direction` tinyint unsigned NOT NULL DEFAULT '2',
`maglevel` int NOT NULL DEFAULT '0',
`mana` int NOT NULL DEFAULT '0',
@@ -342,21 +340,6 @@ CREATE TABLE IF NOT EXISTS `player_storage` (
FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8;
-CREATE TABLE IF NOT EXISTS `player_outfits` (
- `player_id` int NOT NULL DEFAULT '0',
- `outfit_id` smallint unsigned NOT NULL DEFAULT '0',
- `addons` tinyint unsigned NOT NULL DEFAULT '0',
- PRIMARY KEY (`player_id`,`outfit_id`),
- FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8;
-
-CREATE TABLE IF NOT EXISTS `player_mounts` (
- `player_id` int NOT NULL DEFAULT '0',
- `mount_id` smallint unsigned NOT NULL DEFAULT '0',
- PRIMARY KEY (`player_id`,`mount_id`),
- FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8;
-
CREATE TABLE IF NOT EXISTS `server_config` (
`config` varchar(50) NOT NULL,
`value` varchar(256) NOT NULL DEFAULT '',
@@ -391,7 +374,7 @@ CREATE TABLE IF NOT EXISTS `towns` (
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8;
-INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '37'), ('players_record', '0');
+INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '38'), ('players_record', '0');
DROP TRIGGER IF EXISTS `ondelete_players`;
DROP TRIGGER IF EXISTS `oncreate_guilds`;
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3d3bdecd4d..0bec4b64a5 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -35,11 +35,9 @@ set(tfs_SRC
${CMAKE_CURRENT_LIST_DIR}/matrixarea.cpp
${CMAKE_CURRENT_LIST_DIR}/monster.cpp
${CMAKE_CURRENT_LIST_DIR}/monsters.cpp
- ${CMAKE_CURRENT_LIST_DIR}/mounts.cpp
${CMAKE_CURRENT_LIST_DIR}/movement.cpp
${CMAKE_CURRENT_LIST_DIR}/networkmessage.cpp
${CMAKE_CURRENT_LIST_DIR}/npc.cpp
- ${CMAKE_CURRENT_LIST_DIR}/outfit.cpp
${CMAKE_CURRENT_LIST_DIR}/outputmessage.cpp
${CMAKE_CURRENT_LIST_DIR}/party.cpp
${CMAKE_CURRENT_LIST_DIR}/player.cpp
@@ -113,11 +111,9 @@ set(tfs_HDR
${CMAKE_CURRENT_LIST_DIR}/matrixarea.h
${CMAKE_CURRENT_LIST_DIR}/monster.h
${CMAKE_CURRENT_LIST_DIR}/monsters.h
- ${CMAKE_CURRENT_LIST_DIR}/mounts.h
${CMAKE_CURRENT_LIST_DIR}/movement.h
${CMAKE_CURRENT_LIST_DIR}/networkmessage.h
${CMAKE_CURRENT_LIST_DIR}/npc.h
- ${CMAKE_CURRENT_LIST_DIR}/outfit.h
${CMAKE_CURRENT_LIST_DIR}/outputmessage.h
${CMAKE_CURRENT_LIST_DIR}/party.h
${CMAKE_CURRENT_LIST_DIR}/player.h
diff --git a/src/configmanager.cpp b/src/configmanager.cpp
index 9e4fa245ac..de875cf126 100644
--- a/src/configmanager.cpp
+++ b/src/configmanager.cpp
@@ -203,7 +203,6 @@ bool ConfigManager::load()
integer[MARKET_OFFER_DURATION] = getGlobalNumber(L, "marketOfferDuration", 30 * 24 * 60 * 60);
}
- boolean[ALLOW_CHANGEOUTFIT] = getGlobalBoolean(L, "allowChangeOutfit", true);
boolean[ONE_PLAYER_ON_ACCOUNT] = getGlobalBoolean(L, "onePlayerOnlinePerAccount", true);
boolean[AIMBOT_HOTKEY_ENABLED] = getGlobalBoolean(L, "hotkeyAimbotEnabled", true);
boolean[REMOVE_RUNE_CHARGES] = getGlobalBoolean(L, "removeChargesFromRunes", true);
diff --git a/src/configmanager.h b/src/configmanager.h
index c476f24b3c..7c90047afa 100644
--- a/src/configmanager.h
+++ b/src/configmanager.h
@@ -8,7 +8,6 @@ namespace ConfigManager {
enum boolean_config_t
{
- ALLOW_CHANGEOUTFIT,
ONE_PLAYER_ON_ACCOUNT,
AIMBOT_HOTKEY_ENABLED,
REMOVE_RUNE_CHARGES,
diff --git a/src/const.h b/src/const.h
index a67e373569..e257401cfb 100644
--- a/src/const.h
+++ b/src/const.h
@@ -669,7 +669,6 @@ enum ReloadTypes_t : uint8_t
RELOAD_TYPE_GLOBALEVENTS,
RELOAD_TYPE_ITEMS,
RELOAD_TYPE_MONSTERS,
- RELOAD_TYPE_MOUNTS,
RELOAD_TYPE_MOVEMENTS,
RELOAD_TYPE_NPCS,
RELOAD_TYPE_QUESTS,
diff --git a/src/creature.h b/src/creature.h
index 503880414f..f50355cebe 100644
--- a/src/creature.h
+++ b/src/creature.h
@@ -409,7 +409,6 @@ class Creature : public Thing
Outfit_t currentOutfit;
Outfit_t defaultOutfit;
- uint16_t currentMount;
LightInfo internalLight;
diff --git a/src/game.cpp b/src/game.cpp
index e3de8520dd..9ff9b68999 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -17,7 +17,6 @@
#include "iomarket.h"
#include "items.h"
#include "movement.h"
-#include "outfit.h"
#include "party.h"
#include "podium.h"
#include "scheduler.h"
@@ -79,8 +78,6 @@ void Game::setGameState(GameState_t newState)
map.spawns.startup();
- mounts.loadFromXml();
-
tfs::events::game::onStartup();
break;
}
@@ -587,6 +584,7 @@ bool Game::removeCreature(const std::shared_ptr& creature, bool isLogo
summon->setSkillLoss(false);
removeCreature(summon);
}
+
return true;
}
@@ -3351,82 +3349,6 @@ void Game::playerEditPodium(uint32_t playerId, Outfit_t outfit, const Position&
tfs::events::player::onPodiumEdit(player, item, outfit, podiumVisible, direction);
}
-void Game::playerToggleMount(uint32_t playerId, bool mount)
-{
- const auto& player = getPlayerByID(playerId);
- if (!player) {
- return;
- }
-
- player->toggleMount(mount);
-}
-
-void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit, bool randomizeMount /* = false*/)
-{
- if (!getBoolean(ConfigManager::ALLOW_CHANGEOUTFIT)) {
- return;
- }
-
- const auto& player = getPlayerByID(playerId);
- if (!player) {
- return;
- }
-
- player->setRandomizeMount(randomizeMount);
-
- const Outfit* playerOutfit = Outfits::getInstance().getOutfitByLookType(player->getSex(), outfit.lookType);
- if (!playerOutfit) {
- outfit.lookMount = 0;
- }
-
- if (outfit.lookMount != 0) {
- Mount* mount = mounts.getMountByClientID(outfit.lookMount);
- if (!mount) {
- return;
- }
-
- if (!player->hasMount(mount)) {
- return;
- }
-
- int32_t speedChange = mount->speed;
- if (player->isMounted()) {
- Mount* prevMount = mounts.getMountByID(player->getCurrentMount());
- if (prevMount) {
- speedChange -= prevMount->speed;
- }
- }
-
- changeSpeed(player, speedChange);
- player->setCurrentMount(mount->id);
- } else {
- if (player->isMounted()) {
- player->dismount();
- }
-
- player->setWasMounted(false);
- }
-
- if (player->canWear(outfit.lookType, outfit.lookAddons)) {
- player->defaultOutfit = outfit;
-
- if (player->hasCondition(CONDITION_OUTFIT)) {
- return;
- }
-
- if (player->getRandomizeMount() && player->hasMounts()) {
- const Mount* mount = mounts.getMountByID(player->getRandomMount());
- outfit.lookMount = mount->clientId;
- }
-
- internalCreatureChangeOutfit(player, outfit);
- }
-
- if (player->isMounted()) {
- player->onChangeZone(player->getZone());
- }
-}
-
void Game::playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, const std::string& receiver,
const std::string& text)
{
@@ -5498,8 +5420,6 @@ bool Game::reload(ReloadTypes_t reloadType)
return Item::items.reload();
case RELOAD_TYPE_MONSTERS:
return g_monsters.reload();
- case RELOAD_TYPE_MOUNTS:
- return mounts.reload();
case RELOAD_TYPE_MOVEMENTS:
return g_moveEvents->reload();
case RELOAD_TYPE_NPCS: {
@@ -5539,7 +5459,6 @@ bool Game::reload(ReloadTypes_t reloadType)
/*
Npcs::reload();
Item::items.reload();
- mounts.reload();
ConfigManager::reload();
tfs::events::load();
g_chat.load();
@@ -5565,7 +5484,6 @@ bool Game::reload(ReloadTypes_t reloadType)
Item::items.reload();
g_weapons->clear(true);
g_weapons->loadDefaults();
- mounts.reload();
g_globalEvents->reload();
tfs::events::reload();
g_chat.load();
diff --git a/src/game.h b/src/game.h
index f196a7ba6d..0680de545e 100644
--- a/src/game.h
+++ b/src/game.h
@@ -7,7 +7,6 @@
#include "groups.h"
#include "map.h"
#include "monster.h"
-#include "mounts.h"
#include "npc.h"
#include "player.h"
#include "wildcardtree.h"
@@ -375,14 +374,12 @@ class Game
const uint16_t spriteId, bool podiumVisible, Direction direction);
void playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, const std::string& receiver,
const std::string& text);
- void playerChangeOutfit(uint32_t playerId, Outfit_t outfit, bool randomizeMount = false);
void playerInviteToParty(uint32_t playerId, uint32_t invitedId);
void playerJoinParty(uint32_t playerId, uint32_t leaderId);
void playerRevokePartyInvitation(uint32_t playerId, uint32_t invitedId);
void playerPassPartyLeadership(uint32_t playerId, uint32_t newLeaderId);
void playerLeaveParty(uint32_t playerId);
void playerEnableSharedPartyExperience(uint32_t playerId, bool sharedExpActive);
- void playerToggleMount(uint32_t playerId, bool mount);
void playerLeaveMarket(uint32_t playerId);
void playerBrowseMarket(uint32_t playerId, uint16_t spriteId);
void playerBrowseMarketOwnOffers(uint32_t playerId);
@@ -470,7 +467,6 @@ class Game
Groups groups;
Map map;
- Mounts mounts;
std::vector> toDecayItems;
diff --git a/src/iologindata.cpp b/src/iologindata.cpp
index f834747f9f..d76fc22632 100644
--- a/src/iologindata.cpp
+++ b/src/iologindata.cpp
@@ -95,7 +95,7 @@ bool IOLoginData::loadPlayerById(const std::shared_ptr& player, uint32_t
return loadPlayer(
player,
db.storeQuery(std::format(
- "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `lookmount`, `lookmounthead`, `lookmountbody`, `lookmountlegs`, `lookmountfeet`, `currentmount`, `randomizemount`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `id` = {:d}",
+ "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `lookmount`, `lookmounthead`, `lookmountbody`, `lookmountlegs`, `lookmountfeet`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `id` = {:d}",
id)));
}
@@ -105,7 +105,7 @@ bool IOLoginData::loadPlayerByName(const std::shared_ptr& player, const
return loadPlayer(
player,
db.storeQuery(std::format(
- "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `lookmount`, `lookmounthead`, `lookmountbody`, `lookmountlegs`, `lookmountfeet`, `currentmount`, `randomizemount`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `name` = {:s}",
+ "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `lookmount`, `lookmounthead`, `lookmountbody`, `lookmountlegs`, `lookmountfeet`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `name` = {:s}",
db.escapeString(name))));
}
@@ -235,9 +235,7 @@ bool IOLoginData::loadPlayer(const std::shared_ptr& player, std::shared_
player->defaultOutfit.lookMountLegs = result->getNumber("lookmountlegs");
player->defaultOutfit.lookMountFeet = result->getNumber("lookmountfeet");
player->currentOutfit = player->defaultOutfit;
- player->currentMount = result->getNumber("currentmount");
player->setDirection(static_cast(result->getNumber("direction")));
- player->randomizeMount = result->getNumber("randomizemount") != 0;
if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) {
const time_t skullSeconds = result->getNumber("skulltime") - time(nullptr);
@@ -477,22 +475,6 @@ bool IOLoginData::loadPlayer(const std::shared_ptr& player, std::shared_
} while (vipRes->next());
}
- // load outfits & addons
- if (const auto& outfitsRes = db.storeQuery(std::format(
- "SELECT `outfit_id`, `addons` FROM `player_outfits` WHERE `player_id` = {:d}", player->getGUID()))) {
- do {
- player->addOutfit(outfitsRes->getNumber("outfit_id"), outfitsRes->getNumber("addons"));
- } while (outfitsRes->next());
- }
-
- // load mounts
- if (const auto& mountsRes = db.storeQuery(
- std::format("SELECT `mount_id` FROM `player_mounts` WHERE `player_id` = {:d}", player->getGUID()))) {
- do {
- player->tameMount(mountsRes->getNumber("mount_id"));
- } while (mountsRes->next());
- }
-
player->updateBaseSpeed();
player->updateInventoryWeight();
player->updateItemsLight(true);
@@ -629,8 +611,6 @@ bool IOLoginData::savePlayer(const std::shared_ptr& player)
query << "`lookmountbody` = " << static_cast(player->defaultOutfit.lookMountBody) << ',';
query << "`lookmountlegs` = " << static_cast(player->defaultOutfit.lookMountLegs) << ',';
query << "`lookmountfeet` = " << static_cast(player->defaultOutfit.lookMountFeet) << ',';
- query << "`currentmount` = " << static_cast(player->currentMount) << ',';
- query << "`randomizemount` = " << player->randomizeMount << ",";
query << "`maglevel` = " << player->magLevel << ',';
query << "`mana` = " << player->mana << ',';
query << "`manamax` = " << player->manaMax << ',';
@@ -815,40 +795,6 @@ bool IOLoginData::savePlayer(const std::shared_ptr& player)
return false;
}
- // save outfits & addons
- if (!db.executeQuery(std::format("DELETE FROM `player_outfits` WHERE `player_id` = {:d}", player->getGUID()))) {
- return false;
- }
-
- DBInsert outfitQuery("INSERT INTO `player_outfits` (`player_id`, `outfit_id`, `addons`) VALUES ");
-
- for (auto&& [outfitId, addons] : player->outfits | std::views::as_const) {
- if (!outfitQuery.addRow(std::format("{:d}, {:d}, {:d}", player->getGUID(), outfitId, addons))) {
- return false;
- }
- }
-
- if (!outfitQuery.execute()) {
- return false;
- }
-
- // save mounts
- if (!db.executeQuery(std::format("DELETE FROM `player_mounts` WHERE `player_id` = {:d}", player->getGUID()))) {
- return false;
- }
-
- DBInsert mountQuery("INSERT INTO `player_mounts` (`player_id`, `mount_id`) VALUES ");
-
- for (const auto& it : player->mounts) {
- if (!mountQuery.addRow(std::format("{:d}, {:d}", player->getGUID(), it))) {
- return false;
- }
- }
-
- if (!mountQuery.execute()) {
- return false;
- }
-
// End the transaction
return transaction.commit();
}
diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt
index a24b3cfb86..d3a9d5c67a 100644
--- a/src/lua/CMakeLists.txt
+++ b/src/lua/CMakeLists.txt
@@ -29,7 +29,6 @@ set(luamodules_SRC
${CMAKE_CURRENT_LIST_DIR}/modules/moveevent.cpp
${CMAKE_CURRENT_LIST_DIR}/modules/networkmessage.cpp
${CMAKE_CURRENT_LIST_DIR}/modules/npc.cpp
- ${CMAKE_CURRENT_LIST_DIR}/modules/outfit.cpp
${CMAKE_CURRENT_LIST_DIR}/modules/party.cpp
${CMAKE_CURRENT_LIST_DIR}/modules/player.cpp
${CMAKE_CURRENT_LIST_DIR}/modules/podium.cpp
diff --git a/src/lua/api.cpp b/src/lua/api.cpp
index 82a70160bf..27f4e2294f 100644
--- a/src/lua/api.cpp
+++ b/src/lua/api.cpp
@@ -5,7 +5,6 @@
#include "../game.h"
#include "../monster.h"
#include "../monsters.h"
-#include "../mounts.h"
#include "../npc.h"
#include "../podium.h"
#include "../spells.h"
@@ -182,19 +181,6 @@ Outfit_t getOutfit(lua_State* L, int32_t arg)
return outfit;
}
-Outfit getOutfitClass(lua_State* L, int32_t arg)
-{
- Outfit outfit{
- .name = getFieldString(L, arg, "name"),
- .lookType = getField(L, arg, "lookType"),
- .premium = getField(L, arg, "premium") == 1,
- .unlocked = getField(L, arg, "unlocked") == 1,
- };
-
- lua_pop(L, 4);
- return outfit;
-}
-
LuaVariant getVariant(lua_State* L, int32_t arg)
{
LuaVariant var;
@@ -345,16 +331,6 @@ void pushOutfit(lua_State* L, const Outfit_t& outfit)
setField(L, "lookMountFeet", outfit.lookMountFeet);
}
-void pushOutfit(lua_State* L, const Outfit* outfit)
-{
- lua_createtable(L, 0, 4);
- setField(L, "lookType", outfit->lookType);
- setField(L, "name", outfit->name);
- setField(L, "premium", outfit->premium);
- setField(L, "unlocked", outfit->unlocked);
- setMetatable(L, -1, "Outfit");
-}
-
void pushLoot(lua_State* L, const std::vector& lootList)
{
lua_createtable(L, lootList.size(), 0);
diff --git a/src/lua/api.h b/src/lua/api.h
index 5045d09114..e7c89041a1 100644
--- a/src/lua/api.h
+++ b/src/lua/api.h
@@ -1,6 +1,6 @@
#pragma once
-#include "../outfit.h"
+#include "../enums.h"
#include "error.h"
#include "variant.h"
@@ -15,7 +15,6 @@ class Thing;
class ItemType;
struct LootBlock;
-struct Mount;
struct Town;
namespace tfs::lua {
@@ -149,7 +148,6 @@ Position getPosition(lua_State* L, int32_t arg);
Position getPosition(lua_State* L, int32_t arg, int32_t& stackpos);
Outfit_t getOutfit(lua_State* L, int32_t arg);
-Outfit getOutfitClass(lua_State* L, int32_t arg);
LuaVariant getVariant(lua_State* L, int32_t arg);
@@ -160,7 +158,6 @@ std::shared_ptr getPlayer(lua_State* L, int32_t arg);
// High-level push helpers (C++ -> Lua)
void pushPosition(lua_State* L, const Position& position, int32_t stackpos = 0);
void pushOutfit(lua_State* L, const Outfit_t& outfit);
-void pushOutfit(lua_State* L, const Outfit* outfit);
void pushVariant(lua_State* L, const LuaVariant& var);
void pushThing(lua_State* L, const std::shared_ptr& thing);
diff --git a/src/lua/modules.cpp b/src/lua/modules.cpp
index ee3fcd7a1b..2cb99ee5a0 100644
--- a/src/lua/modules.cpp
+++ b/src/lua/modules.cpp
@@ -40,7 +40,6 @@ void importModules(LuaScriptInterface& lsi)
registerMonsters(lsi);
registerMoveEvent(lsi);
registerNetworkMessage(lsi);
- registerOutfit(lsi);
registerParty(lsi);
registerPosition(lsi);
registerSpell(lsi);
diff --git a/src/lua/modules/configmanager.cpp b/src/lua/modules/configmanager.cpp
index 96105332af..9b7518dd23 100644
--- a/src/lua/modules/configmanager.cpp
+++ b/src/lua/modules/configmanager.cpp
@@ -48,7 +48,6 @@ void tfs::lua::registerConfigManager(LuaScriptInterface& lsi)
lsi.registerTable("configKeys");
- registerEnumIn(lsi, "configKeys", ConfigManager::ALLOW_CHANGEOUTFIT);
registerEnumIn(lsi, "configKeys", ConfigManager::ONE_PLAYER_ON_ACCOUNT);
registerEnumIn(lsi, "configKeys", ConfigManager::AIMBOT_HOTKEY_ENABLED);
registerEnumIn(lsi, "configKeys", ConfigManager::REMOVE_RUNE_CHARGES);
diff --git a/src/lua/modules/game.cpp b/src/lua/modules/game.cpp
index 8d520f5a16..b2ee213d56 100644
--- a/src/lua/modules/game.cpp
+++ b/src/lua/modules/game.cpp
@@ -222,22 +222,6 @@ int luaGameGetItemTypeByClientId(lua_State* L)
return 1;
}
-int luaGameGetMountIdByLookType(lua_State* L)
-{
- // Game.getMountIdByLookType(lookType)
- Mount* mount = nullptr;
- if (tfs::lua::isNumber(L, 1)) {
- mount = g_game.mounts.getMountByClientID(tfs::lua::getNumber(L, 1));
- }
-
- if (mount) {
- tfs::lua::pushNumber(L, mount->id);
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
int luaGameGetParties(lua_State* L)
{
// Game.getParties()
@@ -282,54 +266,6 @@ int luaGameGetHouses(lua_State* L)
return 1;
}
-int luaGameGetOutfits(lua_State* L)
-{
- // Game.getOutfits(playerSex)
- if (!tfs::lua::isNumber(L, 1)) {
- lua_pushnil(L);
- return 1;
- }
-
- PlayerSex_t playerSex = tfs::lua::getNumber(L, 1);
- if (playerSex > PLAYERSEX_LAST) {
- lua_pushnil(L);
- return 1;
- }
-
- const auto& outfits = Outfits::getInstance().getOutfits(playerSex);
- lua_createtable(L, outfits.size(), 0);
-
- int index = 0;
- for (const auto& outfit : outfits) {
- tfs::lua::pushOutfit(L, &outfit);
- lua_rawseti(L, -2, ++index);
- }
-
- return 1;
-}
-
-int luaGameGetMounts(lua_State* L)
-{
- // Game.getMounts()
- const auto& mounts = g_game.mounts.getMounts();
- lua_createtable(L, mounts.size(), 0);
-
- int index = 0;
- for (const auto& mount : mounts) {
- lua_createtable(L, 0, 5);
-
- tfs::lua::setField(L, "name", mount.name);
- tfs::lua::setField(L, "speed", mount.speed);
- tfs::lua::setField(L, "clientId", mount.clientId);
- tfs::lua::setField(L, "id", mount.id);
- tfs::lua::setField(L, "premium", mount.premium);
-
- lua_rawseti(L, -2, ++index);
- }
-
- return 1;
-}
-
int luaGameGetVocations(lua_State* L)
{
// Game.getVocations()
@@ -715,13 +651,10 @@ void tfs::lua::registerGame(LuaScriptInterface& lsi)
lsi.registerMethod("Game", "getBestiary", luaGameGetBestiary);
lsi.registerMethod("Game", "getCurrencyItems", luaGameGetCurrencyItems);
lsi.registerMethod("Game", "getItemTypeByClientId", luaGameGetItemTypeByClientId);
- lsi.registerMethod("Game", "getMountIdByLookType", luaGameGetMountIdByLookType);
lsi.registerMethod("Game", "getParties", luaGameGetParties);
lsi.registerMethod("Game", "getTowns", luaGameGetTowns);
lsi.registerMethod("Game", "getHouses", luaGameGetHouses);
- lsi.registerMethod("Game", "getOutfits", luaGameGetOutfits);
- lsi.registerMethod("Game", "getMounts", luaGameGetMounts);
lsi.registerMethod("Game", "getVocations", luaGameGetVocations);
lsi.registerMethod("Game", "getRuneSpells", luaGameGetRuneSpells);
lsi.registerMethod("Game", "getInstantSpells", luaGameGetInstantSpells);
diff --git a/src/lua/modules/globals.cpp b/src/lua/modules/globals.cpp
index ed9e5531ea..2766b141f6 100644
--- a/src/lua/modules/globals.cpp
+++ b/src/lua/modules/globals.cpp
@@ -319,7 +319,6 @@ void tfs::lua::registerGlobals(LuaScriptInterface& lsi)
registerEnum(lsi, RELOAD_TYPE_GLOBALEVENTS);
registerEnum(lsi, RELOAD_TYPE_ITEMS);
registerEnum(lsi, RELOAD_TYPE_MONSTERS);
- registerEnum(lsi, RELOAD_TYPE_MOUNTS);
registerEnum(lsi, RELOAD_TYPE_MOVEMENTS);
registerEnum(lsi, RELOAD_TYPE_NPCS);
registerEnum(lsi, RELOAD_TYPE_QUESTS);
diff --git a/src/lua/modules/outfit.cpp b/src/lua/modules/outfit.cpp
deleted file mode 100644
index 5fc8e7ce10..0000000000
--- a/src/lua/modules/outfit.cpp
+++ /dev/null
@@ -1,38 +0,0 @@
-#include "../../otpch.h"
-
-#include "../../outfit.h"
-
-#include "../api.h"
-#include "../register.h"
-#include "../script.h"
-
-namespace {
-
-int luaOutfitCreate(lua_State* L)
-{
- // Outfit(looktype)
- const Outfit* outfit = Outfits::getInstance().getOutfitByLookType(tfs::lua::getNumber(L, 2));
- if (outfit) {
- tfs::lua::pushOutfit(L, outfit);
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
-int luaOutfitCompare(lua_State* L)
-{
- // outfit == outfitEx
- Outfit outfitEx = tfs::lua::getOutfitClass(L, 2);
- Outfit outfit = tfs::lua::getOutfitClass(L, 1);
- tfs::lua::pushBoolean(L, outfit == outfitEx);
- return 1;
-}
-
-} // namespace
-
-void tfs::lua::registerOutfit(LuaScriptInterface& lsi)
-{
- lsi.registerClass("Outfit", "", luaOutfitCreate);
- lsi.registerMetaMethod("Outfit", "__eq", luaOutfitCompare);
-}
diff --git a/src/lua/modules/player.cpp b/src/lua/modules/player.cpp
index 8f37463b9f..df69587b8d 100644
--- a/src/lua/modules/player.cpp
+++ b/src/lua/modules/player.cpp
@@ -1537,192 +1537,53 @@ int luaPlayerGetParty(lua_State* L)
return 1;
}
-int luaPlayerAddOutfit(lua_State* L)
+int luaPlayerGetCurrentOutfit(lua_State* L)
{
- // player:addOutfit(lookType)
+ // player:getCurrentOutfit()
if (const auto& player = tfs::lua::getSharedPtr(L, 1)) {
- player->addOutfit(tfs::lua::getNumber(L, 2), 0);
- tfs::lua::pushBoolean(L, true);
+ tfs::lua::pushOutfit(L, player->getCurrentOutfit());
} else {
lua_pushnil(L);
}
return 1;
}
-int luaPlayerAddOutfitAddon(lua_State* L)
+int luaPlayerSetCurrentOutfit(lua_State* L)
{
- // player:addOutfitAddon(lookType, addon)
- if (const auto& player = tfs::lua::getSharedPtr(L, 1)) {
- uint16_t lookType = tfs::lua::getNumber(L, 2);
- uint8_t addon = tfs::lua::getNumber(L, 3);
- player->addOutfit(lookType, addon);
- tfs::lua::pushBoolean(L, true);
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
-int luaPlayerRemoveOutfit(lua_State* L)
-{
- // player:removeOutfit(lookType)
- if (const auto& player = tfs::lua::getSharedPtr(L, 1)) {
- uint16_t lookType = tfs::lua::getNumber(L, 2);
- tfs::lua::pushBoolean(L, player->removeOutfit(lookType));
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
-int luaPlayerRemoveOutfitAddon(lua_State* L)
-{
- // player:removeOutfitAddon(lookType, addon)
- if (const auto& player = tfs::lua::getSharedPtr(L, 1)) {
- uint16_t lookType = tfs::lua::getNumber(L, 2);
- uint8_t addon = tfs::lua::getNumber(L, 3);
- tfs::lua::pushBoolean(L, player->removeOutfitAddon(lookType, addon));
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
-int luaPlayerHasOutfit(lua_State* L)
-{
- // player:hasOutfit(lookType[, addon = 0])
- if (const auto& player = tfs::lua::getSharedPtr(L, 1)) {
- uint16_t lookType = tfs::lua::getNumber(L, 2);
- uint8_t addon = tfs::lua::getNumber(L, 3, 0);
- tfs::lua::pushBoolean(L, player->hasOutfit(lookType, addon));
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
-int luaPlayerCanWearOutfit(lua_State* L)
-{
- // player:canWearOutfit(lookType[, addon = 0])
- if (const auto& player = tfs::lua::getSharedPtr(L, 1)) {
- uint16_t lookType = tfs::lua::getNumber(L, 2);
- uint8_t addon = tfs::lua::getNumber(L, 3, 0);
- tfs::lua::pushBoolean(L, player->canWear(lookType, addon));
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
-int luaPlayerSendOutfitWindow(lua_State* L)
-{
- // player:sendOutfitWindow()
- if (const auto& player = tfs::lua::getSharedPtr(L, 1)) {
- player->sendOutfitWindow();
- tfs::lua::pushBoolean(L, true);
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
-int luaPlayerSendEditPodium(lua_State* L)
-{
- // player:sendEditPodium(item)
- auto player = tfs::lua::getSharedPtr(L, 1);
- const auto& item = tfs::lua::getSharedPtr- (L, 2);
- if (player && item) {
- player->sendPodiumWindow(item);
- tfs::lua::pushBoolean(L, true);
- } else {
- lua_pushnil(L);
- }
- return 1;
-}
-
-int luaPlayerAddMount(lua_State* L)
-{
- // player:addMount(mountId or mountName)
- const auto& player = tfs::lua::getSharedPtr(L, 1);
- if (!player) {
- lua_pushnil(L);
- return 1;
- }
-
- uint16_t mountId;
- if (tfs::lua::isNumber(L, 2)) {
- mountId = tfs::lua::getNumber(L, 2);
- } else {
- Mount* mount = g_game.mounts.getMountByName(tfs::lua::getString(L, 2));
- if (!mount) {
- lua_pushnil(L);
- return 1;
- }
- mountId = mount->id;
- }
- tfs::lua::pushBoolean(L, player->tameMount(mountId));
- return 1;
-}
-
-int luaPlayerRemoveMount(lua_State* L)
-{
- // player:removeMount(mountId or mountName)
+ // player:setCurrentOutfit(outfit)
const auto& player = tfs::lua::getSharedPtr(L, 1);
if (!player) {
lua_pushnil(L);
return 1;
}
- uint16_t mountId;
- if (tfs::lua::isNumber(L, 2)) {
- mountId = tfs::lua::getNumber(L, 2);
- } else {
- Mount* mount = g_game.mounts.getMountByName(tfs::lua::getString(L, 2));
- if (!mount) {
- lua_pushnil(L);
- return 1;
- }
- mountId = mount->id;
- }
- tfs::lua::pushBoolean(L, player->untameMount(mountId));
+ player->setCurrentOutfit(tfs::lua::getOutfit(L, 2));
+ tfs::lua::pushBoolean(L, true);
return 1;
}
-int luaPlayerHasMount(lua_State* L)
+int luaPlayerGetDefaultOutfit(lua_State* L)
{
- // player:hasMount(mountId or mountName)
- const auto& player = tfs::lua::getSharedPtr(L, 1);
- if (!player) {
- lua_pushnil(L);
- return 1;
- }
-
- Mount* mount = nullptr;
- if (tfs::lua::isNumber(L, 2)) {
- mount = g_game.mounts.getMountByID(tfs::lua::getNumber(L, 2));
- } else {
- mount = g_game.mounts.getMountByName(tfs::lua::getString(L, 2));
- }
-
- if (mount) {
- tfs::lua::pushBoolean(L, player->hasMount(mount));
+ // player:getDefaultOutfit()
+ if (const auto& player = tfs::lua::getSharedPtr(L, 1)) {
+ tfs::lua::pushOutfit(L, player->getDefaultOutfit());
} else {
lua_pushnil(L);
}
return 1;
}
-int luaPlayerToggleMount(lua_State* L)
+int luaPlayerSetDefaultOutfit(lua_State* L)
{
- // player:toggleMount(mount)
+ // player:setDefaultOutfit(outfit)
const auto& player = tfs::lua::getSharedPtr(L, 1);
if (!player) {
lua_pushnil(L);
return 1;
}
- bool mount = tfs::lua::getBoolean(L, 2);
- tfs::lua::pushBoolean(L, player->toggleMount(mount));
+ player->setDefaultOutfit(tfs::lua::getOutfit(L, 2));
+ tfs::lua::pushBoolean(L, true);
return 1;
}
@@ -2599,20 +2460,10 @@ void tfs::lua::registerPlayer(LuaScriptInterface& lsi)
lsi.registerMethod("Player", "getParty", luaPlayerGetParty);
- lsi.registerMethod("Player", "addOutfit", luaPlayerAddOutfit);
- lsi.registerMethod("Player", "addOutfitAddon", luaPlayerAddOutfitAddon);
- lsi.registerMethod("Player", "removeOutfit", luaPlayerRemoveOutfit);
- lsi.registerMethod("Player", "removeOutfitAddon", luaPlayerRemoveOutfitAddon);
- lsi.registerMethod("Player", "hasOutfit", luaPlayerHasOutfit);
- lsi.registerMethod("Player", "canWearOutfit", luaPlayerCanWearOutfit);
- lsi.registerMethod("Player", "sendOutfitWindow", luaPlayerSendOutfitWindow);
-
- lsi.registerMethod("Player", "sendEditPodium", luaPlayerSendEditPodium);
-
- lsi.registerMethod("Player", "addMount", luaPlayerAddMount);
- lsi.registerMethod("Player", "removeMount", luaPlayerRemoveMount);
- lsi.registerMethod("Player", "hasMount", luaPlayerHasMount);
- lsi.registerMethod("Player", "toggleMount", luaPlayerToggleMount);
+ lsi.registerMethod("Player", "getCurrentOutfit", luaPlayerGetCurrentOutfit);
+ lsi.registerMethod("Player", "setCurrentOutfit", luaPlayerSetCurrentOutfit);
+ lsi.registerMethod("Player", "getDefaultOutfit", luaPlayerGetDefaultOutfit);
+ lsi.registerMethod("Player", "setDefaultOutfit", luaPlayerSetDefaultOutfit);
lsi.registerMethod("Player", "getPremiumEndsAt", luaPlayerGetPremiumEndsAt);
lsi.registerMethod("Player", "setPremiumEndsAt", luaPlayerSetPremiumEndsAt);
diff --git a/src/lua/register.h b/src/lua/register.h
index f0220d750e..456fa07a2b 100644
--- a/src/lua/register.h
+++ b/src/lua/register.h
@@ -25,7 +25,6 @@ void registerMonsters(LuaScriptInterface& i);
void registerMoveEvent(LuaScriptInterface& i);
void registerNetworkMessage(LuaScriptInterface& i);
void registerNpc(LuaScriptInterface& i);
-void registerOutfit(LuaScriptInterface& i);
void registerParty(LuaScriptInterface& i);
void registerPlayer(LuaScriptInterface& i);
void registerPodium(LuaScriptInterface& i);
diff --git a/src/lua/script.h b/src/lua/script.h
index 426a254d99..ad98488db0 100644
--- a/src/lua/script.h
+++ b/src/lua/script.h
@@ -14,7 +14,6 @@ class LuaVariant;
class Npc;
class Player;
class Thing;
-struct Outfit;
using Combat_ptr = std::shared_ptr;
diff --git a/src/mounts.cpp b/src/mounts.cpp
deleted file mode 100644
index d4daee95de..0000000000
--- a/src/mounts.cpp
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2023 The Forgotten Server Authors. All rights reserved.
-// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
-
-#include "otpch.h"
-
-#include "mounts.h"
-
-#include "pugicast.h"
-#include "tools.h"
-
-bool Mounts::reload()
-{
- mounts.clear();
- return loadFromXml();
-}
-
-bool Mounts::loadFromXml()
-{
- pugi::xml_document doc;
- pugi::xml_parse_result result = doc.load_file("data/XML/mounts.xml");
- if (!result) {
- printXMLError("Error - Mounts::loadFromXml", "data/XML/mounts.xml", result);
- return false;
- }
-
- for (auto mountNode : doc.child("mounts").children()) {
- uint32_t nodeId = pugi::cast(mountNode.attribute("id").value());
- if (nodeId == 0 || nodeId > std::numeric_limits::max()) {
- std::cout << "[Notice - Mounts::loadFromXml] Mount id \"" << nodeId << "\" is not within 1 and 65535 range"
- << std::endl;
- continue;
- }
-
- if (getMountByID(nodeId)) {
- std::cout << "[Notice - Mounts::loadFromXml] Duplicate mount with id: " << nodeId << std::endl;
- continue;
- }
-
- mounts.emplace_back(
- static_cast(nodeId), pugi::cast(mountNode.attribute("clientid").value()),
- mountNode.attribute("name").as_string(), pugi::cast(mountNode.attribute("speed").value()),
- mountNode.attribute("premium").as_bool());
- }
- mounts.shrink_to_fit();
- return true;
-}
-
-Mount* Mounts::getMountByID(uint16_t id)
-{
- auto it = std::find_if(mounts.begin(), mounts.end(), [id](const Mount& mount) { return mount.id == id; });
-
- return it != mounts.end() ? &*it : nullptr;
-}
-
-Mount* Mounts::getMountByName(const std::string& name)
-{
- for (auto& it : mounts) {
- if (boost::iequals(name, it.name)) {
- return ⁢
- }
- }
-
- return nullptr;
-}
-
-Mount* Mounts::getMountByClientID(uint16_t clientId)
-{
- auto it = std::find_if(mounts.begin(), mounts.end(),
- [clientId](const Mount& mount) { return mount.clientId == clientId; });
-
- return it != mounts.end() ? &*it : nullptr;
-}
diff --git a/src/mounts.h b/src/mounts.h
deleted file mode 100644
index f89f9a673a..0000000000
--- a/src/mounts.h
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2023 The Forgotten Server Authors. All rights reserved.
-// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
-
-#ifndef FS_MOUNTS_H
-#define FS_MOUNTS_H
-
-struct Mount
-{
- Mount(uint16_t id, uint16_t clientId, std::string name, int32_t speed, bool premium) :
- name(std::move(name)), speed(speed), clientId(clientId), id(id), premium(premium)
- {}
-
- std::string name;
- int32_t speed;
- uint16_t clientId;
- uint16_t id;
- bool premium;
-};
-
-class Mounts
-{
-public:
- bool reload();
- bool loadFromXml();
- Mount* getMountByID(uint16_t id);
- Mount* getMountByName(const std::string& name);
- Mount* getMountByClientID(uint16_t clientId);
-
- const std::vector& getMounts() const { return mounts; }
-
-private:
- std::vector mounts;
-};
-
-#endif // FS_MOUNTS_H
diff --git a/src/otserv.cpp b/src/otserv.cpp
index 60a90d4fe1..6075ea2ab4 100644
--- a/src/otserv.cpp
+++ b/src/otserv.cpp
@@ -10,7 +10,6 @@
#include "http/http.h"
#include "iomarket.h"
#include "monsters.h"
-#include "outfit.h"
#include "protocolstatus.h"
#include "rsa.h"
#include "scheduler.h"
@@ -212,12 +211,6 @@ void mainLoader(ServiceManager* services)
return;
}
- std::cout << ">> Loading outfits" << std::endl;
- if (!Outfits::getInstance().loadFromXml()) {
- startupErrorMessage("Unable to load outfits!");
- return;
- }
-
std::cout << ">> Checking world type... " << std::flush;
std::string worldType = boost::algorithm::to_lower_copy(getString(ConfigManager::WORLD_TYPE));
if (worldType == "pvp") {
diff --git a/src/outfit.cpp b/src/outfit.cpp
deleted file mode 100644
index 84978d4373..0000000000
--- a/src/outfit.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2023 The Forgotten Server Authors. All rights reserved.
-// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
-
-#include "otpch.h"
-
-#include "outfit.h"
-
-#include "pugicast.h"
-#include "tools.h"
-
-bool Outfits::loadFromXml()
-{
- pugi::xml_document doc;
- pugi::xml_parse_result result = doc.load_file("data/XML/outfits.xml");
- if (!result) {
- printXMLError("Error - Outfits::loadFromXml", "data/XML/outfits.xml", result);
- return false;
- }
-
- for (auto outfitNode : doc.child("outfits").children()) {
- pugi::xml_attribute attr;
- if ((attr = outfitNode.attribute("enabled")) && !attr.as_bool()) {
- continue;
- }
-
- if (!(attr = outfitNode.attribute("type"))) {
- std::cout << "[Warning - Outfits::loadFromXml] Missing outfit type." << std::endl;
- continue;
- }
-
- uint16_t type = pugi::cast(attr.value());
- if (type > PLAYERSEX_LAST) {
- std::cout << "[Warning - Outfits::loadFromXml] Invalid outfit type " << type << "." << std::endl;
- continue;
- }
-
- pugi::xml_attribute lookTypeAttribute = outfitNode.attribute("looktype");
- if (!lookTypeAttribute) {
- std::cout << "[Warning - Outfits::loadFromXml] Missing looktype on outfit." << std::endl;
- continue;
- }
-
- outfits[type].push_back({.name = outfitNode.attribute("name").as_string(),
- .lookType = pugi::cast(lookTypeAttribute.value()),
- .premium = outfitNode.attribute("premium").as_bool(),
- .unlocked = outfitNode.attribute("unlocked").as_bool(true)});
- }
- return true;
-}
-
-const Outfit* Outfits::getOutfitByLookType(PlayerSex_t sex, uint16_t lookType) const
-{
- for (const Outfit& outfit : outfits[sex]) {
- if (outfit.lookType == lookType) {
- return &outfit;
- }
- }
- return nullptr;
-}
-
-const Outfit* Outfits::getOutfitByLookType(uint16_t lookType) const
-{
- for (uint8_t sex = PLAYERSEX_FEMALE; sex <= PLAYERSEX_LAST; sex++) {
- for (const Outfit& outfit : outfits[sex]) {
- if (outfit.lookType == lookType) {
- return &outfit;
- }
- }
- }
- return nullptr;
-}
diff --git a/src/outfit.h b/src/outfit.h
deleted file mode 100644
index 337ded5d6a..0000000000
--- a/src/outfit.h
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2023 The Forgotten Server Authors. All rights reserved.
-// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
-
-#ifndef FS_OUTFIT_H
-#define FS_OUTFIT_H
-
-#include "enums.h"
-
-struct Outfit
-{
- std::string name;
- uint16_t lookType;
- bool premium;
- bool unlocked;
-};
-
-inline bool operator==(const Outfit& lhs, const Outfit& rhs)
-{
- return lhs.name == rhs.name && lhs.lookType == rhs.lookType && lhs.premium == rhs.premium &&
- lhs.unlocked == rhs.unlocked;
-}
-
-struct ProtocolOutfit
-{
- ProtocolOutfit(std::string_view name, uint16_t lookType, uint8_t addons) :
- name{name}, lookType{lookType}, addons{addons}
- {}
-
- std::string name;
- uint16_t lookType;
- uint8_t addons;
-};
-
-class Outfits
-{
-public:
- static Outfits& getInstance()
- {
- static Outfits instance;
- return instance;
- }
-
- bool loadFromXml();
-
- const Outfit* getOutfitByLookType(PlayerSex_t sex, uint16_t lookType) const;
- const Outfit* getOutfitByLookType(uint16_t lookType) const;
- const std::vector& getOutfits(PlayerSex_t sex) const { return outfits[sex]; }
-
-private:
- std::vector outfits[PLAYERSEX_LAST + 1];
-};
-
-#endif // FS_OUTFIT_H
diff --git a/src/player.cpp b/src/player.cpp
index b242a4ccf6..d746d77c18 100644
--- a/src/player.cpp
+++ b/src/player.cpp
@@ -14,7 +14,6 @@
#include "house.h"
#include "iologindata.h"
#include "movement.h"
-#include "outfit.h"
#include "party.h"
#include "scheduler.h"
#include "tools.h"
@@ -1029,18 +1028,6 @@ void Player::onCreatureAppear(const std::shared_ptr& creature, bool is
onChangeZone(getZone());
- uint16_t currentMountId = currentOutfit.lookMount;
- if (currentMountId != 0) {
- if (Mount* currentMount = g_game.mounts.getMountByClientID(currentMountId)) {
- if (hasMount(currentMount)) {
- g_game.changeSpeed(asPlayer(), currentMount->speed);
- } else {
- defaultOutfit.lookMount = 0;
- g_game.internalCreatureChangeOutfit(asPlayer(), defaultOutfit);
- }
- }
- }
-
IOLoginData::updateOnlineStatus(guid, true);
if (const auto& guild = getGuild()) {
@@ -1120,17 +1107,6 @@ void Player::onChangeZone(ZoneType_t zone)
removeAttackedCreature();
onAttackedCreatureDisappear(false);
}
-
- if (!group->access && isMounted()) {
- dismount();
- g_game.internalCreatureChangeOutfit(asPlayer(), defaultOutfit);
- wasMounted_ = true;
- }
- } else {
- if (wasMounted_) {
- toggleMount(true);
- wasMounted_ = false;
- }
}
g_game.updateCreatureWalkthrough(asPlayer());
@@ -3410,10 +3386,6 @@ void Player::onAddCondition(ConditionType_t type)
{
Creature::onAddCondition(type);
- if (type == CONDITION_OUTFIT && isMounted()) {
- dismount();
- }
-
sendIcons();
}
@@ -3757,119 +3729,6 @@ void Player::changeSoul(int32_t soulChange)
sendStats();
}
-bool Player::canWear(uint32_t lookType, uint8_t addons) const
-{
- if (group->access) {
- return true;
- }
-
- const Outfit* outfit = Outfits::getInstance().getOutfitByLookType(sex, lookType);
- if (!outfit) {
- return false;
- }
-
- if (outfit->premium && !isPremium()) {
- return false;
- }
-
- if (outfit->unlocked && addons == 0) {
- return true;
- }
-
- for (const auto& [outfitType, addon] : outfits) {
- if (outfitType == lookType) {
- if (addon == addons || addon == 3 || addons == 0) {
- return true;
- }
- return false; // have lookType on list and addons don't match
- }
- }
- return false;
-}
-
-bool Player::hasOutfit(uint32_t lookType, uint8_t addons)
-{
- const Outfit* outfit = Outfits::getInstance().getOutfitByLookType(sex, lookType);
- if (!outfit) {
- return false;
- }
-
- if (outfit->unlocked && addons == 0) {
- return true;
- }
-
- for (const auto& [outfitType, addon] : outfits) {
- if (outfitType == lookType) {
- if (addon == addons || addon == 3 || addons == 0) {
- return true;
- }
- return false; // have lookType on list and addons don't match
- }
- }
- return false;
-}
-
-void Player::addOutfit(uint16_t lookType, uint8_t addons)
-{
- for (auto& [outfit, addon] : outfits) {
- if (outfit == lookType) {
- addon |= addons;
- return;
- }
- }
- outfits.insert(std::pair(lookType, addons));
-}
-
-bool Player::removeOutfit(uint16_t lookType)
-{
- for (const auto& [outfit, addon] : outfits) {
- if (outfit == lookType) {
- outfits.erase(outfit);
- return true;
- }
- }
- return false;
-}
-
-bool Player::removeOutfitAddon(uint16_t lookType, uint8_t addons)
-{
- for (auto& [outfit, addon] : outfits) {
- if (outfit == lookType) {
- addon &= ~addons;
- return true;
- }
- }
- return false;
-}
-
-bool Player::getOutfitAddons(const Outfit& outfit, uint8_t& addons) const
-{
- if (group->access) {
- addons = 3;
- return true;
- }
-
- if (outfit.premium && !isPremium()) {
- return false;
- }
-
- for (const auto& [lookType, addon] : outfits) {
- if (lookType != outfit.lookType) {
- continue;
- }
-
- addons = addon;
- return true;
- }
-
- if (!outfit.unlocked) {
- return false;
- }
-
- addons = 0;
- return true;
-}
-
void Player::setSex(PlayerSex_t newSex) { sex = newSex; }
Skulls_t Player::getSkull() const
@@ -4216,158 +4075,6 @@ GuildEmblems_t Player::getGuildEmblem(const std::shared_ptr& playe
return GUILDEMBLEM_NEUTRAL;
}
-uint16_t Player::getRandomMount() const
-{
- std::vector mountsId;
- for (const Mount& mount : g_game.mounts.getMounts()) {
- if (hasMount(&mount)) {
- mountsId.push_back(mount.id);
- }
- }
-
- return mountsId[uniform_random(0, mountsId.size() - 1)];
-}
-
-uint16_t Player::getCurrentMount() const { return currentMount; }
-
-void Player::setCurrentMount(uint16_t mountId) { currentMount = mountId; }
-
-bool Player::toggleMount(bool mount)
-{
- if ((OTSYS_TIME() - lastToggleMount) < 3000 && !wasMounted_) {
- sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED);
- return false;
- }
-
- if (mount) {
- if (isMounted()) {
- return false;
- }
-
- if (const auto& tile = getTile(); !group->access && tile->hasFlag(TILESTATE_PROTECTIONZONE)) {
- sendCancelMessage(RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE);
- return false;
- }
-
- const Outfit* playerOutfit = Outfits::getInstance().getOutfitByLookType(getSex(), defaultOutfit.lookType);
- if (!playerOutfit) {
- return false;
- }
-
- uint16_t currentMountId = getCurrentMount();
- if (currentMountId == 0) {
- sendOutfitWindow();
- return false;
- }
-
- if (randomizeMount) {
- currentMountId = getRandomMount();
- }
-
- Mount* currentMount = g_game.mounts.getMountByID(currentMountId);
- if (!currentMount) {
- return false;
- }
-
- if (!hasMount(currentMount)) {
- setCurrentMount(0);
- sendOutfitWindow();
- return false;
- }
-
- if (currentMount->premium && !isPremium()) {
- sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT);
- return false;
- }
-
- if (hasCondition(CONDITION_OUTFIT)) {
- sendCancelMessage(RETURNVALUE_NOTPOSSIBLE);
- return false;
- }
-
- defaultOutfit.lookMount = currentMount->clientId;
-
- if (currentMount->speed != 0) {
- g_game.changeSpeed(asPlayer(), currentMount->speed);
- }
- } else {
- if (!isMounted()) {
- return false;
- }
-
- dismount();
- }
-
- g_game.internalCreatureChangeOutfit(asPlayer(), defaultOutfit);
- lastToggleMount = OTSYS_TIME();
- return true;
-}
-
-bool Player::tameMount(uint16_t mountId)
-{
- Mount* mount = g_game.mounts.getMountByID(mountId);
- if (!mount || hasMount(mount)) {
- return false;
- }
-
- mounts.insert(mountId);
- return true;
-}
-
-bool Player::untameMount(uint16_t mountId)
-{
- Mount* mount = g_game.mounts.getMountByID(mountId);
- if (!mount || !hasMount(mount)) {
- return false;
- }
-
- mounts.erase(mountId);
-
- if (getCurrentMount() == mountId) {
- if (isMounted()) {
- dismount();
- g_game.internalCreatureChangeOutfit(asPlayer(), defaultOutfit);
- }
-
- setCurrentMount(0);
- }
-
- return true;
-}
-
-bool Player::hasMount(const Mount* mount) const
-{
- if (isAccessPlayer()) {
- return true;
- }
-
- if (mount->premium && !isPremium()) {
- return false;
- }
-
- return mounts.find(mount->id) != mounts.end();
-}
-
-bool Player::hasMounts() const
-{
- for (const Mount& mount : g_game.mounts.getMounts()) {
- if (hasMount(&mount)) {
- return true;
- }
- }
- return false;
-}
-
-void Player::dismount()
-{
- Mount* mount = g_game.mounts.getMountByID(getCurrentMount());
- if (mount && mount->speed > 0) {
- g_game.changeSpeed(asPlayer(), -mount->speed);
- }
-
- defaultOutfit.lookMount = 0;
-}
-
bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries)
{
if (tries == 0 || skill == SKILL_LEVEL) {
diff --git a/src/player.h b/src/player.h
index c14f9e7818..99becae986 100644
--- a/src/player.h
+++ b/src/player.h
@@ -18,7 +18,6 @@
#include "vocation.h"
class House;
-struct Mount;
class NetworkMessage;
class Npc;
class Party;
@@ -122,23 +121,6 @@ class Player final : public Creature
CreatureType_t getType() const override { return CREATURETYPE_PLAYER; }
- uint16_t getRandomMount() const;
- uint16_t getCurrentMount() const;
- void setCurrentMount(uint16_t mountId);
- bool isMounted() const { return defaultOutfit.lookMount != 0; }
- bool toggleMount(bool mount);
- bool tameMount(uint16_t mountId);
- bool untameMount(uint16_t mountId);
- bool hasMount(const Mount* mount) const;
- bool hasMounts() const;
- void dismount();
-
- bool wasMounted() const { return wasMounted_; }
- void setWasMounted(bool wasMounted) { wasMounted_ = wasMounted; }
-
- bool getRandomizeMount() const { return randomizeMount; }
- void setRandomizeMount(bool mode) { randomizeMount = mode; }
-
void sendFYIBox(const std::string& message)
{
if (client) {
@@ -566,12 +548,6 @@ class Player final : public Creature
client->sendCreatureSkull(creature);
}
}
- bool canWear(uint32_t lookType, uint8_t addons) const;
- bool hasOutfit(uint32_t lookType, uint8_t addons);
- void addOutfit(uint16_t lookType, uint8_t addons);
- bool removeOutfit(uint16_t lookType);
- bool removeOutfitAddon(uint16_t lookType, uint8_t addons);
- bool getOutfitAddons(const Outfit& outfit, uint8_t& addons) const;
size_t getMaxVIPEntries() const;
size_t getMaxDepotItems() const;
@@ -1093,18 +1069,6 @@ class Player final : public Creature
client->sendOpenPrivateChannel(receiver);
}
}
- void sendOutfitWindow()
- {
- if (client) {
- client->sendOutfitWindow();
- }
- }
- void sendPodiumWindow(const std::shared_ptr& item)
- {
- if (client) {
- client->sendPodiumWindow(item);
- }
- }
void sendCloseContainer(uint8_t cid)
{
if (client) {
@@ -1259,8 +1223,6 @@ class Player final : public Creature
std::map openContainers;
std::map> depotChests;
- std::map outfits;
- std::unordered_set mounts;
GuildWarVector guildWarVector;
std::list shopItemList;
@@ -1366,12 +1328,10 @@ class Player final : public Creature
bool chaseMode = false;
bool secureMode = false;
bool inMarket = false;
- bool wasMounted_ = false;
bool ghostMode = false;
bool pzLocked = false;
bool addAttackSkillPoint = false;
bool inventoryAbilities[CONST_SLOT_LAST + 1] = {};
- bool randomizeMount = false;
void updateItemsLight(bool internal = false);
int32_t getStepSpeed() const override
diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp
index 34c2a4adee..77614624f4 100644
--- a/src/protocolgame.cpp
+++ b/src/protocolgame.cpp
@@ -13,7 +13,6 @@
#include "game.h"
#include "iologindata.h"
#include "iomarket.h"
-#include "outfit.h"
#include "outputmessage.h"
#include "player.h"
#include "podium.h"
@@ -732,9 +731,8 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
parseSeekInContainer(msg);
break;
// case 0xCD: break; // request inspect window
- case 0xD3:
- parseSetOutfit(msg);
- break;
+ // case 0xD2: break; // request outfit window
+ // case 0xD3: break; // set outfit
// case 0xD5: break; // apply imbuement
// case 0xD6: break; // clear imbuement
// case 0xD7: break; // close imbuing window
@@ -1061,72 +1059,6 @@ void ProtocolGame::parseAutoWalk(NetworkMessage& msg)
[playerID = player->getID(), path = std::move(path)]() { g_game.playerAutoWalk(playerID, path); });
}
-void ProtocolGame::parseSetOutfit(NetworkMessage& msg)
-{
- uint8_t outfitType = msg.getByte();
-
- Outfit_t newOutfit;
- newOutfit.lookType = msg.get();
- newOutfit.lookHead = msg.getByte();
- newOutfit.lookBody = msg.getByte();
- newOutfit.lookLegs = msg.getByte();
- newOutfit.lookFeet = msg.getByte();
- newOutfit.lookAddons = msg.getByte();
-
- // Set outfit window
- if (outfitType == 0) {
- newOutfit.lookMount = msg.get();
- if (newOutfit.lookMount != 0) {
- newOutfit.lookMountHead = msg.getByte();
- newOutfit.lookMountBody = msg.getByte();
- newOutfit.lookMountLegs = msg.getByte();
- newOutfit.lookMountFeet = msg.getByte();
- } else {
- msg.skipBytes(4);
-
- // prevent mount color settings from resetting
- const Outfit_t& currentOutfit = player->getCurrentOutfit();
- newOutfit.lookMountHead = currentOutfit.lookMountHead;
- newOutfit.lookMountBody = currentOutfit.lookMountBody;
- newOutfit.lookMountLegs = currentOutfit.lookMountLegs;
- newOutfit.lookMountFeet = currentOutfit.lookMountFeet;
- }
-
- msg.get(); // familiar looktype
- bool randomizeMount = msg.getByte() == 0x01;
- g_dispatcher.addTask(
- [=, playerID = player->getID()]() { g_game.playerChangeOutfit(playerID, newOutfit, randomizeMount); });
-
- // Store "try outfit" window
- } else if (outfitType == 1) {
- newOutfit.lookMount = 0;
- // mount colors or store offerId (needs testing)
- newOutfit.lookMountHead = msg.getByte();
- newOutfit.lookMountBody = msg.getByte();
- newOutfit.lookMountLegs = msg.getByte();
- newOutfit.lookMountFeet = msg.getByte();
- // player->? (open store?)
-
- // Podium interaction
- } else if (outfitType == 2) {
- Position pos = msg.getPosition();
- uint16_t spriteId = msg.get();
- uint8_t stackpos = msg.getByte();
- newOutfit.lookMount = msg.get();
- newOutfit.lookMountHead = msg.getByte();
- newOutfit.lookMountBody = msg.getByte();
- newOutfit.lookMountLegs = msg.getByte();
- newOutfit.lookMountFeet = msg.getByte();
- Direction direction = static_cast(msg.getByte());
- bool podiumVisible = msg.getByte() == 1;
-
- // apply to podium
- g_dispatcher.addTask(DISPATCHER_TASK_EXPIRATION, [=, playerID = player->getID()]() {
- g_game.playerEditPodium(playerID, newOutfit, pos, stackpos, spriteId, podiumVisible, direction);
- });
- }
-}
-
void ProtocolGame::parseEditPodiumRequest(NetworkMessage& msg)
{
Position pos = msg.getPosition();
@@ -2999,233 +2931,6 @@ void ProtocolGame::sendCombatAnalyzer(CombatType_t type, int32_t amount, DamageA
writeToOutputBuffer(msg);
}
-void ProtocolGame::sendOutfitWindow()
-{
- const auto& outfits = Outfits::getInstance().getOutfits(player->getSex());
- if (outfits.size() == 0) {
- return;
- }
-
- NetworkMessage msg;
- msg.addByte(0xC8);
-
- Outfit_t currentOutfit = player->getDefaultOutfit();
-
- if (currentOutfit.lookType == 0) {
- Outfit_t newOutfit;
- newOutfit.lookType = outfits.front().lookType;
- currentOutfit = newOutfit;
- }
-
- Mount* currentMount = g_game.mounts.getMountByID(player->getCurrentMount());
- if (currentMount) {
- currentOutfit.lookMount = currentMount->clientId;
- }
-
- bool mounted;
- if (player->wasMounted()) {
- mounted = currentOutfit.lookMount != 0;
- } else {
- mounted = player->isMounted();
- }
-
- AddOutfit(msg, currentOutfit);
-
- // mount color bytes are required here regardless of having one
- if (currentOutfit.lookMount == 0) {
- msg.addByte(currentOutfit.lookMountHead);
- msg.addByte(currentOutfit.lookMountBody);
- msg.addByte(currentOutfit.lookMountLegs);
- msg.addByte(currentOutfit.lookMountFeet);
- }
-
- msg.add(0); // current familiar looktype
-
- std::vector protocolOutfits;
- if (player->isAccessPlayer()) {
- protocolOutfits.emplace_back("Gamemaster", 75, 0);
- }
-
- for (const Outfit& outfit : outfits) {
- uint8_t addons;
- if (!player->getOutfitAddons(outfit, addons)) {
- continue;
- }
-
- protocolOutfits.emplace_back(outfit.name, outfit.lookType, addons);
- }
-
- msg.add(protocolOutfits.size());
- for (const ProtocolOutfit& outfit : protocolOutfits) {
- msg.add(outfit.lookType);
- msg.addString(outfit.name);
- msg.addByte(outfit.addons);
- msg.addByte(0x00); // mode: 0x00 - available, 0x01 store (requires U32 store offerId), 0x02 golden outfit
- // tooltip (hardcoded)
- }
-
- std::vector mounts;
- for (const Mount& mount : g_game.mounts.getMounts()) {
- if (player->hasMount(&mount)) {
- mounts.push_back(&mount);
- }
- }
-
- msg.add(mounts.size());
- for (const Mount* mount : mounts) {
- msg.add(mount->clientId);
- msg.addString(mount->name);
- msg.addByte(0x00); // mode: 0x00 - available, 0x01 store (requires U32 store offerId)
- }
-
- msg.add(0x00); // familiars.size()
- // size > 0
- // U16 looktype
- // String name
- // 0x00 // mode: 0x00 - available, 0x01 store (requires U32 store offerId)
-
- msg.addByte(0x00); // Try outfit mode (?)
- msg.addByte(mounted ? 0x01 : 0x00);
- msg.addByte(player->getRandomizeMount() ? 0x01 : 0x00);
- writeToOutputBuffer(msg);
-}
-
-void ProtocolGame::sendPodiumWindow(const std::shared_ptr& item)
-{
- if (!item) {
- return;
- }
-
- const auto& podium = item->asPodium();
- if (!podium) {
- return;
- }
-
- const auto& tile = item->getTile();
- if (!tile) {
- return;
- }
-
- int32_t stackpos = tile->getThingIndex(item);
-
- // read podium outfit
- Outfit_t podiumOutfit = podium->getOutfit();
- Outfit_t playerOutfit = player->getDefaultOutfit();
- bool isEmpty = podiumOutfit.lookType == 0 && podiumOutfit.lookMount == 0;
-
- if (podiumOutfit.lookType == 0) {
- // copy player outfit
- podiumOutfit.lookType = playerOutfit.lookType;
- podiumOutfit.lookHead = playerOutfit.lookHead;
- podiumOutfit.lookBody = playerOutfit.lookBody;
- podiumOutfit.lookLegs = playerOutfit.lookLegs;
- podiumOutfit.lookFeet = playerOutfit.lookFeet;
- podiumOutfit.lookAddons = playerOutfit.lookAddons;
- }
-
- if (podiumOutfit.lookMount == 0) {
- // copy player mount
- podiumOutfit.lookMount = playerOutfit.lookMount;
- podiumOutfit.lookMountHead = playerOutfit.lookMountHead;
- podiumOutfit.lookMountBody = playerOutfit.lookMountBody;
- podiumOutfit.lookMountLegs = playerOutfit.lookMountLegs;
- podiumOutfit.lookMountFeet = playerOutfit.lookMountFeet;
- }
-
- // fetch player outfits
- const auto& outfits = Outfits::getInstance().getOutfits(player->getSex());
- if (outfits.size() == 0) {
- player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE);
- return;
- }
-
- // add GM outfit for staff members
- std::vector protocolOutfits;
- if (player->isAccessPlayer()) {
- protocolOutfits.emplace_back("Gamemaster", 75, 0);
- }
-
- // fetch player addons info
- for (const Outfit& outfit : outfits) {
- uint8_t addons;
- if (!player->getOutfitAddons(outfit, addons)) {
- continue;
- }
-
- protocolOutfits.emplace_back(outfit.name, outfit.lookType, addons);
- }
-
- // select first outfit available when the one from podium is not unlocked
- if (!player->canWear(podiumOutfit.lookType, 0)) {
- podiumOutfit.lookType = outfits.front().lookType;
- }
-
- // fetch player mounts
- std::vector mounts;
- for (const Mount& mount : g_game.mounts.getMounts()) {
- if (player->hasMount(&mount)) {
- mounts.push_back(&mount);
- }
- }
-
- // packet header
- NetworkMessage msg;
- msg.addByte(0xC8);
-
- // current outfit
- msg.add(podiumOutfit.lookType);
- msg.addByte(podiumOutfit.lookHead);
- msg.addByte(podiumOutfit.lookBody);
- msg.addByte(podiumOutfit.lookLegs);
- msg.addByte(podiumOutfit.lookFeet);
- msg.addByte(podiumOutfit.lookAddons);
-
- // current mount
- msg.add(podiumOutfit.lookMount);
- msg.addByte(podiumOutfit.lookMountHead);
- msg.addByte(podiumOutfit.lookMountBody);
- msg.addByte(podiumOutfit.lookMountLegs);
- msg.addByte(podiumOutfit.lookMountFeet);
-
- // current familiar (not used in podium mode)
- msg.add(0);
-
- // available outfits
- msg.add(protocolOutfits.size());
- for (const ProtocolOutfit& outfit : protocolOutfits) {
- msg.add(outfit.lookType);
- msg.addString(outfit.name);
- msg.addByte(outfit.addons);
- msg.addByte(0x00); // mode: 0x00 - available, 0x01 store (requires U32 store offerId), 0x02 golden outfit
- // tooltip (hardcoded)
- }
-
- // available mounts
- msg.add(mounts.size());
- for (const Mount* mount : mounts) {
- msg.add(mount->clientId);
- msg.addString(mount->name);
- msg.addByte(0x00); // mode: 0x00 - available, 0x01 store (requires U32 store offerId)
- }
-
- // available familiars (not used in podium mode)
- msg.add(0);
-
- msg.addByte(0x05); // "set outfit" window mode (5 = podium)
- msg.addByte((isEmpty && playerOutfit.lookMount != 0) || podium->hasFlag(PODIUM_SHOW_MOUNT)
- ? 0x01
- : 0x00); // "mount" checkbox
- msg.add(0); // unknown
- msg.addPosition(item->getPosition());
- msg.add(item->getClientID());
- msg.addByte(stackpos);
-
- msg.addByte(podium->hasFlag(PODIUM_SHOW_PLATFORM) ? 0x01 : 0x00); // is platform visible
- msg.addByte(0x01); // "outfit" checkbox, ignored by the client
- msg.addByte(podium->getDirection()); // outfit direction
- writeToOutputBuffer(msg);
-}
-
void ProtocolGame::sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus)
{
NetworkMessage msg;
diff --git a/src/protocolgame.h b/src/protocolgame.h
index 3dec2e5df4..6b241b8e1b 100644
--- a/src/protocolgame.h
+++ b/src/protocolgame.h
@@ -88,7 +88,6 @@ class ProtocolGame final : public Protocol
// Parse methods
void parseAutoWalk(NetworkMessage& msg);
- void parseSetOutfit(NetworkMessage& msg);
void parseEditPodiumRequest(NetworkMessage& msg);
void parseSay(NetworkMessage& msg);
void parseLookAt(NetworkMessage& msg);
@@ -212,9 +211,6 @@ class ProtocolGame final : public Protocol
void sendHouseWindow(uint32_t windowTextId, const std::string& text);
void sendCombatAnalyzer(CombatType_t type, int32_t amount, DamageAnalyzerImpactType impactType,
const std::string& target);
- void sendOutfitWindow();
-
- void sendPodiumWindow(const std::shared_ptr& item);
void sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus);
void sendVIP(uint32_t guid, const std::string& name, const std::string& description, uint32_t icon, bool notify,
diff --git a/src/script.cpp b/src/script.cpp
index 36ec236d1c..ab93cefac3 100644
--- a/src/script.cpp
+++ b/src/script.cpp
@@ -35,7 +35,7 @@ bool Scripts::loadScripts(std::string_view folderName, bool isLib, bool reload)
size_t found = it->path().filename().string().find(disable);
if (found != std::string::npos) {
if (getBoolean(ConfigManager::SCRIPTS_CONSOLE_LOGS)) {
- std::cout << "> " << it->path().filename().string() << " [disabled]" << std::endl;
+ std::cout << "> " << it->path().lexically_relative(dir).string() << " [disabled]" << std::endl;
}
continue;
}
@@ -57,16 +57,16 @@ bool Scripts::loadScripts(std::string_view folderName, bool isLib, bool reload)
}
if (scriptInterface.loadFile(scriptFile) == -1) {
- std::cout << "> " << it->filename().string() << " [error]" << std::endl;
+ std::cout << "> " << it->lexically_relative(dir).string() << " [error]" << std::endl;
std::cout << "^ " << scriptInterface.getLastLuaError() << std::endl;
continue;
}
if (getBoolean(ConfigManager::SCRIPTS_CONSOLE_LOGS)) {
if (!reload) {
- std::cout << "> " << it->filename().string() << " [loaded]" << std::endl;
+ std::cout << "> " << it->lexically_relative(dir).string() << " [loaded]" << std::endl;
} else {
- std::cout << "> " << it->filename().string() << " [reloaded]" << std::endl;
+ std::cout << "> " << it->lexically_relative(dir).string() << " [reloaded]" << std::endl;
}
}
}
diff --git a/src/signals.cpp b/src/signals.cpp
index fb82fccbb0..4a0c334790 100644
--- a/src/signals.cpp
+++ b/src/signals.cpp
@@ -12,7 +12,6 @@
#include "game.h"
#include "globalevent.h"
#include "monsters.h"
-#include "mounts.h"
#include "movement.h"
#include "scheduler.h"
#include "spells.h"
@@ -80,9 +79,6 @@ void sighupHandler()
g_weapons->loadDefaults();
std::cout << "Reloaded weapons." << std::endl;
- g_game.mounts.reload();
- std::cout << "Reloaded mounts." << std::endl;
-
g_globalEvents->reload();
std::cout << "Reloaded globalevents." << std::endl;
diff --git a/vc18/atlas.vcxproj b/vc18/atlas.vcxproj
index 6218c335f9..2af8c1a057 100644
--- a/vc18/atlas.vcxproj
+++ b/vc18/atlas.vcxproj
@@ -185,7 +185,6 @@
-
@@ -206,7 +205,6 @@
-
@@ -215,7 +213,6 @@
Create
otpch.h
-
@@ -302,13 +299,11 @@
-
-
diff --git a/vc18/atlas.vcxproj.filters b/vc18/atlas.vcxproj.filters
index c1238e4bac..fe96336bd9 100644
--- a/vc18/atlas.vcxproj.filters
+++ b/vc18/atlas.vcxproj.filters
@@ -197,9 +197,6 @@
Source Files
-
- Source Files
-
Source Files
@@ -212,9 +209,6 @@
Source Files
-
- Source Files
-
Source Files
@@ -302,9 +296,6 @@
Source Files\lua\modules
-
- Source Files\lua\modules
-
Source Files\lua\modules
@@ -550,9 +541,6 @@
Header Files
-
- Header Files
-
Header Files
@@ -568,9 +556,6 @@
Header Files
-
- Header Files
-
Header Files