Skip to content

Commit b821fa1

Browse files
committed
Chocobo racing packets/bindings
1 parent e84e5f8 commit b821fa1

7 files changed

Lines changed: 370 additions & 27 deletions

File tree

scripts/enum/chocobo_racing.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
xi = xi or {}
55
xi.chocoboRacing = xi.chocoboRacing or {}
66

7+
-- Chocobo Racing Jockey Orders
8+
---@enum xi.chocoboRacing.order
9+
xi.chocoboRacing.order =
10+
{
11+
SPRINT = 0, -- Top speed from the start until 75% stamina, then eases off
12+
KEEP_PACE = 1, -- Steady pace, even stamina drain, holds position
13+
FINAL_SPURT = 2, -- Holds back >=25% stamina for a top-speed finish
14+
}
15+
716
-- Stored in Chocobet and Completion Certificates exdata
817
---@enum xi.chocoboRacing.raceGrade
918
xi.chocoboRacing.raceGrade =
@@ -28,3 +37,21 @@ xi.chocoboRacing.jockeySize =
2837
TARUTARU_F = 6,
2938
MITHRA = 7,
3039
}
40+
41+
-- Per-section race events.
42+
---@enum xi.chocoboRacing.sectionEvent
43+
xi.chocoboRacing.sectionEvent =
44+
{
45+
STRAINING = 0x00, -- Low-stamina state
46+
SPEED_APPLE = 0x01,
47+
STAMINA_APPLE = 0x02,
48+
SHADOW_APPLE = 0x03,
49+
PEPPER_BISCUIT = 0x04, -- Target in Param
50+
FIRE_BISCUIT = 0x05, -- Target in Targets
51+
GYSAHL_BOMB = 0x06,
52+
SPORE_BOMB = 0x07,
53+
FAIRWEATHER_FETISH = 0x08,
54+
FOULWEATHER_FROG = 0x09,
55+
RACE_START = 0x20,
56+
ACCIDENT = 0x21, -- "feet caught in mud". Rainy races only.
57+
}

scripts/specs/core/CBaseEntity.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,47 @@ end
11141114
function CBaseEntity:sendLinkshellConcierge(data)
11151115
end
11161116

1117+
---@class ChocoboRaceStats
1118+
---@field str xi.chocoboRaising.statRank?
1119+
---@field ["end"] xi.chocoboRaising.statRank?
1120+
---@field dsc xi.chocoboRaising.statRank?
1121+
---@field rcp xi.chocoboRaising.statRank?
1122+
1123+
---@class ChocoboRaceEntry
1124+
---@field item xi.chocoboRacing.sectionEvent?
1125+
---@field orders xi.chocoboRacing.order?
1126+
---@field size xi.chocoboRacing.jockeySize?
1127+
---@field color xi.chocoboRaising.color?
1128+
---@field gender xi.chocoboRaising.gender?
1129+
---@field weather xi.chocoboRaising.weather?
1130+
---@field temperament xi.chocoboRaising.temperament?
1131+
---@field ability1 xi.chocoboRaising.ability?
1132+
---@field ability2 xi.chocoboRaising.ability?
1133+
---@field stats ChocoboRaceStats?
1134+
1135+
---@class ChocoboRaceTrigger
1136+
---@field type xi.chocoboRacing.sectionEvent?
1137+
---@field user integer? Bitmask: the acting chocobo
1138+
---@field targets integer? Bitmask: affected chocobos
1139+
---@field param integer? Bitmask: extra affected / type param
1140+
1141+
---@class ChocoboRaceSection
1142+
---@field from integer[] Per-chocobo positions at section start
1143+
---@field to integer[] Per-chocobo positions at section end
1144+
---@field trigger ChocoboRaceTrigger? Optional per-section event
1145+
1146+
---@class ChocoboRace
1147+
---@field counter integer? Race counter (rolling 0-3)
1148+
---@field weather xi.weather? Race weather
1149+
---@field chocobos ChocoboRaceEntry[]? The racers (up to 8)
1150+
---@field sections ChocoboRaceSection[]? The race sections
1151+
---@field places integer[]? Finishing place per chocobo (0 = 1st)
1152+
1153+
---@param race ChocoboRace
1154+
---@return nil
1155+
function CBaseEntity:sendChocoboRace(race)
1156+
end
1157+
11171158
---@nodiscard
11181159
---@param locationID integer
11191160
---@return integer

