From d4b3cb7dee9b136ceaa199eda6b88ddf69a5cacf Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Jun 2025 02:33:04 -0600 Subject: [PATCH 1/2] Lobby chr_info_sub2 race_change Co-Authored-By: atom0s --- src/login/data_session.cpp | 1 + src/login/login_packets.h | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/login/data_session.cpp b/src/login/data_session.cpp index a9a93ea6c53..02c41f0bade 100644 --- a/src/login/data_session.cpp +++ b/src/login/data_session.cpp @@ -133,6 +133,7 @@ void data_session::read_func() characterInfo.ffxi_id_world = charIdMain; characterInfo.worldid = worldId; characterInfo.status = 1; // 0 = Invalid/Hidden, 1 = Available, 2 = Disabled (unpaid) + characterInfo.race_change = 0; // 0 = no race change service, 1 = race change service (gold star icon) (NOT YET SUPPORTED!) characterInfo.renamef = 0; // 0 = no rename required, 1 = rename required (NOT YET SUPPORTED!) characterInfo.ffxi_id_world_tbl = charIdExtra; diff --git a/src/login/login_packets.h b/src/login/login_packets.h index 412c90496e4..7dcd94df7f3 100644 --- a/src/login/login_packets.h +++ b/src/login/login_packets.h @@ -131,7 +131,9 @@ struct lpkt_chr_info_sub2 uint16_t ffxi_id_world; // PS2: ffxi_id_world uint16_t worldid; // PS2: worldid uint16_t status; // PS2: status - uint8_t renamef; // PS2: renamef + uint8_t renamef : 1; // PS2: renamef + uint8_t race_change : 1; // PS2: (New; did not exist.) + uint8_t unused : 6; // PS2: (New; did not exist.) uint8_t ffxi_id_world_tbl; // PS2: (New; did not exist.) char character_name[16]; // PS2: character_name char world_name[16]; // PS2: world_name From c70ee9bd690ce2dc166672cad157e1658404fae6 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Jun 2025 02:40:48 -0600 Subject: [PATCH 2/2] Race Change --- scripts/commands/racechange.lua | 48 ++++ scripts/globals/ancestry_moogle.lua | 262 ++++++++++++++++++ scripts/specs/core/CBaseEntity.lua | 23 ++ .../Aht_Urhgan_Whitegate/DefaultActions.lua | 1 - .../Aht_Urhgan_Whitegate/npcs/Jarafah.lua | 19 ++ scripts/zones/Port_Bastok/DefaultActions.lua | 1 - scripts/zones/Port_Bastok/IDs.lua | 1 + .../Port_Bastok/npcs/Ancestry_Moogle.lua | 22 ++ scripts/zones/Port_Bastok/npcs/Gallagher.lua | 19 ++ scripts/zones/Port_Jeuno/npcs/Garridan.lua | 19 ++ scripts/zones/Port_San_dOria/IDs.lua | 1 + .../Port_San_dOria/npcs/Ancestry_Moogle.lua | 22 ++ .../Southern_San_dOria/DefaultActions.lua | 1 - .../Southern_San_dOria/npcs/Poudoruchant.lua | 19 ++ scripts/zones/Windurst_Walls/IDs.lua | 1 + .../Windurst_Walls/npcs/Ancestry_Moogle.lua | 22 ++ .../zones/Windurst_Waters/DefaultActions.lua | 1 - .../Windurst_Waters/npcs/Olaky-Yayulaky.lua | 19 ++ sql/npc_list.sql | 9 + src/login/login_helpers.cpp | 24 ++ src/map/entities/charentity.h | 39 +++ src/map/lua/lua_baseentity.cpp | 82 +++++- src/map/lua/lua_baseentity.h | 3 + src/map/utils/charutils.cpp | 69 ++++- src/map/utils/charutils.h | 2 + 25 files changed, 701 insertions(+), 28 deletions(-) create mode 100644 scripts/commands/racechange.lua create mode 100644 scripts/globals/ancestry_moogle.lua create mode 100644 scripts/zones/Aht_Urhgan_Whitegate/npcs/Jarafah.lua create mode 100644 scripts/zones/Port_Bastok/npcs/Ancestry_Moogle.lua create mode 100644 scripts/zones/Port_Bastok/npcs/Gallagher.lua create mode 100644 scripts/zones/Port_Jeuno/npcs/Garridan.lua create mode 100644 scripts/zones/Port_San_dOria/npcs/Ancestry_Moogle.lua create mode 100644 scripts/zones/Southern_San_dOria/npcs/Poudoruchant.lua create mode 100644 scripts/zones/Windurst_Walls/npcs/Ancestry_Moogle.lua create mode 100644 scripts/zones/Windurst_Waters/npcs/Olaky-Yayulaky.lua diff --git a/scripts/commands/racechange.lua b/scripts/commands/racechange.lua new file mode 100644 index 00000000000..cdb7c8f2919 --- /dev/null +++ b/scripts/commands/racechange.lua @@ -0,0 +1,48 @@ +----------------------------------- +-- func: racechange +-- desc: Make player eligible for Ancestry Moogle Race Change service. +----------------------------------- +---@type TCommand +local commandObj = {} + +commandObj.cmdprops = +{ + permission = 3, + parameters = 's' +} + +local function error(player, msg) + player:printToPlayer(msg) + player:printToPlayer('!racechange (player)') +end + +commandObj.onTrigger = function(player, arg1) + local target + + if arg1 ~= nil then + target = GetPlayerByName(arg1) + if target == nil then + error(player, string.format('Player named "%s" not found!', arg1)) + return + end + else + target = player:getCursorTarget() + if target and not target:isPC() then + error(player, 'Target is not a PC') + return + end + + if target == nil then + target = player + end + end + + target:setCharVar('[RaceChange]Eligible', os.time() + 1209600, os.time() + 1209600) + target:setCharVar('[RaceChange]Last', 0) + + player:printToPlayer(string.format( + '%s is now eligible for race change service at Ancestry Moogle. Eligibility will expire in 14 days.', + target:getName())) +end + +return commandObj diff --git a/scripts/globals/ancestry_moogle.lua b/scripts/globals/ancestry_moogle.lua new file mode 100644 index 00000000000..86539d2a049 --- /dev/null +++ b/scripts/globals/ancestry_moogle.lua @@ -0,0 +1,262 @@ +----------------------------------- +-- Ancestry Moogle (Race Change) +-- +-- Player must have CharVar '[RaceChange]Eligible' set to a non-zero value to use the service. +----------------------------------- + +xi = xi or {} +xi.ancestryMoogle = {} + +local settings = +{ + -- Interactions disabled if set to false. + enabled = xi.settings.main.RACE_CHANGE_ENABLED or true, + + -- The time in seconds that the player has to wait before they can use the service again. + -- Default is 14 days (1209600 seconds). + cooldown = xi.settings.main.RACE_CHANGE_COOLDOWN or 1209600, +} + +local csidKey = +{ + NOT_ELIGIBLE = 1, + RACE_CHANGE = 2, +} + +local csidLookup = +{ + [xi.zone.PORT_BASTOK] = { 480, 479 }, + [xi.zone.PORT_SAN_DORIA] = { 833, 832 }, + [xi.zone.WINDURST_WALLS] = { 580, 579 }, +} + +local swappableItems = +{ + [0] = -- Female trading Male gear + { + xi.item.DANCERS_TIARA_M, + xi.item.DANCERS_CASAQUE_M, + xi.item.DANCERS_BANGLES_M, + xi.item.DANCERS_TIGHTS_M, + xi.item.DANCERS_TOE_SHOES_M, + + xi.item.DANCERS_TIARA_M_P1, + xi.item.DANCERS_CASAQUE_M_P1, + xi.item.DANCERS_BANGLES_M_P1, + xi.item.DANCERS_TIGHTS_M_P1, + xi.item.DANCERS_TOE_SHOES_M_P1, + + xi.item.MAXIXI_TIARA_M, + xi.item.MAXIXI_CASAQUE_M, + xi.item.MAXIXI_BANGLES_M, + xi.item.MAXIXI_TIGHTS_M, + xi.item.MAXIXI_TOE_SHOES_M, + + xi.item.MAXIXI_TIARA_M_P1, + xi.item.MAXIXI_CASAQUE_M_P1, + xi.item.MAXIXI_BANGLES_M_P1, + xi.item.MAXIXI_TIGHTS_M_P1, + xi.item.MAXIXI_TOE_SHOES_M_P1, + + xi.item.MAXIXI_TIARA_M_P2, + xi.item.MAXIXI_CASAQUE_M_P2, + xi.item.MAXIXI_BANGLES_M_P2, + xi.item.MAXIXI_TIGHTS_M_P2, + xi.item.MAXIXI_TOE_SHOES_M_P2, + + xi.item.MAXIXI_TIARA_M_P3, + xi.item.MAXIXI_CASAQUE_M_P3, + xi.item.MAXIXI_BANGLES_M_P3, + xi.item.MAXIXI_TIGHTS_M_P3, + xi.item.MAXIXI_TOE_SHOES_M_P3, + }, + + [1] = -- Male trading Female gear + { + xi.item.DANCERS_TIARA_F, + xi.item.DANCERS_CASAQUE_F, + xi.item.DANCERS_BANGLES_F, + xi.item.DANCERS_TIGHTS_F, + xi.item.DANCERS_TOE_SHOES_F, + + xi.item.DANCERS_TIARA_F_P1, + xi.item.DANCERS_CASAQUE_F_P1, + xi.item.DANCERS_BANGLES_F_P1, + xi.item.DANCERS_TIGHTS_F_P1, + xi.item.DANCERS_TOE_SHOES_F_P1, + + xi.item.MAXIXI_TIARA_F, + xi.item.MAXIXI_CASAQUE_F, + xi.item.MAXIXI_BANGLES_F, + xi.item.MAXIXI_TIGHTS_F, + xi.item.MAXIXI_TOE_SHOES_F, + + xi.item.MAXIXI_TIARA_F_P1, + xi.item.MAXIXI_CASAQUE_F_P1, + xi.item.MAXIXI_BANGLES_F_P1, + xi.item.MAXIXI_TIGHTS_F_P1, + xi.item.MAXIXI_TOE_SHOES_F_P1, + + xi.item.MAXIXI_TIARA_F_P2, + xi.item.MAXIXI_CASAQUE_F_P2, + xi.item.MAXIXI_BANGLES_F_P2, + xi.item.MAXIXI_TIGHTS_F_P2, + xi.item.MAXIXI_TOE_SHOES_F_P2, + + xi.item.MAXIXI_TIARA_F_P3, + xi.item.MAXIXI_CASAQUE_F_P3, + xi.item.MAXIXI_BANGLES_F_P3, + xi.item.MAXIXI_TIGHTS_F_P3, + xi.item.MAXIXI_TOE_SHOES_F_P3, + }, +} + +xi.ancestryMoogle.onTrade = function(player, npc, trade) + if not settings.enabled then + return + end + + local confirmedItems = {} + + for slotId = 0, 7 do + local item = trade:getItem(slotId) + if item then + local itemId = item:getID() + if utils.contains(itemId, swappableItems[player:getGender()]) then + print('Item eligible for swap: ' .. itemId) + -- Player submitted an eligible item from the opposite gender. + table.insert(confirmedItems, itemId) + end + end + end + + for _, v in ipairs(confirmedItems) do + -- Give new item. Item ID + 1 for MtoF, -1 for FtoM. + -- getGender returns 0 for F and 1 for M. + if npcUtil.giveItem(player, player:getGender() == 1 and v - 1 or v + 1) then + trade:confirmItem(v, 1) + end + end + + player:confirmTrade() + return true +end + +xi.ancestryMoogle.onTrigger = function(player, npc) + if not settings.enabled then + return + end + + local zoneID = player:getZoneID() + local raceChangeLast = player:getCharVar('[RaceChange]Last') + local raceChangeExpiry = player:getCharVar('[RaceChange]Eligible') + + -- If the player is on cooldown, show a message and exit. + if raceChangeLast + settings.cooldown >= os.time() then + player:startEvent(csidLookup[zoneID][csidKey.NOT_ELIGIBLE], + 236, + 0, -- unknown, seen 0 + 0, -- unknown, seen 0 + 0, -- unknown, seen 0 + 0, -- unknown, time remaining for an unknown variant of the event + 5, -- 0 is message when service not purchased, 1+ when on cooldown. + 1, -- 1+ displays "Thank you for using the service!". Capture shows misc values. + 0 -- unknown, seen random values. + ) + return + end + + -- Player must have "purchased" the service and have enough time left to use it. + if + raceChangeExpiry == 0 or -- Expired charvar + raceChangeExpiry - os.time() <= 0 -- If the charvar is set but expired. + then + player:startEvent(csidLookup[zoneID][csidKey.NOT_ELIGIBLE], 236) + player:setCharVar('[RaceChange]Eligible', 0) + return + end + + -- The upcoming event need the Previous_Race NPC set to the player current race/face. + -- The Previous_Race NPC is 2 IDs higher than the Ancestry Moogle NPC. + local previousRaceNpc = GetNPCByID(npc:getID() + 2) + if previousRaceNpc then + previousRaceNpc:setLook({ race = player:getRace(), face = player:getFace() }) + player:sendEntityUpdateToPlayer(previousRaceNpc, xi.entityUpdate.ENTITY_UPDATE, xi.updateType.UPDATE_ALL_CHAR) + end + + player:startEvent(csidLookup[zoneID][csidKey.RACE_CHANGE], + player:getRace(), -- current race + bit.rshift(player:getFace(), 1), -- current face, flattened from 0-15 to 0-7 + player:getSize(), -- current size + player:getFace() % 2, -- current hair color (face variant) + raceChangeExpiry - os.time(), -- Time left to use the service, in seconds. + 0, -- unknown, seen 0 + 0, -- unknown, seen random values + 0 -- unknown, seen random values + ) +end + +xi.ancestryMoogle.onEventFinish = function(player, csid, option, npc) + if not settings.enabled then + return + end + + local zoneID = player:getZoneID() + local raceChangeExpiry = player:getCharVar('[RaceChange]Eligible') + + if + csid == csidLookup[zoneID][csidKey.RACE_CHANGE] and + option ~= utils.EVENT_CANCELLED_OPTION + then + -- If timer expired between the event start and finish, reset + if + raceChangeExpiry == 0 or -- Expired charvar + raceChangeExpiry - os.time() <= 0 -- If the charvar is set but expired. + then + player:messageSpecial(zones[zoneID].text.UNABLE_RACE_CHANGE) + -- Must rezone character to exit the special event. + player:forceRezone() + return + end + + -- Bits 8-11: Race ID (4 bits, values 1-8) + -- Bits 12-15: Face/Hair combination (4 bits, values 0-15) + -- Bits 16-17: Size (2 bits, values 0(small)-2(large)) + local newRace = bit.band(bit.rshift(option, 8), 0xF) + local newFace = bit.band(bit.rshift(option, 12), 0xF) + local newSize = bit.band(bit.rshift(option, 16), 0x3) + + -- Sanity checks + if + (not newRace or not newFace or not newSize) or + (newRace < xi.race.HUME_M or newRace > xi.race.GALKA) or + (newFace > 15) or -- 1A (0) to 8B (15) + (newSize > 2) -- 0 (small) to 2 (large) + then + printf('bad race change data: player=%s, newRace=%d, newFace=%d, newSize=%d', + player:getName(), newRace, newFace, newSize) + player:messageSpecial(zones[zoneID].text.UNABLE_RACE_CHANGE) + -- Must rezone character to exit the special event. + player:forceRezone() + return + end + + printf('race change: player=%s, race %d -> %d, face %d -> %d, size %d -> %d', + player:getName(), + player:getRace(), newRace, + player:getFace(), newFace, + player:getSize(), newSize) + + -- Set the timer to 0 so the player can't use the service again + -- This is done before the actual race change since it will teleport the player + -- and player may no longer be valid + player:setCharVar('[RaceChange]Eligible', 0) + player:setCharVar('[RaceChange]Last', os.time()) + + if not player:raceChange(newRace, newFace, newSize) then + player:messageSpecial(zones[zoneID].text.UNABLE_RACE_CHANGE) + -- Must rezone character to exit the special event. + player:forceRezone() + end + end +end diff --git a/scripts/specs/core/CBaseEntity.lua b/scripts/specs/core/CBaseEntity.lua index 55d418b366e..9408d1d6427 100644 --- a/scripts/specs/core/CBaseEntity.lua +++ b/scripts/specs/core/CBaseEntity.lua @@ -1175,11 +1175,29 @@ end function CBaseEntity:getRace() end +---@nodiscard +---@return integer +function CBaseEntity:getFace() +end + ---@nodiscard ---@return integer function CBaseEntity:getGender() end +---@nodiscard +---@return integer +function CBaseEntity:getSize() +end + +---@nodiscard +---@param newRace integer +---@param newFace integer +---@param newSize integer +---@return boolean +function CBaseEntity:raceChange(newRace, newFace, newSize) +end + ---@nodiscard ---@return string function CBaseEntity:getName() @@ -1212,6 +1230,11 @@ end function CBaseEntity:setModelId(modelId, slotObj) end +---@param look table +---@return nil +function CBaseEntity:setLook(look) +end + ---@nodiscard ---@return integer function CBaseEntity:getCostume() diff --git a/scripts/zones/Aht_Urhgan_Whitegate/DefaultActions.lua b/scripts/zones/Aht_Urhgan_Whitegate/DefaultActions.lua index 95709eb535d..b355a42717b 100644 --- a/scripts/zones/Aht_Urhgan_Whitegate/DefaultActions.lua +++ b/scripts/zones/Aht_Urhgan_Whitegate/DefaultActions.lua @@ -23,7 +23,6 @@ return { ['Hashayra'] = { event = 676 }, ['Hishahma'] = { event = 571 }, ['Imperial_Whitegate'] = { messageSpecial = ID.text.GATE_IS_FIRMLY_CLOSED }, - ['Jarafah'] = { event = 702 }, ['Jumaaf'] = { event = 243 }, ['Kabihyam'] = { event = 245 }, ['Kalimahf'] = { event = 680 }, diff --git a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Jarafah.lua b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Jarafah.lua new file mode 100644 index 00000000000..5f64dd569bc --- /dev/null +++ b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Jarafah.lua @@ -0,0 +1,19 @@ +----------------------------------- +-- Area: Aht Urhgan Whitegate +-- NPC: Jarafah +-- Item Depository NPC (not implemented) +-- !pos 14.88 0 -15 50 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + player:startEvent(702) +end + +entity.onEventFinish = function(player, csid, option, npc) + -- TODO: Implement + -- Must account for race change item swaps. See http://www.playonline.com/ff11eu/envi/racechange/ +end + +return entity diff --git a/scripts/zones/Port_Bastok/DefaultActions.lua b/scripts/zones/Port_Bastok/DefaultActions.lua index dcf6ad03ac9..2681cce4403 100644 --- a/scripts/zones/Port_Bastok/DefaultActions.lua +++ b/scripts/zones/Port_Bastok/DefaultActions.lua @@ -16,7 +16,6 @@ return { ['Ensetsu'] = { event = 27 }, ['Evi'] = { event = 21 }, ['Ferrol'] = { event = 254 }, - ['Gallagher'] = { event = 349 }, ['Grin'] = { event = 295 }, ['Gudav'] = { event = 31 }, ['Gwinar'] = { event = 365 }, diff --git a/scripts/zones/Port_Bastok/IDs.lua b/scripts/zones/Port_Bastok/IDs.lua index 4ae140e2cbb..306579f807a 100644 --- a/scripts/zones/Port_Bastok/IDs.lua +++ b/scripts/zones/Port_Bastok/IDs.lua @@ -77,6 +77,7 @@ zones[xi.zone.PORT_BASTOK] = OBTAINED_GUILD_POINTS = 12699, -- Obtained: guild points. OBTAINED_NUM_KEYITEMS = 13092, -- Obtained key item: ! NOT_ACQUAINTED = 13094, -- I'm sorry, but I don't believe we're acquainted. Please leave me be. + UNABLE_RACE_CHANGE = 14186, -- You were unable to use the specified appearance for your character. }, mob = { diff --git a/scripts/zones/Port_Bastok/npcs/Ancestry_Moogle.lua b/scripts/zones/Port_Bastok/npcs/Ancestry_Moogle.lua new file mode 100644 index 00000000000..c92d5a1118e --- /dev/null +++ b/scripts/zones/Port_Bastok/npcs/Ancestry_Moogle.lua @@ -0,0 +1,22 @@ +----------------------------------- +-- Area: Port Bastok +-- NPC: Ancestry Moogle +-- Type: Race Change NPC +-- !pos 116.080 7.372 -31.820 236 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrade = function(player, npc, trade) + return xi.ancestryMoogle.onTrade(player, npc, trade) +end + +entity.onTrigger = function(player, npc) + return xi.ancestryMoogle.onTrigger(player, npc) +end + +entity.onEventFinish = function(player, csid, option, npc) + return xi.ancestryMoogle.onEventFinish(player, csid, option, npc) +end + +return entity diff --git a/scripts/zones/Port_Bastok/npcs/Gallagher.lua b/scripts/zones/Port_Bastok/npcs/Gallagher.lua new file mode 100644 index 00000000000..8372edd07c9 --- /dev/null +++ b/scripts/zones/Port_Bastok/npcs/Gallagher.lua @@ -0,0 +1,19 @@ +----------------------------------- +-- Area: Port Bastok +-- NPC: Gallagher +-- Item Depository NPC (not implemented) +-- !pos -33 7.5 -179 236 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + player:startEvent(349) +end + +entity.onEventFinish = function(player, csid, option, npc) + -- TODO: Implement + -- Must account for race change item swaps. See http://www.playonline.com/ff11eu/envi/racechange/ +end + +return entity diff --git a/scripts/zones/Port_Jeuno/npcs/Garridan.lua b/scripts/zones/Port_Jeuno/npcs/Garridan.lua new file mode 100644 index 00000000000..0e27f58e091 --- /dev/null +++ b/scripts/zones/Port_Jeuno/npcs/Garridan.lua @@ -0,0 +1,19 @@ +----------------------------------- +-- Area: Port Jeuno +-- NPC: Garridan +-- Item Depository NPC (not implemented) +-- !pos 19.59 0 -9.9 246 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + player:startEvent(308) +end + +entity.onEventFinish = function(player, csid, option, npc) + -- TODO: Implement + -- Must account for race change item swaps. See http://www.playonline.com/ff11eu/envi/racechange/ +end + +return entity diff --git a/scripts/zones/Port_San_dOria/IDs.lua b/scripts/zones/Port_San_dOria/IDs.lua index 885a9af6a36..8f05539a0e8 100644 --- a/scripts/zones/Port_San_dOria/IDs.lua +++ b/scripts/zones/Port_San_dOria/IDs.lua @@ -84,6 +84,7 @@ zones[xi.zone.PORT_SAN_DORIA] = OBTAINED_NUM_KEYITEMS = 11562, -- Obtained key item: ! NOT_ACQUAINTED = 11564, -- I'm sorry, but I don't believe we're acquainted. Please leave me be. MAP_MARKER_TUTORIAL = 11912, -- Selecting Map from the main menu opens the map of the area in which you currently reside. Select Markers and press the right arrow key to see all the markers placed on your map. + UNABLE_RACE_CHANGE = 12234, -- You were unable to use the specified appearance for your character. }, mob = { diff --git a/scripts/zones/Port_San_dOria/npcs/Ancestry_Moogle.lua b/scripts/zones/Port_San_dOria/npcs/Ancestry_Moogle.lua new file mode 100644 index 00000000000..2666b3a632c --- /dev/null +++ b/scripts/zones/Port_San_dOria/npcs/Ancestry_Moogle.lua @@ -0,0 +1,22 @@ +----------------------------------- +-- Area: Port San d'Oria +-- NPC: Ancestry Moogle +-- Type: Race Change NPC +-- !pos 73.7 -124 -16 232 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrade = function(player, npc, trade) + return xi.ancestryMoogle.onTrade(player, npc, trade) +end + +entity.onTrigger = function(player, npc) + return xi.ancestryMoogle.onTrigger(player, npc) +end + +entity.onEventFinish = function(player, csid, option, npc) + return xi.ancestryMoogle.onEventFinish(player, csid, option, npc) +end + +return entity diff --git a/scripts/zones/Southern_San_dOria/DefaultActions.lua b/scripts/zones/Southern_San_dOria/DefaultActions.lua index b526fee2f40..88d7eb01dd9 100644 --- a/scripts/zones/Southern_San_dOria/DefaultActions.lua +++ b/scripts/zones/Southern_San_dOria/DefaultActions.lua @@ -38,7 +38,6 @@ return { ['Ophelia'] = { event = 751 }, ['Paouala'] = { event = 82 }, ['Phillone'] = { event = 29 }, - ['Poudoruchant'] = { event = 779 }, ['qm2'] = { messageSpecial = ID.text.NOTHING_OUT_OF_ORDINARY }, ['qm4'] = { messageSpecial = ID.text.NOTHING_OUT_OF_ORDINARY }, ['Rosel'] = { text = ID.text.ROSEL_GREETINGS }, diff --git a/scripts/zones/Southern_San_dOria/npcs/Poudoruchant.lua b/scripts/zones/Southern_San_dOria/npcs/Poudoruchant.lua new file mode 100644 index 00000000000..4c9e41be6d1 --- /dev/null +++ b/scripts/zones/Southern_San_dOria/npcs/Poudoruchant.lua @@ -0,0 +1,19 @@ +----------------------------------- +-- Area: Southern San d'Oria +-- NPC: Poudoruchant +-- Item Depository NPC (not implemented) +-- !pos -139.56 -2 21.31 230 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + player:startEvent(779) +end + +entity.onEventFinish = function(player, csid, option, npc) + -- TODO: Implement + -- Must account for race change item swaps. See http://www.playonline.com/ff11eu/envi/racechange/ +end + +return entity diff --git a/scripts/zones/Windurst_Walls/IDs.lua b/scripts/zones/Windurst_Walls/IDs.lua index 9d9decfb6fa..17472b07820 100644 --- a/scripts/zones/Windurst_Walls/IDs.lua +++ b/scripts/zones/Windurst_Walls/IDs.lua @@ -41,6 +41,7 @@ zones[xi.zone.WINDURST_WALLS] = EARNED_ALLIED_NOTES = 9657, -- You have earned Allied Note[/s]! OBTAINED_GUILD_POINTS = 9658, -- Obtained: guild points. TEAR_IN_FABRIC_OF_SPACE = 10870, -- There appears to be a tear in the fabric of space... + UNABLE_RACE_CHANGE = 11470, -- You were unable to use the specified appearance for your character. }, mob = { diff --git a/scripts/zones/Windurst_Walls/npcs/Ancestry_Moogle.lua b/scripts/zones/Windurst_Walls/npcs/Ancestry_Moogle.lua new file mode 100644 index 00000000000..6a4f1dd5712 --- /dev/null +++ b/scripts/zones/Windurst_Walls/npcs/Ancestry_Moogle.lua @@ -0,0 +1,22 @@ +----------------------------------- +-- Area: Windurst Walls +-- NPC: Ancestry Moogle +-- Type: Race Change NPC +-- !pos -220 1 -108 239 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrade = function(player, npc, trade) + return xi.ancestryMoogle.onTrade(player, npc, trade) +end + +entity.onTrigger = function(player, npc) + return xi.ancestryMoogle.onTrigger(player, npc) +end + +entity.onEventFinish = function(player, csid, option, npc) + return xi.ancestryMoogle.onEventFinish(player, csid, option, npc) +end + +return entity diff --git a/scripts/zones/Windurst_Waters/DefaultActions.lua b/scripts/zones/Windurst_Waters/DefaultActions.lua index d37ea875fef..32c3511fc76 100644 --- a/scripts/zones/Windurst_Waters/DefaultActions.lua +++ b/scripts/zones/Windurst_Waters/DefaultActions.lua @@ -58,7 +58,6 @@ return { ['Nine_of_Hearts'] = { event = 277 }, ['Ohbiru-Dohbiru'] = { event = 344 }, ['Okaka'] = { event = 574 }, - ['Olaky-Yayulaky'] = { event = 910 }, ['Orn'] = { event = 652 }, ['Pakesse-Myukesse'] = { event = 434 }, ['Paku-Nakku'] = { event = 431 }, diff --git a/scripts/zones/Windurst_Waters/npcs/Olaky-Yayulaky.lua b/scripts/zones/Windurst_Waters/npcs/Olaky-Yayulaky.lua new file mode 100644 index 00000000000..457379741d5 --- /dev/null +++ b/scripts/zones/Windurst_Waters/npcs/Olaky-Yayulaky.lua @@ -0,0 +1,19 @@ +----------------------------------- +-- Area: Windurst Waters +-- NPC: Olaky-Yayulaky +-- Item Depository NPC (not implemented) +-- !pos -60 -3.5 71 238 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + player:startEvent(910) +end + +entity.onEventFinish = function(player, csid, option, npc) + -- TODO: Implement + -- Must account for race change item swaps. See http://www.playonline.com/ff11eu/envi/racechange/ +end + +return entity diff --git a/sql/npc_list.sql b/sql/npc_list.sql index 30e305b6e4d..45b5757abff 100644 --- a/sql/npc_list.sql +++ b/sql/npc_list.sql @@ -27266,6 +27266,9 @@ INSERT INTO `npc_list` VALUES (17727682,'blank','',0,0.000,0.000,0.000,0,50,50,0 INSERT INTO `npc_list` VALUES (17727683,'blank','',0,0.000,0.000,0.000,0,50,50,0,0,0,6,3,0x0000340000000000000000000000000000000000,0,NULL,0); INSERT INTO `npc_list` VALUES (17727684,'Curio_Vendor_Moogle','Curio Vendor Moogle',160,52.000,-12.000,-114.000,14,50,50,0,0,0,0,3,0x0000520000000000000000000000000000000000,0,'SOA',1); INSERT INTO `npc_list` VALUES (17727685,'AMAN_Liaison','A.M.A.N. Liaison',232,66.770,-16.000,-132.000,21,50,50,0,0,1,0,3,0x0100070477106720683066406850006000700000,0,'SOA',0); +INSERT INTO `npc_list` VALUES (17727688,'Ancestry_Moogle','Ancestry Moogle',96,75,-16,-122,1,50,50,0,0,0,0,27,0x0000520000000000000000000000000000000000,32,'SOA',1); +INSERT INTO `npc_list` VALUES (17727689,'NPC','',0,150,0,-150,1,50,50,0,0,0,6,25,0x0001010010002000300040005000600070000000,32,'SOA',0); +INSERT INTO `npc_list` VALUES (17727690,'Previous_Race','Previous Race',0,150,0,-150,1,50,50,0,0,0,2,25,0x0001070010002000300040005000600070000000,32,'SOA',0); -- ------------------------------------------------------------ -- Chateau d'Oraguille (Zone 233) @@ -28577,6 +28580,9 @@ INSERT INTO `npc_list` VALUES (17744224,'Ruenda','Ruenda',0,-150.000,-6.000,-1.5 INSERT INTO `npc_list` VALUES (17744236,'AMAN_Liaison','A.M.A.N. Liaison',64,68.640,8.500,-221.240,21,50,50,0,0,1,0,3,0x01000C0277106720683066406850006000700000,0,'SOA',0); INSERT INTO `npc_list` VALUES (17744237,'blank',' ',116,-175.167,-6.000,-17.574,1,50,50,0,0,1,0,2051,0x0000AE0900000000000000000000000000000000,130,'SOA',0); INSERT INTO `npc_list` VALUES (17744238,'Tales_Beginning','Tales\' Beginning',116,-175.167,-6.000,-17.574,1,50,50,0,17,1,0,3,0x0000810900000000000000000000000000000000,130,'SOA',0); +INSERT INTO `npc_list` VALUES (17744239,'Ancestry_Moogle','Ancestry Moogle',64,52,8.5,-221,1,50,50,0,0,0,0,27,0x0000520000000000000000000000000000000000,32,'SOA',1); +INSERT INTO `npc_list` VALUES (17744240,'NPC','',0,150,0,-150,1,50,50,0,0,0,6,25,0x0001010010002000300040005000600070000000,32,'SOA',0); +INSERT INTO `npc_list` VALUES (17744241,'Previous_Race','Previous Race',0,150,0,-150,1,50,50,0,0,0,2,25,0x0001070010002000300040005000600070000000,32,'SOA',0); -- ------------------------------------------------------------ -- Metalworks (Zone 237) @@ -29678,6 +29684,9 @@ INSERT INTO `npc_list` VALUES (17756509,'Linkshell_Concierge','Linkshell Concier INSERT INTO `npc_list` VALUES (17756510,'Chat_Manual','Chat Manual',249,-220.890,0.380,-140.900,1,50,50,0,0,96,0,3,0x0000F20800000000000000000000000000000000,0,NULL,1); INSERT INTO `npc_list` VALUES (17756511,'blank','',192,-182.561,-2.456,143.515,1,50,50,0,0,112,0,2051,0x0000AE0900000000000000000000000000000000,130,NULL,1); INSERT INTO `npc_list` VALUES (17756512,'Tales_Beginning','Tales\' Beginning',192,-182.561,-2.456,143.515,1,50,50,0,17,112,0,3,0x0000810900000000000000000000000000000000,130,NULL,1); +INSERT INTO `npc_list` VALUES (17756513,'Ancestry_Moogle','Ancestry Moogle',0,-220,1,-108,1,50,50,0,0,0,0,27,0x0000520000000000000000000000000000000000,32,'SOA',1); +INSERT INTO `npc_list` VALUES (17756514,'NPC','',0,150,0,-150,1,50,50,0,0,0,6,25,0x0001010010002000300040005000600070000000,32,'SOA',0); +INSERT INTO `npc_list` VALUES (17756515,'Previous_Race','Previous Race',0,150,0,-150,1,50,50,0,0,0,2,25,0x0001070010002000300040005000600070000000,32,'SOA',0); -- ------------------------------------------------------------ -- Port Windurst (Zone 240) diff --git a/src/login/login_helpers.cpp b/src/login/login_helpers.cpp index d352d87da40..a759d6782f0 100644 --- a/src/login/login_helpers.cpp +++ b/src/login/login_helpers.cpp @@ -219,6 +219,24 @@ namespace loginHelpers createchar.m_look.size = ref(buf, 57); createchar.m_look.face = ref(buf, 60); + if (createchar.m_look.race < 1 || createchar.m_look.race > 8) // 1(HumeM) to 8(Galka) + { + ShowError(fmt::format("{} attempted to create character with invalid race {}", charName, createchar.m_look.race)); + return -1; + } + + if (createchar.m_look.size > 2) // Large + { + ShowError(fmt::format("{} attempted to create character with invalid size {}", charName, createchar.m_look.size)); + return -1; + } + + if (createchar.m_look.face > 15) // Face 8B + { + ShowError(fmt::format("{} attempted to create character with invalid face {}", charName, createchar.m_look.face)); + return -1; + } + // Validate that the job is a starting job. uint8 mjob = ref(buf, 50); createchar.m_mjob = std::clamp(mjob, 1, 6); @@ -232,6 +250,12 @@ namespace loginHelpers createchar.m_nation = ref(buf, 54); + if (createchar.m_nation > 2) // 0x00 = San d'Oria, 0x01 = Bastok, 0x02 = Windurst + { + ShowError(fmt::format("{} attempted to create character with invalid nation {}", charName, createchar.m_nation)); + return -1; + } + std::vector bastokStartingZones = { 0xEA, 0xEB, 0xEC }; std::vector sandoriaStartingZones = { 0xE6, 0xE7, 0xE8 }; std::vector windurstStartingZones = { 0xEE, 0xF0, 0xF1 }; diff --git a/src/map/entities/charentity.h b/src/map/entities/charentity.h index 0f4487f6ef3..7fbc29f3279 100644 --- a/src/map/entities/charentity.h +++ b/src/map/entities/charentity.h @@ -214,6 +214,45 @@ enum CHAR_PERSIST : uint8 EFFECTS = 0x04, }; +enum class CharRace : uint8 +{ + HumeMale = 1, + HumeFemale = 2, + ElvaanMale = 3, + ElvaanFemale = 4, + TarutaruMale = 5, + TarutaruFemale = 6, + Mithra = 7, + Galka = 8, +}; + +enum class CharSize : uint8 +{ + Small = 0, + Medium = 1, + Large = 2, +}; + +enum class CharFace : uint8 +{ + Face1A = 0, + Face1B = 1, + Face2A = 2, + Face2B = 3, + Face3A = 4, + Face3B = 5, + Face4A = 6, + Face4B = 7, + Face5A = 8, + Face5B = 9, + Face6A = 10, + Face6B = 11, + Face7A = 12, + Face7B = 13, + Face8A = 14, + Face8B = 15, +}; + class CBasicPacket; class CLinkshell; class CUnityChat; diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 30c49b879c7..8e4e94ad216 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -5396,13 +5396,13 @@ void CLuaBaseEntity::retrieveItemFromSlip(uint16 slipId, uint16 itemId, uint16 e uint8 CLuaBaseEntity::getRace() { - if (m_PBaseEntity->objtype != TYPE_PC) + if (const auto* PChar = dynamic_cast(m_PBaseEntity)) { - ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); - return 0; + return PChar->look.race; } - return static_cast(m_PBaseEntity)->look.race; + ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); + return 0; } /************************************************************************ @@ -5414,13 +5414,13 @@ uint8 CLuaBaseEntity::getRace() uint8 CLuaBaseEntity::getFace() { - if (m_PBaseEntity->objtype != TYPE_PC) + if (const auto* PChar = dynamic_cast(m_PBaseEntity)) { - ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); - return 0; + return PChar->look.face; } - return static_cast(m_PBaseEntity)->look.face; + ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); + return 0; } /************************************************************************ @@ -5432,15 +5432,49 @@ uint8 CLuaBaseEntity::getFace() uint8 CLuaBaseEntity::getGender() { - if (m_PBaseEntity->objtype != TYPE_PC) + if (auto* PChar = dynamic_cast(m_PBaseEntity)) { - ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); - return 0; + return PChar->GetGender(); } - auto* PChar = static_cast(m_PBaseEntity); + ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); + return 0; +} + +/************************************************************************ + * Function: getSize() + * Purpose : Returns the integer value of the size of the character + * Small: 0, Medium: 1, Large: 2 + * Example : player:getSize() + ************************************************************************/ + +uint8 CLuaBaseEntity::getSize() +{ + if (const auto* PChar = dynamic_cast(m_PBaseEntity)) + { + return PChar->look.size; + } - return PChar->GetGender(); + ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); + return 0; +} + +/************************************************************************ + * Function: raceChange() + * Purpose : Updates a character race, face and size. + * Example : player:raceChange(xi.race.HUME_F, 1, 0) + * Note : Will force-zone the character after the change. + ************************************************************************/ + +bool CLuaBaseEntity::raceChange(const CharRace newRace, const CharFace newFace, const CharSize newSize) +{ + if (auto* PChar = dynamic_cast(m_PBaseEntity)) + { + return charutils::raceChange(PChar, newRace, newFace, newSize); + } + + ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); + return false; } /************************************************************************ @@ -5574,6 +5608,25 @@ void CLuaBaseEntity::setModelId(uint16 modelId, sol::object const& slotObj) m_PBaseEntity->updatemask |= UPDATE_LOOK; } +/************************************************************************ + * Function: setLook() + * Purpose : Updates the look of an equipped NPC + * Example : npc:setLook({ race = xi.race.HUME_M, face = 1 }) + * Note : Only for equipped NPCs that dynamically change their race/face + ************************************************************************/ +void CLuaBaseEntity::setLook(sol::table const& look) +{ + if (auto* PNpc = dynamic_cast(m_PBaseEntity)) + { + PNpc->look.size = MODEL_EQUIPPED; + PNpc->look.face = look.get_or("face", 0); + PNpc->look.race = look.get_or("race", 0); + return; + } + + ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); +} + /************************************************************************ * Function: getCostume() * Purpose : Returns the PC's appearance @@ -19206,12 +19259,15 @@ void CLuaBaseEntity::Register() SOL_REGISTER("getRace", CLuaBaseEntity::getRace); SOL_REGISTER("getFace", CLuaBaseEntity::getFace); SOL_REGISTER("getGender", CLuaBaseEntity::getGender); + SOL_REGISTER("getSize", CLuaBaseEntity::getSize); + SOL_REGISTER("raceChange", CLuaBaseEntity::raceChange); SOL_REGISTER("getName", CLuaBaseEntity::getName); SOL_REGISTER("getPacketName", CLuaBaseEntity::getPacketName); SOL_REGISTER("renameEntity", CLuaBaseEntity::renameEntity); SOL_REGISTER("hideName", CLuaBaseEntity::hideName); SOL_REGISTER("getModelId", CLuaBaseEntity::getModelId); SOL_REGISTER("setModelId", CLuaBaseEntity::setModelId); + SOL_REGISTER("setLook", CLuaBaseEntity::setLook); SOL_REGISTER("getCostume", CLuaBaseEntity::getCostume); SOL_REGISTER("setCostume", CLuaBaseEntity::setCostume); SOL_REGISTER("getCostume2", CLuaBaseEntity::getCostume2); diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index 5ced6bfe6cb..6885fb5162b 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -287,12 +287,15 @@ class CLuaBaseEntity uint8 getRace(); uint8 getFace(); uint8 getGender(); + uint8 getSize(); + bool raceChange(CharRace newRace, CharFace newFace, CharSize newSize); auto getName() -> std::string; auto getPacketName() -> std::string; void renameEntity(std::string const& newName, sol::object const& arg2); void hideName(bool isHidden); uint16 getModelId(); void setModelId(uint16 modelId, sol::object const& slotObj); + void setLook(sol::table const& look); uint16 getCostume(); void setCostume(uint16 costume); uint16 getCostume2(); diff --git a/src/map/utils/charutils.cpp b/src/map/utils/charutils.cpp index da05314c7e9..7136f317898 100644 --- a/src/map/utils/charutils.cpp +++ b/src/map/utils/charutils.cpp @@ -168,22 +168,29 @@ namespace charutils uint8 race = 0; // Hume - switch (PChar->look.race) + switch (static_cast(PChar->look.race)) { - case 3: - case 4: + case CharRace::HumeMale: + case CharRace::HumeFemale: + race = 0; + break; + case CharRace::ElvaanMale: + case CharRace::ElvaanFemale: race = 1; - break; // Elvaan - case 5: - case 6: + break; + case CharRace::TarutaruMale: + case CharRace::TarutaruFemale: race = 2; - break; // Tarutaru - case 7: + break; + case CharRace::Mithra: race = 3; - break; // Mithra - case 8: + break; + case CharRace::Galka: race = 4; - break; // Galka + break; + default: + race = 0; + break; } // HP Calculation from Main Job @@ -7496,4 +7503,44 @@ namespace charutils } } } + + bool raceChange(CCharEntity* PChar, CharRace newRace, CharFace newFace, CharSize newSize) + { + if (!PChar) + { + return false; + } + + if (newRace < CharRace::HumeMale || + newRace > CharRace::Galka || + newFace > CharFace::Face8B || + newSize > CharSize::Large) + { + ShowError("charutils::raceChange: Arguments out of bounds for charid: %u", PChar->id); + return false; + } + + if (!db::preparedStmt("UPDATE char_look SET " + "face = ?, race = ?, size = ? " + "WHERE charid = ?", + newFace, newRace, newSize, PChar->id)) + { + ShowError("charutils::raceChange: Failed to update char_look for charid: %u", PChar->id); + return false; + } + + for (uint8 slotId = SLOT_MAIN; slotId <= SLOT_BACK; ++slotId) + { + if (auto* PItem = PChar->getEquip(static_cast(slotId))) + { + if (!PItem->isEquippableByRace(static_cast(newRace))) + { + charutils::UnequipItem(PChar, slotId); + } + } + } + + ForceRezone(PChar); + return true; + } }; // namespace charutils diff --git a/src/map/utils/charutils.h b/src/map/utils/charutils.h index 152ff0e0d48..7f7ef12dc7d 100644 --- a/src/map/utils/charutils.h +++ b/src/map/utils/charutils.h @@ -293,6 +293,8 @@ namespace charutils bool isOrchestrionPlaced(CCharEntity* PChar); void updateMannequins(CCharEntity* PChar); + + bool raceChange(CCharEntity* PChar, CharRace newRace, CharFace newFace, CharSize newSize); }; // namespace charutils #endif // _CHARUTILS_H