🔨 Chocobo Racing Research & Design & Implementation Tracking
I affirm:
- [x] I understand that if I do not agree to the following points by completing the checkboxes my issue will be ignored.
- [x] I have read and understood the Contributing Guide and the Code of Conduct.
- [x] I have searched existing issues to see if the issue has already been opened, and I have checked the commit log to see if the issue has been resolved since my server was last updated.
Describe the feature
As of https://github.com/LandSandBoat/server/pull/5920 we have race playout. So now it's possible to start pulling apart those data packets and plug in things for races. It's likely going to be horrendously boring to do by hand, but that comes with the territory of anything to do with Chocobo Minigames...
Advice
(thanks atom0s!) Use Ashita v4 and set /fps 0, turn down resolution and as many settings as possible to maximise FPS so races play out faster.
Chocobo Circuit
- [ ] Register to go into the grandstand and be eligible to see the race
- [ ] Crowd NPCs and their betting tickets
- [ ] The race timing cycle
Scoreboard / Paddock
- [ ] Start figuring out the patterns for the data
- [ ] NPC racers, their skills, history, etc. (before eventual integration with player Chocobos)
Racing
- [x] Properly list Chocobo names (https://github.com/LandSandBoat/server/pull/5923)
- [x] Chocobo & rider look data (https://github.com/LandSandBoat/server/pull/5923)
- [ ] Extended Chocobo look data based on their stats (Feet, crests, beak, etc.)
- [ ] Extended Racer looks, are the racers actually random apart from their race?
- [ ] Race events
- [ ] A timer to make sure you can't packet inject and end the race early for yourself
Misc
- [ ] Once the packets are mostly/completely figured out, submit their details to
VieweD
Links
Scoreboard data: https://github.com/atom0s/XiPackets/tree/main/world/server/0x0074 Race data: https://github.com/atom0s/XiPackets/blob/main/world/server/0x0069/README.md
In case it is useful to anyone else that may look into and/or work on the Chocobo Racing system, here is the information I shared with Zach from my reversing of the client that is related to the racing system.
Please Note: Things marked as Unknown in any of the below reversed pseudocode means that I have not personally validated its purpose and have not been given a name at this time. While some things may have a assumed/guessed purpose, it is not named in my stuff until I can hand-test and validate to ensure its meaning.
Client Packet Handling
The client makes use of the following packets in relation to the Chocobo Racing system:
C2S-0x009B- Scoreboard RequestS2C-0x0069- Race Information Update(s)S2C-0x0074- Scoreboard Information Update(s)
Scoreboard Handling
The client can make a request to the server to see the current scoreboard by using the 0x009B packet. This request is made using the following:
char __cdecl sub_100EA560(uint32_t param, void* callback, uint32_t cbparam)
{
if ( !PTR_pGlobalNowZone )
return 0;
auto pkt = (packet_c2s_09B_t *)FUNC_gcZoneSendQueSearch(0x9B, 0, 0);
if ( !pkt )
return 0;
pkt->Param = param;
pkt->Kind = 2;
FUNC_gcZoneSendQueSet((EN_QUE *)pkt, 0x0C, 0);
PTR_pGlobalNowZone->ChocoboRacingSys.Unknown13_Func = callback;
PTR_pGlobalNowZone->ChocoboRacingSys.Unknown13_FuncParam = cbparam;
return 1;
}
This function is invoked when the client first opens the scoreboard race card window (CTkChocoboRaceCard). The callback function is invoked upon receiving the 0x0074 packet response from the server which holds the updated scoreboard data, when all data has been populated.
The incoming 0x00074 packet handler from the server looks like this:
char __cdecl FUNC_Packet_Incoming_0x0074(GC_ZONE *zone, GP_GAME_PACKET_HEAD *head, uint8_t *pkt)
{
if ( !PTR_pGlobalNowZone )
return 1;
switch ( pkt[16] )
{
case 1u:
memset(PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11, 0, sizeof(PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11));
*(_DWORD *)&PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11[12] = *((_DWORD *)pkt + 5);
*(_DWORD *)&PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11[16] = *((_DWORD *)pkt + 6);
return 1;
case 2u:
qmemcpy(&PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11[12 * pkt[17] + 20], pkt + 20, 0x60u);
return 1;
case 3u:
qmemcpy(&PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11[20 * pkt[17] + 116], pkt + 20, 0xA0u);
return 1;
case 4u:
*(_DWORD *)PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11 = *((_DWORD *)pkt + 1);
*(_DWORD *)&PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11[4] = *((_DWORD *)pkt + 2);
*(_WORD *)&PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11[8] = *((_WORD *)pkt + 6);
PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11[10] |= 0x10u;
if ( PTR_pGlobalNowZone->ChocoboRacingSys.Unknown13_Func )
PTR_pGlobalNowZone->ChocoboRacingSys.Unknown13_Func(PTR_pGlobalNowZone->ChocoboRacingSys.Unknown13_FuncParam, PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11);
return 1;
}
return 1;
}
The actual callback function simply updates a few values inside of the race card window:
// a1 = The CTkChocoboRaceCard object instance.
// a2 = The PTR_pGlobalNowZone->ChocoboRacingSys.Unknown11 buffer.
int __cdecl sub_10203960(int a1, int a2)
{
int result; // eax
result = a2 + 20;
*(_DWORD *)(a1 + 36) = a2 + 116;
*(_DWORD *)(a1 + 32) = a2 + 20;
return result;
}
And then the actual window renderer that makes use of this data looks like this:
void __thiscall FUNC_CTkChocoboRaceCard_OnDrawPrimitive(int this)
{
int v2; // ebp
int v3; // ebx
int i; // edi
if ( *(_DWORD *)(this + 36) && *(_DWORD *)(this + 32) )
{
v2 = 0;
v3 = 0;
for ( i = 0; i < 96; i += 12 )
{
sub_102039B0(*(__int16 *)(this + 20), v2 + *(__int16 *)(this + 22), (_DWORD *)(v3 + *(_DWORD *)(this + 36)), i + *(_DWORD *)(this + 32));
v3 += 20;
v2 += 30;
}
}
}
void __stdcall sub_102039B0(int a1, int a2, _DWORD *a3, int a4)
{
_DWORD *MenuRes; // esi
int v5; // ebp
unsigned int v6; // eax
MenuRes = (_DWORD *)FUNC_GetMenuRes(2u);
if ( MenuRes )
{
FUNC_YkDrawString(a1 + 32, a2 + 5, (int)(a3 + 1), 0x80808080);
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * ((*(_DWORD *)(a4 + 4) >> 5) & 7) + 1820), a1 + 58, a2 + 18, 0x80808080, 0, 0);
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * (*(unsigned __int8 *)(a4 + 5) >> 5) + 1820), a1 + 104, a2 + 18, 0x80808080, 0, 0);
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * (*(_BYTE *)(a4 + 6) >> 5) + 1820), a1 + 153, a2 + 18, 0x80808080, 0, 0);
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * (*(unsigned __int8 *)(a4 + 7) >> 5) + 1820), a1 + 203, a2 + 18, 0x80808080, 0, 0);
if ( (*a3 & 3) != 0 )
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * (*a3 & 3) + 1736), a1 + 224, a2 + 8, 0x80808080, 0, 0);
v5 = a2 + 8;
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * ((*(_DWORD *)(a4 + 8) >> 25) & 7) + 1756), a1 + 240, a2 + 8, 0x80808080, 0, 0);
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * (*(_BYTE *)(a4 + 11) & 1) + 1776), a1 + 240, a2 + 8, 0x80808080, 0, 0);
v6 = (*(_DWORD *)(a4 + 8) >> 20) & 0xF;
if ( v6 && v6 <= 8 )
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * v6 + 1848), a1 + 256, v5, 0x80808080, 0, 0);
if ( ((*(_DWORD *)(a4 + 8) >> 9) & 0xF) != 0 )
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * ((*(_DWORD *)(a4 + 8) >> 9) & 0xF) + 1792), a1 + 276, a2 + 7, 0x80808080, 0, 0);
if ( ((*(_DWORD *)(a4 + 8) >> 13) & 0xF) != 0 )
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * ((*(_DWORD *)(a4 + 8) >> 13) & 0xF) + 1792), a1 + 276, a2 + 17, 0x80808080, 0, 0);
FUNC_YmMenuShape_Draw(*(_DWORD **)(*MenuRes + 4 * (*(_DWORD *)a4 & 0xF) + 1784), a1 + 333, v5, 0x80808080, 0, 0);
}
}
For more information on how these packet layouts look, please see:
0x009B- https://github.com/atom0s/XiPackets/tree/main/world/client/0x009B0x0074- https://github.com/atom0s/XiPackets/tree/main/world/server/0x0074
Race Information Updates
The other packet used with this system is the race information update packet 0x0069. The server will send multiple instances of this packet to fully populate the various data about the race. This includes:
RaceParamsChocoboParamsSectionParams
The packet handler for this looks like:
char __cdecl FUNC_Packet_Incoming_0x0069(GC_ZONE *zone, GP_GAME_PACKET_HEAD *head, uint8_t *pkt)
{
switch ( pkt[4] )
{
case 1u:
zone->ChocoboRacingSys.DownloadFlg = 0;
*(_QWORD *)zone->ChocoboRacingSys.RaceParams = *((_QWORD *)pkt + 1);
return 1;
case 2u:
zone->ChocoboRacingSys.DownloadFlg = 0;
qmemcpy(&zone->ChocoboRacingSys.ChocoboParams[3 * pkt[5]], pkt + 8, pkt[6]);
return 1;
case 3u:
zone->ChocoboRacingSys.DownloadFlg = 0;
qmemcpy(&zone->ChocoboRacingSys.SectionParams[3 * pkt[5]], pkt + 8, pkt[6]);
return 1;
case 4u:
zone->ChocoboRacingSys.DownloadFlg = 0;
qmemcpy(&zone->ChocoboRacingSys.ResultParams, pkt + 8, 4 * (pkt[6] >> 2));
qmemcpy(&zone->ChocoboRacingSys.SectionParams[(pkt[6] >> 2) + 96], &pkt[4 * (pkt[6] >> 2) + 8], pkt[6] & 3);
return 1;
default:
zone->ChocoboRacingSys.DownloadFlg = 1;
break;
}
return 1;
}
When populating the Chocobo system information, the server will send multiple 0x0069 packets to update the various bits of data needed that have changed. If the packet mode is 1, 2, 3 or 4 then the system will be marked as 'not ready' until a final packet is received using a different mode value which will trigger the default handler and mark the system as ready.
For more information on how this packet layout looks, please see: https://github.com/atom0s/XiPackets/tree/main/world/server/0x0069
Chocobo Race Event Handling
Aside from the card window handling shown above, this systems' data is also mainly used within the event VM system. The main opcode this system uses is the opcode 0x00BF. This opcode is used to load and push various data from the Chocobo system into the event VM to be used with other opcodes.
You can find more information about this opcode handler here: https://github.com/atom0s/XiEvents/blob/main/OpCodes/0x00BF.md