src/map/lua/lua_base_entity.cpp

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
#include "packets/s2c/0x063_miscdata_job_points.h"
150150
#include "packets/s2c/0x063_miscdata_merits.h"
151151
#include "packets/s2c/0x063_miscdata_monstrosity.h"
152+
#include "packets/s2c/0x069_chocobo_racing.h"
152153
#include "packets/s2c/0x075_battlefield.h"
153154
#include "packets/s2c/0x077_entity_vis.h"
154155
#include "packets/s2c/0x082_guild_buy.h"
@@ -1127,6 +1128,112 @@ void CLuaBaseEntity::sendLinkshellConcierge(const sol::table& data) const
11271128
}
11281129
}
11291130

1131+
/************************************************************************
1132+
* Function: sendChocoboRace()
1133+
* Purpose : Send the entire chocobo race content to the player.
1134+
* Note : Complex API, see chocobo_racing.lua for usage.
1135+
************************************************************************/
1136+
void CLuaBaseEntity::sendChocoboRace(const sol::table& race) const
1137+
{
1138+
auto* PChar = dynamic_cast<CCharEntity*>(m_PBaseEntity);
1139+
if (!PChar)
1140+
{
1141+
return;
1142+
}
1143+
1144+
// Read a 1-indexed lua table of per-chocobo values (positions or places) into a fixed array.
1145+
const auto readNibbles = [](const sol::table& values) -> std::array<uint8, GP_SERV_COMMAND_CHOCOBO_RACING::kNumRacers>
1146+
{
1147+
std::array<uint8, GP_SERV_COMMAND_CHOCOBO_RACING::kNumRacers> out{};
1148+
for (uint8 racer = 0; racer < GP_SERV_COMMAND_CHOCOBO_RACING::kNumRacers; ++racer)
1149+
{
1150+
out[racer] = values.get_or<uint8>(racer + 1, 0);
1151+
}
1152+
1153+
return out;
1154+
};
1155+
1156+
// Mode 1: Race parameters
1157+
PChar->pushPacket<GP_SERV_COMMAND_CHOCOBO_RACING::RACINGPARAMS>(race.get_or<uint32>("weather", 1), race.get_or<uint32>("counter", 0)); // 1 = xi.weather.SUNSHINE (clear)
1158+
1159+
// Mode 2: Racing Chocobos
1160+
if (const auto chocobos = race.get<sol::optional<sol::table>>("chocobos"))
1161+
{
1162+
const auto count = std::min<size_t>(chocobos->size(), GP_SERV_COMMAND_CHOCOBO_RACING::kNumRacers);
1163+
std::vector<GP_SERV_COMMAND_CHOCOBO_RACING::ChocoboParam> entries(count);
1164+
1165+
for (size_t idx = 1; idx <= count; ++idx)
1166+
{
1167+
auto& entry = entries[idx - 1];
1168+
const auto data = chocobos->get<sol::table>(idx);
1169+
1170+
entry.Item = data.get_or<uint8>("item", 0); // equipped item (sectionEvent)
1171+
entry.Orders = data.get_or<uint8>("orders", 0); // jockey orders
1172+
entry.Size = data.get_or<uint8>("size", 0); // jockey (jockeySize)
1173+
entry.Color = data.get_or<uint8>("color", 0); // plumage colour
1174+
entry.Gender = data.get_or<uint8>("gender", 0);
1175+
entry.Weather = data.get_or<uint8>("weather", 0); // preferred weather (xi.chocoboRaising.weather)
1176+
entry.Temperament = data.get_or<uint8>("temperament", 0); // display/raising trait; no race effect
1177+
entry.Ability1 = data.get_or<uint8>("ability1", 0); // xi.chocoboRaising.ability (Gallop/Canter/...)
1178+
entry.Ability2 = data.get_or<uint8>("ability2", 0);
1179+
1180+
// Racing stats (chococard ranks 0-7 = F..SS): STR/END/DSC/RCP.
1181+
if (const auto stats = data.get<sol::optional<sol::table>>("stats"))
1182+
{
1183+
entry.STR.Rank = stats->get_or<uint8>("str", 0);
1184+
entry.END.Rank = stats->get_or<uint8>("end", 0);
1185+
entry.DSC.Rank = stats->get_or<uint8>("dsc", 0);
1186+
entry.RCP.Rank = stats->get_or<uint8>("rcp", 0);
1187+
}
1188+
}
1189+
1190+
PChar->pushPacket<GP_SERV_COMMAND_CHOCOBO_RACING::CHOCOBOPARAMS>(entries);
1191+
}
1192+
1193+
// Mode 3: Racing sections, 16 sections per packet.
1194+
if (const auto raceSections = race.get<sol::optional<sol::table>>("sections"))
1195+
{
1196+
const size_t sectionCount = std::min<size_t>(raceSections->size(), GP_SERV_COMMAND_CHOCOBO_RACING::kMaxSections);
1197+
std::vector<GP_SERV_COMMAND_CHOCOBO_RACING::SectionParam> sections(sectionCount);
1198+
1199+
for (size_t idx = 1; idx <= sectionCount; ++idx)
1200+
{
1201+
const auto sec = raceSections->get<sol::table>(idx);
1202+
auto& section = sections[idx - 1];
1203+
1204+
GP_SERV_COMMAND_CHOCOBO_RACING::packNibbles(section.From, readNibbles(sec.get<sol::table>("from")));
1205+
GP_SERV_COMMAND_CHOCOBO_RACING::packNibbles(section.To, readNibbles(sec.get<sol::table>("to")));
1206+
1207+
if (const auto event = sec.get<sol::optional<sol::table>>("trigger"))
1208+
{
1209+
section.Trigger.User = event->get_or<uint8>("user", 0);
1210+
section.Trigger.Targets = event->get_or<uint8>("targets", 0);
1211+
section.Trigger.Param = event->get_or<uint8>("param", 0);
1212+
section.Trigger.Type = static_cast<GP_SERV_COMMAND_CHOCOBO_RACING::SectionEventType>(event->get_or<uint8>("type", 0));
1213+
}
1214+
}
1215+
1216+
// Each packet carries up to kSectionsPerPacket sections;
1217+
// ParamIndex is the starting section index.
1218+
for (size_t offset = 0; offset < sections.size(); offset += GP_SERV_COMMAND_CHOCOBO_RACING::kSectionsPerPacket)
1219+
{
1220+
const auto end = std::min(offset + GP_SERV_COMMAND_CHOCOBO_RACING::kSectionsPerPacket, sections.size());
1221+
1222+
std::vector<GP_SERV_COMMAND_CHOCOBO_RACING::SectionParam> chunk(sections.begin() + offset, sections.begin() + end);
1223+
PChar->pushPacket<GP_SERV_COMMAND_CHOCOBO_RACING::SECTIONPARAMS>(static_cast<uint8>(offset), chunk);
1224+
}
1225+
}
1226+
1227+
// Mode 4: Final race results.
1228+
if (const auto places = race.get<sol::optional<sol::table>>("places"))
1229+
{
1230+
PChar->pushPacket<GP_SERV_COMMAND_CHOCOBO_RACING::RESULTPARAMS>(readNibbles(*places));
1231+
}
1232+
1233+
// Mode 5: Notify client exchange is done.
1234+
PChar->pushPacket<GP_SERV_COMMAND_CHOCOBO_RACING::END>();
1235+
}
1236+
11301237
/************************************************************************
11311238
* Helper function for the lua bindings that start events.
11321239
************************************************************************/
@@ -20153,6 +20260,7 @@ void CLuaBaseEntity::Register()
2015320260
SOL_REGISTER("entityAnimationPacket", CLuaBaseEntity::entityAnimationPacket);
2015420261
SOL_REGISTER("sendDebugPacket", CLuaBaseEntity::sendDebugPacket);
2015520262
SOL_REGISTER("sendLinkshellConcierge", CLuaBaseEntity::sendLinkshellConcierge);
20263+
SOL_REGISTER("sendChocoboRace", CLuaBaseEntity::sendChocoboRace);
2015620264

