LandSandBoat / server

:sailboat: LandSandBoat - a server emulator for Final Fantasy XI
https://landsandboat.github.io/server/
GNU General Public License v3.0
294 stars 589 forks source link

🔨 Chocobo Racing Research & Design & Implementation Tracking #5921

Open zach2good opened 3 months ago

zach2good commented 3 months ago

I affirm:

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

Scoreboard / Paddock

Racing

Misc

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

atom0s commented 3 months ago

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:

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:

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:

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