From cc544e832d3919e38a992cb554781ea19d14d38a Mon Sep 17 00:00:00 2001 From: sruon Date: Thu, 9 Jul 2026 17:02:53 -0600 Subject: [PATCH] Toteboard packets --- scripts/globals/chocobo_racing.lua | 251 ++++++++++++++++++ scripts/zones/Chocobo_Circuit/npcs/Jadamo.lua | 17 ++ .../zones/Chocobo_Circuit/npcs/Jaicedion.lua | 17 ++ .../zones/Chocobo_Circuit/npcs/Olorinda.lua | 17 ++ .../Chocobo_Circuit/npcs/Uhko_Rolinzoh.lua | 17 ++ scripts/zones/Chocobo_Circuit/npcs/Urbano.lua | 17 ++ src/map/enums/packet_s2c.h | 4 +- src/map/lua/lua_base_entity.cpp | 22 +- .../packets/c2s/0x09b_chocobo_race_req.cpp | 177 +++--------- src/map/packets/s2c/0x069_chocobo_racing.cpp | 28 ++ src/map/packets/s2c/0x069_chocobo_racing.h | 3 + .../packets/s2c/0x073_chocobo_toteboard.cpp | 36 +++ src/map/packets/s2c/0x073_chocobo_toteboard.h | 45 ++++ src/map/packets/s2c/0x074_chocobo_list.cpp | 76 ++++++ src/map/packets/s2c/0x074_chocobo_list.h | 112 ++++++++ src/map/packets/s2c/CMakeLists.txt | 4 + 16 files changed, 683 insertions(+), 160 deletions(-) create mode 100644 scripts/zones/Chocobo_Circuit/npcs/Jadamo.lua create mode 100644 scripts/zones/Chocobo_Circuit/npcs/Jaicedion.lua create mode 100644 scripts/zones/Chocobo_Circuit/npcs/Olorinda.lua create mode 100644 scripts/zones/Chocobo_Circuit/npcs/Uhko_Rolinzoh.lua create mode 100644 scripts/zones/Chocobo_Circuit/npcs/Urbano.lua create mode 100644 src/map/packets/s2c/0x073_chocobo_toteboard.cpp create mode 100644 src/map/packets/s2c/0x073_chocobo_toteboard.h create mode 100644 src/map/packets/s2c/0x074_chocobo_list.cpp create mode 100644 src/map/packets/s2c/0x074_chocobo_list.h diff --git a/scripts/globals/chocobo_racing.lua b/scripts/globals/chocobo_racing.lua index ff376eecee3..58fe2d06899 100644 --- a/scripts/globals/chocobo_racing.lua +++ b/scripts/globals/chocobo_racing.lua @@ -8,6 +8,12 @@ require('scripts/globals/packet') xi = xi or {} xi.chocoboRacing = xi.chocoboRacing or {} +local settings = +{ + RACE_PERIOD = 900, -- A race every 15 minutes. + VANADIEL_EPOCH = 1009810800, -- Jan 1 2002 +} + -- FOR HEAVILY-IN-DEVELOPMET TESTING, you can force these setting: -- TODO: When ready for release, publish these to main settings files. xi.settings.main.ENABLE_CHOCOBO_RACING = false @@ -260,3 +266,248 @@ xi.chocoboRacing.onEventFinish = function(player, csid, option, npc) debug(player, 'Hand out winnings') end end + +-- This is an example set of 8 Chocobos competing in the upcoming race +-- Replace with actual chocobos when implementing. +local fakeChocoboRacers = +{ + { + name = 'Joyeuse', + orders = xi.chocoboRacing.order.KEEP_PACE, + item = xi.chocoboRacing.sectionEvent.SPEED_APPLE, + weather = xi.chocoboRaising.weather.RAINY, + color = xi.chocoboRaising.color.BLACK, + gender = xi.chocoboRaising.gender.FEMALE, + size = xi.chocoboRacing.jockeySize.HUME_M, + ability1 = xi.chocoboRaising.ability.BURROW, + ability2 = xi.chocoboRaising.ability.NONE, + temperament = xi.chocoboRaising.temperament.VERY_PATIENT, + stats = + { + str = xi.chocoboRaising.statRank.BETTER_THAN_AVG, + ['end'] = xi.chocoboRaising.statRank.OUTSTANDING, + dsc = xi.chocoboRaising.statRank.IMPRESSIVE, + rcp = xi.chocoboRaising.statRank.IMPRESSIVE, + }, + }, + { + name = 'Schuss', + orders = xi.chocoboRacing.order.KEEP_PACE, + item = xi.chocoboRacing.sectionEvent.FIRE_BISCUIT, + weather = xi.chocoboRaising.weather.CLEAR, + color = xi.chocoboRaising.color.RED, + gender = xi.chocoboRaising.gender.MALE, + size = xi.chocoboRacing.jockeySize.HUME_M, + ability1 = xi.chocoboRaising.ability.NONE, + ability2 = xi.chocoboRaising.ability.NONE, + temperament = xi.chocoboRaising.temperament.ILL_TEMPERED, + stats = + { + str = xi.chocoboRaising.statRank.BETTER_THAN_AVG, + ['end'] = xi.chocoboRaising.statRank.AVERAGE, + dsc = xi.chocoboRaising.statRank.FIRST_CLASS, + rcp = xi.chocoboRaising.statRank.OUTSTANDING, + }, + }, + { + name = 'Humble', + orders = xi.chocoboRacing.order.FINAL_SPURT, + item = xi.chocoboRacing.sectionEvent.SPEED_APPLE, + weather = xi.chocoboRaising.weather.CLEAR, + color = xi.chocoboRaising.color.BLACK, + gender = xi.chocoboRaising.gender.MALE, + size = xi.chocoboRacing.jockeySize.HUME_F, + ability1 = xi.chocoboRaising.ability.BURROW, + ability2 = xi.chocoboRaising.ability.GALLOP, + temperament = xi.chocoboRaising.temperament.QUITE_SENSITIVE, + stats = + { + str = xi.chocoboRaising.statRank.FIRST_CLASS, + ['end'] = xi.chocoboRaising.statRank.FIRST_CLASS, + dsc = xi.chocoboRaising.statRank.BETTER_THAN_AVG, + rcp = xi.chocoboRaising.statRank.POOR, + }, + }, + { + name = 'Melody', + orders = xi.chocoboRacing.order.FINAL_SPURT, + item = xi.chocoboRacing.sectionEvent.GYSAHL_BOMB, + weather = xi.chocoboRaising.weather.CLEAR, + color = xi.chocoboRaising.color.GREEN, + gender = xi.chocoboRaising.gender.MALE, + size = xi.chocoboRacing.jockeySize.HUME_M, + ability1 = xi.chocoboRaising.ability.NONE, + ability2 = xi.chocoboRaising.ability.NONE, + temperament = xi.chocoboRaising.temperament.VERY_PATIENT, + stats = + { + str = xi.chocoboRaising.statRank.FIRST_CLASS, + ['end'] = xi.chocoboRaising.statRank.FIRST_CLASS, + dsc = xi.chocoboRaising.statRank.A_BIT_DEFICIENT, + rcp = xi.chocoboRaising.statRank.A_BIT_DEFICIENT, + }, + }, + { + name = 'Gagnante', + orders = xi.chocoboRacing.order.KEEP_PACE, + item = xi.chocoboRacing.sectionEvent.PEPPER_BISCUIT, + weather = xi.chocoboRaising.weather.RAINY, + color = xi.chocoboRaising.color.RED, + gender = xi.chocoboRaising.gender.MALE, + size = xi.chocoboRacing.jockeySize.HUME_F, + ability1 = xi.chocoboRaising.ability.BURROW, + ability2 = xi.chocoboRaising.ability.CANTER, + temperament = xi.chocoboRaising.temperament.ENIGMATIC, + stats = + { + str = xi.chocoboRaising.statRank.FIRST_CLASS, + ['end'] = xi.chocoboRaising.statRank.OUTSTANDING, + dsc = xi.chocoboRaising.statRank.AVERAGE, + rcp = xi.chocoboRaising.statRank.BETTER_THAN_AVG, + }, + }, + { + name = 'Jolie', + orders = xi.chocoboRacing.order.KEEP_PACE, + item = xi.chocoboRacing.sectionEvent.FIRE_BISCUIT, + weather = xi.chocoboRaising.weather.SANDSTORMS, + color = xi.chocoboRaising.color.YELLOW, + gender = xi.chocoboRaising.gender.FEMALE, + size = xi.chocoboRacing.jockeySize.HUME_F, + ability1 = xi.chocoboRaising.ability.BORE, + ability2 = xi.chocoboRaising.ability.NONE, + temperament = xi.chocoboRaising.temperament.ENIGMATIC, + stats = + { + str = xi.chocoboRaising.statRank.BETTER_THAN_AVG, + ['end'] = xi.chocoboRaising.statRank.AVERAGE, + dsc = xi.chocoboRaising.statRank.FIRST_CLASS, + rcp = xi.chocoboRaising.statRank.OUTSTANDING, + }, + }, + { + name = 'Invincible', + orders = xi.chocoboRacing.order.SPRINT, + item = xi.chocoboRacing.sectionEvent.SHADOW_APPLE, + weather = xi.chocoboRaising.weather.CLEAR, + color = xi.chocoboRaising.color.YELLOW, + gender = xi.chocoboRaising.gender.FEMALE, + size = xi.chocoboRacing.jockeySize.HUME_M, + ability1 = xi.chocoboRaising.ability.NONE, + ability2 = xi.chocoboRaising.ability.NONE, + temperament = xi.chocoboRaising.temperament.ILL_TEMPERED, + stats = + { + str = xi.chocoboRaising.statRank.FIRST_CLASS, + ['end'] = xi.chocoboRaising.statRank.FIRST_CLASS, + dsc = xi.chocoboRaising.statRank.POOR, + rcp = xi.chocoboRaising.statRank.POOR, + }, + }, + { + name = 'Bec', + orders = xi.chocoboRacing.order.KEEP_PACE, + item = xi.chocoboRacing.sectionEvent.FIRE_BISCUIT, + weather = xi.chocoboRaising.weather.CLEAR, + color = xi.chocoboRaising.color.YELLOW, + gender = xi.chocoboRaising.gender.FEMALE, + size = xi.chocoboRacing.jockeySize.HUME_F, + ability1 = xi.chocoboRaising.ability.NONE, + ability2 = xi.chocoboRaising.ability.NONE, + temperament = xi.chocoboRaising.temperament.QUITE_SENSITIVE, + stats = + { + str = xi.chocoboRaising.statRank.BETTER_THAN_AVG, + ['end'] = xi.chocoboRaising.statRank.AVERAGE, + dsc = xi.chocoboRaising.statRank.FIRST_CLASS, + rcp = xi.chocoboRaising.statRank.OUTSTANDING, + }, + }, +} + +-- Return auto-generated race number, advancing every 15 minutes +xi.chocoboRacing.currentRaceNo = function() + return math.floor(GetSystemTime() / settings.RACE_PERIOD) +end + +-- Sends the "Betting ends in..." countdown via a PENDINGNUM +xi.chocoboRacing.sendBettingCountdown = function(player, listIndex) + local now = GetSystemTime() + local closesIn = settings.RACE_PERIOD - (now % settings.RACE_PERIOD) + player:updateEvent( + closesIn, -- When the betting period ends + now - settings.VANADIEL_EPOCH, -- Current server time. Used by client to know when to refresh. + 0, 0, 0, + closesIn, -- When the betting period ends + 1, -- Unknown + listIndex or 0 -- Presently unused but may be used to index into a different race + ) +end + +xi.chocoboRacing.onToteboardTrigger = function(player, eventId) + player:startEvent( + eventId, -- NPC Event ID + 1, -- Race weather + -1, -1, -1, -- PC position, retail sends -1 + player:getRotPos() * 16, -- PC rotation, retail sends rot * 16 + 0, -- Copied but never used by event scripts + GetSystemTime() - settings.VANADIEL_EPOCH, -- Server timestamp, appears to be unused by event + 2 -- 2 opens the board, -1 shows "Preparations for the next race are underway" + ) +end + +xi.chocoboRacing.onToteboardEventUpdate = function(player, option) + -- Client is browsing to the Toteboard or the client is requesting an automatic update + if option == 3 or option == 4 then + xi.chocoboRacing.sendBettingCountdown(player, 0) + return + end + + -- Client is requesting to view a specific chocobo from the Paddock + if option % 8 == 0 and option <= 56 then + local slot = math.floor(option / 8) + local choco = fakeChocoboRacers[slot + 1] + if choco then + -- Pack the chocobo stats for event purposes: + -- statBytes = STR|END|DSC|RCP, dna = the ability/weather/gender/colour/size/temperament bitfield. + local stats = choco.stats + local statBytes = bit.bor(bit.lshift(stats.str, 5), bit.lshift(stats['end'], 13), bit.lshift(stats.dsc, 21), bit.lshift(stats.rcp, 29)) + local dna = bit.bor(bit.lshift(choco.ability1, 9), bit.lshift(choco.ability2, 13), bit.lshift(choco.temperament, 17), bit.lshift(choco.weather, 20), bit.lshift(choco.gender, 24), + bit.lshift(choco.color, 25), bit.lshift(choco.size, 28)) + + -- First, send a PENDINGSTR with the name of the Chocobo (all 4 slots) + player:updateEventString(choco.name, choco.name, choco.name, choco.name) + -- Second, update the event VM with the Chocobo stats + player:updateEvent(6, 0, choco.orders, choco.item, statBytes, dna, 1, slot * 2) + end + end +end + +-- Flow race information back to core for specific menu requests. (Toteboard) +xi.chocoboRacing.onRaceRequest = function(player, param, kind) + if kind == 1 then -- Toteboard: betting odds + -- Quinella odds: exactly 28 entries must be passed, one per chocobo pair (8-choose-2 = 28), + -- in this fixed order (a pair = the two chocobos finishing 1st/2nd, in either order): + -- [1]=(1,2) [2]=(1,3) [3]=(1,4) [4]=(1,5) [5]=(1,6) [6]=(1,7) [7]=(1,8) + -- [8]=(2,3) [9]=(2,4) [10]=(2,5) [11]=(2,6) [12]=(2,7) [13]=(2,8) + -- [14]=(3,4) [15]=(3,5) [16]=(3,6) [17]=(3,7) [18]=(3,8) + -- [19]=(4,5) [20]=(4,6) [21]=(4,7) [22]=(4,8) + -- [23]=(5,6) [24]=(5,7) [25]=(5,8) + -- [26]=(6,7) [27]=(6,8) + -- [28]=(7,8) + -- Each value is that pair's payout; 50000 means "no bet". + return + { + grade = xi.chocoboRacing.raceGrade.C4, + raceNumber = xi.chocoboRacing.currentRaceNo(), + odds = { 10050, 773, 837, 913, 50000, 50000, 773, 2512, 3350, 2512, 50000, 50000, 50000, 558, 670, 50000, 50000, 1256, 628, 50000, 50000, 773, 50000, 50000, 3350, 50000, 50000, 50000 }, + } + end + + -- ChocoboList: race weather and entrant field + return + { + weather = xi.weather.SUNSHINE, + chocobos = fakeChocoboRacers, + } +end diff --git a/scripts/zones/Chocobo_Circuit/npcs/Jadamo.lua b/scripts/zones/Chocobo_Circuit/npcs/Jadamo.lua new file mode 100644 index 00000000000..f1ef3002484 --- /dev/null +++ b/scripts/zones/Chocobo_Circuit/npcs/Jadamo.lua @@ -0,0 +1,17 @@ +----------------------------------- +-- Area: Chocobo Circuit +-- NPC: Jadamo +-- !pos -65.812 -14.500 -136.126 70 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + xi.chocoboRacing.onToteboardTrigger(player, 335) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.chocoboRacing.onToteboardEventUpdate(player, option) +end + +return entity diff --git a/scripts/zones/Chocobo_Circuit/npcs/Jaicedion.lua b/scripts/zones/Chocobo_Circuit/npcs/Jaicedion.lua new file mode 100644 index 00000000000..2b9a03a4227 --- /dev/null +++ b/scripts/zones/Chocobo_Circuit/npcs/Jaicedion.lua @@ -0,0 +1,17 @@ +----------------------------------- +-- Area: Chocobo Circuit +-- NPC: Jaicedion +-- !pos -41.904 -14.500 -135.923 70 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + xi.chocoboRacing.onToteboardTrigger(player, 336) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.chocoboRacing.onToteboardEventUpdate(player, option) +end + +return entity diff --git a/scripts/zones/Chocobo_Circuit/npcs/Olorinda.lua b/scripts/zones/Chocobo_Circuit/npcs/Olorinda.lua new file mode 100644 index 00000000000..86cf02f71c6 --- /dev/null +++ b/scripts/zones/Chocobo_Circuit/npcs/Olorinda.lua @@ -0,0 +1,17 @@ +----------------------------------- +-- Area: Chocobo Circuit +-- NPC: Olorinda +-- !pos -113.471 -14.500 -136.238 70 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + xi.chocoboRacing.onToteboardTrigger(player, 333) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.chocoboRacing.onToteboardEventUpdate(player, option) +end + +return entity diff --git a/scripts/zones/Chocobo_Circuit/npcs/Uhko_Rolinzoh.lua b/scripts/zones/Chocobo_Circuit/npcs/Uhko_Rolinzoh.lua new file mode 100644 index 00000000000..7f8ea31a971 --- /dev/null +++ b/scripts/zones/Chocobo_Circuit/npcs/Uhko_Rolinzoh.lua @@ -0,0 +1,17 @@ +----------------------------------- +-- Area: Chocobo Circuit +-- NPC: Uhko Rolinzoh +-- !pos -89.524 -14.500 -136.175 70 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + xi.chocoboRacing.onToteboardTrigger(player, 334) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.chocoboRacing.onToteboardEventUpdate(player, option) +end + +return entity diff --git a/scripts/zones/Chocobo_Circuit/npcs/Urbano.lua b/scripts/zones/Chocobo_Circuit/npcs/Urbano.lua new file mode 100644 index 00000000000..356523efc91 --- /dev/null +++ b/scripts/zones/Chocobo_Circuit/npcs/Urbano.lua @@ -0,0 +1,17 @@ +----------------------------------- +-- Area: Chocobo Circuit +-- NPC: Urbano +-- !pos -17.515 -14.500 -136.001 70 +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + xi.chocoboRacing.onToteboardTrigger(player, 337) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.chocoboRacing.onToteboardEventUpdate(player, option) +end + +return entity diff --git a/src/map/enums/packet_s2c.h b/src/map/enums/packet_s2c.h index bcb1b960f86..fb3685f5a42 100644 --- a/src/map/enums/packet_s2c.h +++ b/src/map/enums/packet_s2c.h @@ -110,8 +110,8 @@ enum class PacketS2C : uint16_t GP_SERV_COMMAND_COMBINE_INF = 0x070, GP_SERV_COMMAND_INFLUENCE = 0x071, GP_SERV_COMMAND_UNKNOWN_072 = 0x072, - GP_SERV_COMMAND_UNKNOWN_073 = 0x073, - GP_SERV_COMMAND_UNKNOWN_074 = 0x074, + GP_SERV_COMMAND_CHOCOBO_TOTEBOARD = 0x073, + GP_SERV_COMMAND_CHOCOBO_LIST = 0x074, GP_SERV_COMMAND_BATTLEFIELD = 0x075, GP_SERV_COMMAND_GROUP_EFFECTS = 0x076, GP_SERV_COMMAND_ENTITY_VIS = 0x077, diff --git a/src/map/lua/lua_base_entity.cpp b/src/map/lua/lua_base_entity.cpp index ffc6feafc78..f0a86be2dfb 100644 --- a/src/map/lua/lua_base_entity.cpp +++ b/src/map/lua/lua_base_entity.cpp @@ -1150,27 +1150,7 @@ void CLuaBaseEntity::sendChocoboRace(const sol::table& race) const for (size_t idx = 1; idx <= count; ++idx) { - auto& entry = entries[idx - 1]; - const auto data = chocobos->get(idx); - - entry.Item = data.get_or("item", 0); // equipped item (sectionEvent) - entry.Orders = data.get_or("orders", 0); // jockey orders - entry.Size = data.get_or("size", 0); // jockey (jockeySize) - entry.Color = data.get_or("color", 0); // plumage colour - entry.Gender = data.get_or("gender", 0); - entry.Weather = data.get_or("weather", 0); // preferred weather (xi.chocoboRaising.weather) - entry.Temperament = data.get_or("temperament", 0); // display/raising trait; no race effect - entry.Ability1 = data.get_or("ability1", 0); // xi.chocoboRaising.ability (Gallop/Canter/...) - entry.Ability2 = data.get_or("ability2", 0); - - // Racing stats (chococard ranks 0-7 = F..SS): STR/END/DSC/RCP. - if (const auto stats = data.get>("stats")) - { - entry.STR.Rank = stats->get_or("str", 0); - entry.END.Rank = stats->get_or("end", 0); - entry.DSC.Rank = stats->get_or("dsc", 0); - entry.RCP.Rank = stats->get_or("rcp", 0); - } + entries[idx - 1] = GP_SERV_COMMAND_CHOCOBO_RACING::ChocoboParam::fromLua(chocobos->get(idx)); } PChar->pushPacket(entries); diff --git a/src/map/packets/c2s/0x09b_chocobo_race_req.cpp b/src/map/packets/c2s/0x09b_chocobo_race_req.cpp index 79d0d34a7bf..9e32605c401 100644 --- a/src/map/packets/c2s/0x09b_chocobo_race_req.cpp +++ b/src/map/packets/c2s/0x09b_chocobo_race_req.cpp @@ -22,166 +22,69 @@ #include "0x09b_chocobo_race_req.h" #include "entities/char_entity.h" -#include "packets/basic.h" +#include "lua/luautils.h" +#include "packets/s2c/0x073_chocobo_toteboard.h" +#include "packets/s2c/0x074_chocobo_list.h" auto GP_CLI_COMMAND_CHOCOBO_RACE_REQ::validate(MapSession* PSession, const CCharEntity* PChar) const -> PacketValidationResult { return PacketValidator(PChar) - .blockedBy({ BlockedState::InEvent }) - .oneOf(this->Param) + .mustEqual(PChar->getZone(), ZONE_CHOCOBO_CIRCUIT, "Not in Chocobo Circuit") .oneOf(this->Kind); } void GP_CLI_COMMAND_CHOCOBO_RACE_REQ::process(MapSession* PSession, CCharEntity* PChar) const { - ShowDebugFmt("GP_CLI_COMMAND_CHOCOBO_RACE_REQ: Not fully implemented. Param: {}, Kind: {}", this->Param, this->Kind); - - // NOTE: Can trigger with !cs 335 from Chocobo Circuit + const auto board = luautils::callGlobal("xi.chocoboRacing.onRaceRequest", PChar, this->Param, this->Kind); + if (board.empty()) + { + return; + } switch (static_cast(this->Kind)) { - case GP_CLI_COMMAND_CHOCOBO_RACE_REQ_KIND::Toteboard: + case GP_CLI_COMMAND_CHOCOBO_RACE_REQ_KIND::ChocoboList: { - auto packet = std::make_unique(); - packet->setType(0x73); - packet->setSize(0x48); + // Mode 1: race parameters. + PChar->pushPacket(board.get_or("weather", 0)); + + // Modes 2 & 3: entrant stats and names. + if (const auto chocobos = board.get>("chocobos")) + { + const auto count = std::min(chocobos->size(), GP_SERV_COMMAND_CHOCOBO_LIST::kNumRacers); + std::vector stats(count); + std::vector names(count); + + for (size_t idx = 1; idx <= count; ++idx) + { + const auto data = chocobos->get(idx); - packet->ref(0x04) = 0x01; + stats[idx - 1] = GP_SERV_COMMAND_CHOCOBO_RACING::ChocoboParam::fromLua(data); + names[idx - 1] = data.get_or("name", ""); + } - // Lots of look data, maybe? - packet->ref(0x08) = 0x003B4879; - packet->ref(0x10) = 0x00B1C350; - // etc. + PChar->pushPacket(stats); + PChar->pushPacket(names); + } - PChar->pushPacket(std::move(packet)); + // Mode 4: notify the client the exchange is done. + PChar->pushPacket(); } break; - case GP_CLI_COMMAND_CHOCOBO_RACE_REQ_KIND::ChocoboList: + case GP_CLI_COMMAND_CHOCOBO_RACE_REQ_KIND::Toteboard: { - // Send Chocobo Race Data (4x 0x074) - for (int idx = 0x01; idx <= 0x04; ++idx) + std::vector odds; + if (const auto oddsTable = board.get>("odds")) { - auto packet = std::make_unique(); - packet->setType(0x74); - packet->setSize(0xB3); - - packet->ref(0x03) = 0x04; - packet->ref(0x04) = 0x03; - - packet->ref(0x10) = idx; - - switch (idx) + const auto count = std::min(oddsTable->size(), GP_SERV_COMMAND_CHOCOBO_TOTEBOARD::kNumOdds); + odds.reserve(count); + for (size_t idx = 1; idx <= count; ++idx) { - /* - [2023-11-13 12:33:14] Incoming packet 0x074: - | 0 1 2 3 4 5 6 7 8 9 A B C D E F | 0123456789ABCDEF - ----------------------------------------------------- ---------------------- - 0 | 74 5A 98 04 03 00 00 00 00 00 00 00 00 00 00 00 0 | tZ.............. - 1 | 01 00 08 00 28 00 00 00 03 00 00 C0 00 00 00 00 1 | ....(........... - 2 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2 | ................ - 3 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3 | ................ - 4 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4 | ................ - 5 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 5 | ................ - 6 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 6 | ................ - 7 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7 | ................ - 8 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8 | ................ - 9 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 9 | ................ - A | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A | ................ - B | 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- B | ....------------ - */ - case 0x01: - { - packet->ref(0x12) = 0x08; - packet->ref(0x14) = 0x28; // Seen also as 0xC8 - packet->ref(0x18) = 0x03; // Seen also as 0x01 - packet->ref(0x1B) = 0xC0; - break; - } - /* - [2023-11-13 12:33:14] Incoming packet 0x074: - | 0 1 2 3 4 5 6 7 8 9 A B C D E F | 0123456789ABCDEF - ----------------------------------------------------- ---------------------- - 0 | 74 5A 98 04 03 00 00 00 00 00 00 00 00 00 00 00 0 | tZ.............. - 1 | 02 00 60 00 30 00 00 00 FF FF 00 00 00 02 24 13 1 | ..`.0.........$. - 2 | 62 00 00 00 FF FF 40 40 00 82 02 15 41 00 00 00 2 | b.....@@....A... - 3 | E0 C0 60 80 00 02 20 26 21 00 00 00 C0 80 C0 80 3 | ..`... &!....... - 4 | 00 00 24 10 12 00 00 00 FF FF 80 00 00 02 40 10 4 | ..$...........@. - 5 | 51 00 00 00 80 60 E0 C0 00 08 08 10 30 00 00 00 5 | Q....`......0... - 6 | FF FF 00 00 00 0C 02 11 62 00 00 00 FF FF 40 40 6 | ........b.....@@ - 7 | 00 C6 20 22 00 00 00 00 00 00 00 00 00 00 00 00 7 | .. "............ - 8 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8 | ................ - 9 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 9 | ................ - A | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A | ................ - B | 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- B | ....------------ - */ - case 0x02: - { - // Stat and other data starting at 0x12 - packet->ref(0x04) = 0x01; - packet->ref(0x14) = 0x12; - - packet->ref(0x18) = 0x0080FFFF; - packet->ref(0x1C) = 0x13000A00; - - break; - } - /* - [2023-11-13 12:33:14] Incoming packet 0x074: - | 0 1 2 3 4 5 6 7 8 9 A B C D E F | 0123456789ABCDEF - ----------------------------------------------------- ---------------------- - 0 | 74 5A 98 04 03 00 00 00 00 00 00 00 00 00 00 00 0 | tZ.............. - 1 | 03 00 A0 00 00 00 00 00 49 72 69 73 00 00 00 00 1 | ........Iris.... - 2 | 00 00 00 00 00 00 00 00 00 00 00 00 53 61 64 64 2 | ............Sadd - 3 | 6C 65 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3 | le.............. - 4 | 43 79 63 6C 6F 6E 65 00 00 00 00 00 00 00 00 00 4 | Cyclone......... - 5 | 00 00 00 00 50 72 69 6E 74 65 6D 70 73 00 00 00 5 | ....Printemps... - 6 | 00 00 00 00 00 00 00 00 54 72 69 73 74 61 6E 00 6 | ........Tristan. - 7 | 00 00 00 00 00 00 00 00 00 00 00 00 4F 75 74 6C 7 | ............Outl - 8 | 61 77 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8 | aw.............. - 9 | 48 75 72 72 69 63 61 6E 65 00 00 00 00 00 00 00 9 | Hurricane....... - A | 00 00 00 00 52 61 67 69 6E 67 00 00 00 00 00 00 A | ....Raging...... - B | 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- B | ....------------ - */ - case 0x03: - { - // Name Data starting at 0x18 - break; - } - /* - [2023-11-13 12:33:15] Incoming packet 0x074: - | 0 1 2 3 4 5 6 7 8 9 A B C D E F | 0123456789ABCDEF - ----------------------------------------------------- ---------------------- - 0 | 74 5A 99 04 03 00 00 00 00 00 00 00 00 00 00 00 0 | tZ.............. - 1 | 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1 | ................ - 2 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2 | ................ - 3 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3 | ................ - 4 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4 | ................ - 5 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 5 | ................ - 6 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 6 | ................ - 7 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7 | ................ - 8 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8 | ................ - 9 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 9 | ................ - A | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A | ................ - B | 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- B | ....------------ - */ - case 0x04: - { - packet->ref(0x04) = 0x9B; - packet->ref(0x05) = 0x60; - packet->ref(0x06) = 0x04; - packet->ref(0x07) = 0x01; - packet->ref(0x08) = 0x9B; - packet->ref(0x30) = 0x01; - break; - } - default: - { - break; - } + odds.push_back(static_cast(oddsTable->get_or(idx, 0))); } - - PChar->pushPacket(std::move(packet)); } + + PChar->pushPacket(this->Param, board.get_or("grade", 0), board.get_or("raceNumber", 0), odds); } break; } diff --git a/src/map/packets/s2c/0x069_chocobo_racing.cpp b/src/map/packets/s2c/0x069_chocobo_racing.cpp index 1462296add4..6ba5b5c77a8 100644 --- a/src/map/packets/s2c/0x069_chocobo_racing.cpp +++ b/src/map/packets/s2c/0x069_chocobo_racing.cpp @@ -21,11 +21,39 @@ #include "0x069_chocobo_racing.h" +#include "common/lua.h" + #include namespace GP_SERV_COMMAND_CHOCOBO_RACING { +auto ChocoboParam::fromLua(const sol::table& data) -> ChocoboParam +{ + ChocoboParam param{}; + + param.Item = data.get_or("item", 0); // xi.chocoboRacing.sectionEvent + param.Orders = data.get_or("orders", 0); // xi.chocoboRacing.order + param.Size = data.get_or("size", 0); // xi.chocoboRacing.jockeySize + param.Color = data.get_or("color", 0); // xi.chocoboRaising.color + param.Gender = data.get_or("gender", 0); // xi.chocoboRaising.gender + param.Weather = data.get_or("weather", 0); // xi.chocoboRaising.weather + param.Temperament = data.get_or("temperament", 0); // xi.chocoboRaising.temperament + param.Ability1 = data.get_or("ability1", 0); // xi.chocoboRaising.ability + param.Ability2 = data.get_or("ability2", 0); // xi.chocoboRaising.ability + + // stats str/end/dsc/rcp: xi.chocoboRaising.statRank + if (const auto stats = data.get>("stats")) + { + param.STR.Rank = stats->get_or("str", 0); + param.END.Rank = stats->get_or("end", 0); + param.DSC.Rank = stats->get_or("dsc", 0); + param.RCP.Rank = stats->get_or("rcp", 0); + } + + return param; +} + // Various fields pack 2 chocobos per uint8. void packNibbles(uint8_t out[4], const std::array& nibbles) { diff --git a/src/map/packets/s2c/0x069_chocobo_racing.h b/src/map/packets/s2c/0x069_chocobo_racing.h index 7f9bef2e41f..4e7ec7a0718 100644 --- a/src/map/packets/s2c/0x069_chocobo_racing.h +++ b/src/map/packets/s2c/0x069_chocobo_racing.h @@ -23,6 +23,7 @@ #include "base.h" #include "items/exdata/chocobo_card.h" +#include "sol/forward.hpp" #include #include @@ -61,6 +62,8 @@ struct ChocoboParam uint32_t Color : 3; // xi.chocoboRaising.color uint32_t Size : 3; // xi.chocoboRacing.jockeySize uint32_t unknown00 : 1; + + static ChocoboParam fromLua(const sol::table& data); }; // Each racing section/keyframe can contain optional trigger/events diff --git a/src/map/packets/s2c/0x073_chocobo_toteboard.cpp b/src/map/packets/s2c/0x073_chocobo_toteboard.cpp new file mode 100644 index 00000000000..40c947ad3cd --- /dev/null +++ b/src/map/packets/s2c/0x073_chocobo_toteboard.cpp @@ -0,0 +1,36 @@ +/* +=========================================================================== + + Copyright (c) 2025 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#include "0x073_chocobo_toteboard.h" + +GP_SERV_COMMAND_CHOCOBO_TOTEBOARD::GP_SERV_COMMAND_CHOCOBO_TOTEBOARD(const uint32_t slotIndex, const uint32_t grade, const uint32_t raceNumber, const std::vector& odds) +{ + auto& packet = this->data(); + + packet.SlotIndex = slotIndex; + packet.Ident = (grade << 18) | (raceNumber & 0x3FFFF); // grade in bits 18-23, race number in bits 0-17 + + const auto count = std::min(odds.size(), kNumOdds); + for (size_t i = 0; i < count; ++i) + { + packet.Odds[i] = odds[i]; + } +} diff --git a/src/map/packets/s2c/0x073_chocobo_toteboard.h b/src/map/packets/s2c/0x073_chocobo_toteboard.h new file mode 100644 index 00000000000..c8c9de29a2c --- /dev/null +++ b/src/map/packets/s2c/0x073_chocobo_toteboard.h @@ -0,0 +1,45 @@ +/* +=========================================================================== + + Copyright (c) 2025 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "base.h" + +#include + +// https://github.com/atom0s/XiPackets/tree/main/world/server/0x0073 +// This packet is sent by the server with the chocobo racing toteboard betting odds. +// 28 quinella pair combinations for the 8 entrants in response to GP_CLI_COMMAND_CHOCOBO_RACE_REQ +class GP_SERV_COMMAND_CHOCOBO_TOTEBOARD final : public GP_SERV_PACKET +{ +public: + static constexpr size_t kNumOdds = 28; // 8-choose-2 quinella pairs + + struct PacketData + { + uint32_t SlotIndex; // TODO: Research this more. Usually mirrors incoming Param + uint32_t Ident; // (grade << 18) | raceNumber (race number in bits 0-17) + uint32_t padding00; // Always 0 in captures + uint16_t Odds[kNumOdds]; // Actual odds + }; + + GP_SERV_COMMAND_CHOCOBO_TOTEBOARD(uint32_t slotIndex, uint32_t grade, uint32_t raceNumber, const std::vector& odds); +}; diff --git a/src/map/packets/s2c/0x074_chocobo_list.cpp b/src/map/packets/s2c/0x074_chocobo_list.cpp new file mode 100644 index 00000000000..bbfb010763c --- /dev/null +++ b/src/map/packets/s2c/0x074_chocobo_list.cpp @@ -0,0 +1,76 @@ +/* +=========================================================================== + + Copyright (c) 2025 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#include "0x074_chocobo_list.h" + +#include +#include + +namespace GP_SERV_COMMAND_CHOCOBO_LIST +{ + +RACE::RACE(const uint32_t weather) +{ + auto& packet = this->data(); + + packet.Mode = 1; + packet.ParamSize = 8; + packet.RaceParams[0] = (weather << 5) + 8; // pack the weather id + packet.RaceParams[1] = 0xC0000000; // TODO: Research more +} + +CHOCOBOS::CHOCOBOS(const std::vector& chocobos) +{ + auto& packet = this->data(); + const auto count = std::min(chocobos.size(), kNumRacers); + + packet.Mode = 2; + packet.ParamSize = static_cast(count * sizeof(GP_SERV_COMMAND_CHOCOBO_RACING::ChocoboParam)); + + for (size_t i = 0; i < count; ++i) + { + packet.Chocobos[i] = chocobos[i]; + } +} + +NAMES::NAMES(const std::vector& names) +{ + auto& packet = this->data(); + const auto count = std::min(names.size(), kNumRacers); + + packet.Mode = 3; + packet.ParamSize = static_cast(count * sizeof(ChocoboName)); + + for (size_t i = 0; i < count; ++i) + { + std::memcpy(packet.Chocobos[i].Name, names[i].c_str(), std::min(names[i].size(), kNameLength - 1)); + } +} + +END::END() +{ + auto& packet = this->data(); + + packet.Mode = 4; + packet.ParamSize = 0; +} + +} // namespace GP_SERV_COMMAND_CHOCOBO_LIST diff --git a/src/map/packets/s2c/0x074_chocobo_list.h b/src/map/packets/s2c/0x074_chocobo_list.h new file mode 100644 index 00000000000..f3810d9c205 --- /dev/null +++ b/src/map/packets/s2c/0x074_chocobo_list.h @@ -0,0 +1,112 @@ +/* +=========================================================================== + + Copyright (c) 2025 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "base.h" + +#include "0x069_chocobo_racing.h" + +#include +#include + +// https://github.com/atom0s/XiPackets/tree/main/world/server/0x0074 +// This packet is sent by the server for the chocobo racing entrant list in response to GP_CLI_COMMAND_CHOCOBO_RACE_REQ (Kind = ChocoboList). +// It carries the race weather, the entrant stats and the entrant names for a given race. +namespace GP_SERV_COMMAND_CHOCOBO_LIST +{ + +constexpr size_t kNumRacers = 8; // entrants per race +constexpr size_t kNameLength = 16; // chocobo name buffer + +struct ChocoboName +{ + uint32_t padding00; // always 0 in captures + char Name[kNameLength]; +}; + +// Mode 1: race parameters. +class RACE final : public GP_SERV_PACKET +{ +public: + struct PacketData + { + uint32_t SlotIndex; // TODO: Research this more. Mirrors incoming Param. + uint8_t padding04[8]; + uint16_t Mode; + uint16_t ParamSize; + uint32_t RaceParams[2]; + uint8_t junk00[152]; + }; + + RACE(uint32_t weather); +}; + +// Mode 2: racers' stats. +class CHOCOBOS final : public GP_SERV_PACKET +{ +public: + struct PacketData + { + uint32_t SlotIndex; // TODO: Research this more. Mirrors incoming Param. + uint8_t padding04[8]; + uint16_t Mode; + uint16_t ParamSize; + GP_SERV_COMMAND_CHOCOBO_RACING::ChocoboParam Chocobos[kNumRacers]; + uint8_t junk00[64]; + }; + + CHOCOBOS(const std::vector& chocobos); +}; + +// Mode 3: entrant names. +class NAMES final : public GP_SERV_PACKET +{ +public: + struct PacketData + { + uint32_t SlotIndex; // TODO: Research this more. Mirrors incoming Param. + uint8_t padding04[8]; + uint16_t Mode; + uint16_t ParamSize; + ChocoboName Chocobos[kNumRacers]; + }; + + NAMES(const std::vector& names); +}; + +// Mode 4: notify the client the toteboard exchange is done. +class END final : public GP_SERV_PACKET +{ +public: + struct PacketData + { + uint32_t SlotIndex; // TODO: Research this more. Mirrors incoming Param. + uint8_t unknown04[8]; // Used by client but captured as 0 + uint16_t Mode; + uint16_t ParamSize; + uint8_t junk00[160]; + }; + + END(); +}; + +} // namespace GP_SERV_COMMAND_CHOCOBO_LIST diff --git a/src/map/packets/s2c/CMakeLists.txt b/src/map/packets/s2c/CMakeLists.txt index f2739af6fc1..59500d25493 100644 --- a/src/map/packets/s2c/CMakeLists.txt +++ b/src/map/packets/s2c/CMakeLists.txt @@ -172,6 +172,10 @@ set(PACKET_S2C_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/0x071_influence_campaign.cpp ${CMAKE_CURRENT_SOURCE_DIR}/0x071_influence_colonization.h ${CMAKE_CURRENT_SOURCE_DIR}/0x071_influence_colonization.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/0x073_chocobo_toteboard.h + ${CMAKE_CURRENT_SOURCE_DIR}/0x073_chocobo_toteboard.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/0x074_chocobo_list.h + ${CMAKE_CURRENT_SOURCE_DIR}/0x074_chocobo_list.cpp ${CMAKE_CURRENT_SOURCE_DIR}/0x075_battlefield.h ${CMAKE_CURRENT_SOURCE_DIR}/0x075_battlefield.cpp ${CMAKE_CURRENT_SOURCE_DIR}/0x076_group_effects.h