2015720265
SOL_REGISTER("startEvent", CLuaBaseEntity::startEvent);
2015820266
SOL_REGISTER("startCutscene", CLuaBaseEntity::startCutscene);

src/map/lua/lua_base_entity.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class CLuaBaseEntity
9797
void entityAnimationPacket(const char* command, const sol::object& target);
9898
void sendDebugPacket(const sol::table& packetData);
9999
void sendLinkshellConcierge(const sol::table& data) const;
100+
void sendChocoboRace(const sol::table& race) const;
100101

101102
void StartEventHelper(int32 EventID, sol::variadic_args va, EVENT_TYPE eventType);
102103
EventInfo* ParseEvent(int32 EventID, sol::variadic_args va, EventPrep* eventPreparation, EVENT_TYPE eventType);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
===========================================================================
3+
4+
Copyright (c) 2025 LandSandBoat Dev Teams
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see http://www.gnu.org/licenses/
18+
19+
===========================================================================
20+
*/
21+
22+
#include "0x069_chocobo_racing.h"
23+
24+
#include <algorithm>
25+
26+
namespace GP_SERV_COMMAND_CHOCOBO_RACING
27+
{
28+
29+
// Various fields pack 2 chocobos per uint8.
30+
void packNibbles(uint8_t out[4], const std::array<uint8_t, kNumRacers>& nibbles)
31+
{
32+
for (size_t i = 0; i < kNumRacers / 2; ++i)
33+
{
34+
const uint8_t hi = nibbles[i * 2] & 0x0F;
35+
const uint8_t lo = nibbles[i * 2 + 1] & 0x0F;
36+
out[i] = static_cast<uint8_t>(hi << 4 | lo);
37+
}
38+
}
39+
40+
RACINGPARAMS::RACINGPARAMS(const uint32_t weather, const uint32_t raceCounter)
41+
{
42+
auto& packet = this->data();
43+
44+
packet.Mode = 1;
45+
packet.RaceParams[0] = (weather << 5) + 8; // pack the xi.weather id
46+
packet.RaceParams[1] = 0x80000000 | raceCounter & 0x3; // high bit + 2-bit rolling counter
47+
}
48+
49+
CHOCOBOPARAMS::CHOCOBOPARAMS(const std::vector<ChocoboParam>& chocobos)
50+
{
51+
auto& packet = this->data();
52+
const auto count = std::min<size_t>(chocobos.size(), kNumRacers); // Up to 8 chocobos. Some races use less.
53+
54+
packet.Mode = 2;
55+
packet.ParamSize = static_cast<uint8_t>(count * sizeof(ChocoboParam));
56+
57+
for (size_t i = 0; i < count; ++i)
58+
{
59+
packet.Chocobos[i] = chocobos[i];
60+
}
61+
}
62+
63+
SECTIONPARAMS::SECTIONPARAMS(const uint8_t startIndex, const std::vector<SectionParam>& sections)
64+
{
65+
auto& packet = this->data();
66+
const auto count = std::min<size_t>(sections.size(), kSectionsPerPacket);
67+
68+
packet.Mode = 3;
69+
packet.ParamIndex = startIndex; // This can be 0 or 16
70+
packet.ParamSize = static_cast<uint8_t>(count * sizeof(SectionParam));
71+
72+
for (size_t i = 0; i < count; ++i)
73+
{
74+
packet.Sections[i] = sections[i];
75+
}
76+
}
77+
78+
RESULTPARAMS::RESULTPARAMS(const std::array<uint8_t, kNumRacers>& places)
79+
{
80+
auto& packet = this->data();
81+
82+
packet.Mode = 4;
83+
packet.ParamSize = static_cast<uint8_t>(sizeof(packet.Places));
84+
packNibbles(packet.Places, places);
85+
}
86+
87+
END::END()
88+
{
89+
auto& packet = this->data();
90+
91+
packet.Mode = 5;
92+
}
93+
94+
} // namespace GP_SERV_COMMAND_CHOCOBO_RACING

0 commit comments

Comments
 (0)