Difference between revisions of "LEX (File Format)/C example"

From Custom Mario Kart
Jump to navigation Jump to search
(Add LE-CODE implementation source for some of the sections.)
 
(2 intermediate revisions by 2 users not shown)
Line 1: Line 1:
[[File:lecode-ver-36x160.png|right]]
+
[[File:LE-CODE Logo (Vertical).png|right]]
This page shows example code in C for »LEX (File Format)«
 
  
 +
While the specifications for [[LEX]] files have always been publicly available here on Tockdom some people expressed concerns about implementation differences when implemening LEX themselves, so we're making the LEX-related code parts from LE-CODE available here. All the code on this particular Tockdom page is under the MIT license "Copyright 2023 Wiimm & Leseratte" so there should be no licensing issues putting it into any kind of distribution, open-source or closed-source.
  
 
__TOC__
 
__TOC__
  
 +
== Structures ==
  
== Structures ==
 
 
=== lex_header_t ===
 
=== lex_header_t ===
 
<pre>
 
<pre>
Line 26: Line 26:
 
{
 
{
 
     u32 magic; // identification of section
 
     u32 magic; // identification of section
     uint size; // size of 'data' (this header excluded)
+
     u32 size; // size of 'data' (this header excluded)
 
     u8 data[]; // section data, 32-bit aligned
 
     u8 data[]; // section data, 32-bit aligned
 
}
 
}
Line 32: Line 32:
 
</pre>
 
</pre>
  
== Function Headers ==
+
 
 +
== Section-independant code ==
 +
 
 +
The main hook for LEX files is at 80512820, just before a track's KMP file is getting loaded.
 +
In that place we open the LEX file (if present), reset all modifications previously done (lex_file_reset), then start parsing the lex file (lex_file_parse).
  
 
<pre>
 
<pre>
#define LEX_MAJOR_VERSION 1
+
    lex_ptr = open_szs_subfile(file_open_data_addr, 1, "course.lex", 0); // call to 805411fc
#define LEX_MINOR_VERSION 0
+
    lex_file_reset();
#define LEX_BIN_MAGIC "LE-X"
+
    lex_file_parse();
 +
    set_online_time_limit_by_lex(lex_set1_data.apply_online_sec);
 +
</pre>
 +
 
 +
The lex_file_parse sets up various global variables that are then checked by other hooks around the code whenever needed (and has special handling for the cannon subsection CANN because of legacy reasons):
 +
 
 +
<spoiler text="void lex_file_parse()">
 +
<pre>
 +
void lex_file_parse()
 +
{
 +
    if ( !lex_ptr || isModeWorldwide() )
 +
        return;
  
typedef enumError (*lex_stream_func)
+
    if (lex_ptr->magic != 0x4c452d58) {
( u32 magic, const u8 *data, uint size, cvp user_ptr );
+
        LEReport("This is not a valid course.lex file\n");
 +
        return;
 +
    }
  
enumError ScanLexFile
+
    if (lex_ptr->major_version != 1) {
( const void *data, uint data_size, lex_stream_func func, const void *user_ptr );
+
        LEReport("course.lex file has invalid version (%d), need %d\n", lex_ptr->major_version, 1);
 +
        return;
 +
    }
  
enumError ScanLexStream
+
    lex_element_t * lex_element_ptr = (lex_element_t *)((int)((int)lex_ptr + (int)lex_ptr->element_off));
( const void *data, uint data_size, lex_stream_func func, const void *user_ptr );
+
 
 +
    void * object_data_ptr;
 +
 
 +
    while (lex_element_ptr->magic != 0)
 +
    {
 +
        object_data_ptr = (void *)((int)lex_element_ptr + 8);
 +
 
 +
        switch (lex_element_ptr->magic)
 +
        {
 +
            case 0x43414e4e: // "CANN": Cannon settings.
 +
                // object data ptr contains offset to cannon data
 +
                // The game stores cannon speeds in a float[4] for each cannon type which is at PAL 808b5ce8.
 +
                // This array is loaded / used at PAL 805850a4, in a set of lis/addi instructions.
 +
                // We're just patching these, so cannon data is loaded from the temporary LEX buffer instead.
 +
                object_data_ptr = (void *)(((int)object_data_ptr) + 4); // skip size
 +
                code_patcher(&mkwfun_lex_cannon_type_patch_addr, 0, 0x3c600000 + (((unsigned int)object_data_ptr >> 16) & 0xffff));
 +
                code_patcher(&mkwfun_lex_cannon_type_patch_addr, 4, 0x60630000 + ((unsigned int)object_data_ptr & 0xffff));
 +
                break;
 +
 
 +
            case 0x48495054: // "HIPT": HIde Position Tracker
 +
                lex_hipt_len = lex_element_ptr->size / sizeof(*lex_hipt_list);
 +
                lex_hipt_list = object_data_ptr;
 +
                LEDebugReport("XPF: load LEX/HIPT, size=%d -> n=%d\n", lex_element_ptr->size, lex_hipt_len);
 +
                break;
 +
 
 +
            case 0x52495450: // "RITP": Random ITem Points
 +
                lex_ritp_len = lex_element_ptr->size / sizeof(*lex_ritp_list);
 +
                lex_ritp_list = object_data_ptr;
 +
                LEDebugReport("XPF: load LEX/RITP, size=%d -> n=%d\n", lex_element_ptr->size, lex_ritp_len);
 +
                break;
 +
 
 +
 
 +
            case 0x53455431: // "SET1": Settings #1
 +
{
 +
    unsigned size = lex_element_ptr->size;
 +
    LEDebugReport("load LEX/SET1, size=%d, max-size=%d\n",
 +
size, sizeof(lex_set1_t));
 +
    if (size > sizeof(lex_set1_t))
 +
size = sizeof(lex_set1_t);
 +
    memcpy(&lex_set1_data, object_data_ptr, size);
 +
}
 +
break;
 +
 
 +
            case 0x54455354: // "TEST": Settings to tests.
 +
{
 +
    unsigned size = lex_element_ptr->size;
 +
    LEDebugReport("XPF: load LEX/TEST, size=%d, max-size=%d\n", size, sizeof(lex_test_t));
 +
    if (size > sizeof(lex_test_t))
 +
size = sizeof(lex_test_t);
 +
    memcpy(&lex_test_data, object_data_ptr, size);
 +
}
 +
break;
 +
 
 +
            // case 0x2d2d2d2d: // "----", invalidated section, ignore.
 +
            default: // ignore unknown sections
 +
                break;
 +
        }
 +
 
 +
        // go to next element:
 +
        lex_element_ptr = (lex_element_t *)((int)lex_element_ptr + lex_element_ptr->size + 8);
 +
    }
 +
 
 +
    lex_ptr = 0;
 +
}
 
</pre>
 
</pre>
 +
</spoiler>
 +
 +
The lex_file_reset function is called every time a new track loads as well, and resets all modifications done by a LEX file so the next track starts with a clean state. The cannon speed modifications will be directly reset here, any other modifications just set a bunch of global variables to their proper state and the functions hooking into the game code will react accordingly:
  
== Scanning Functions ==
+
<spoiler text="void lex_file_reset()">
=== ScanLexFile() ===
 
 
<pre>
 
<pre>
enumError ScanLexFile
+
const lex_dev1_t lex_dev1_data_reset = { 0 };
( const void *data, uint data_size, lex_stream_func func, const void *user_ptr )
+
const lex_set1_t lex_set1_data_reset = { { 1.0, 1.0, 1.0 }, 0, 0, 0 };
 +
const lex_test_t lex_test_data_reset = { 0, 0, 0, -1, 0, 0 };
 +
 
 +
void lex_file_reset()
 
{
 
{
    if ( !data || data_size < sizeof(lex_header_t) )
+
        // reset cannon param (PAL 805850a4)
return ERR_INVALID_DATA;
+
        #if REGION_LETTER == 'P'
 +
            code_patcher(&mkwfun_lex_cannon_type_patch_addr, 0, 0x3c60808b);
 +
            code_patcher(&mkwfun_lex_cannon_type_patch_addr, 4, 0x38635ce8);
 +
        #elif REGION_LETTER == 'E'
 +
            code_patcher(&mkwfun_lex_cannon_type_patch_addr, 0, 0x3c60808b);
 +
            code_patcher(&mkwfun_lex_cannon_type_patch_addr, 4, 0x38631428);
 +
        #elif REGION_LETTER == 'J'
 +
            code_patcher(&mkwfun_lex_cannon_type_patch_addr, 0, 0x3c60808b);
 +
            code_patcher(&mkwfun_lex_cannon_type_patch_addr, 4, 0x38634e48);
 +
        #elif REGION_LETTER == 'K'
 +
            code_patcher(&mkwfun_lex_cannon_type_patch_addr, 0, 0x3c60808a);
 +
            code_patcher(&mkwfun_lex_cannon_type_patch_addr, 4, 0x38634160);
 +
        #else
 +
            #error "no region"
 +
        #endif
 +
 
 +
            // reset LEX data
 +
        // [[lex-dev]]
 +
            lex_dev1_data = lex_dev1_data_reset;
 +
            lex_set1_data = lex_set1_data_reset;
 +
            lex_test_data = lex_test_data_reset;
 +
            lex_hipt_len = 0;
 +
            lex_ritp_len = 0;
 +
            *lex_ritp_log = 0;
 +
}
 +
 
 +
</pre>
 +
</spoiler>
 +
 
 +
 
 +
== Section-dependant code ==
 +
 
 +
Nearly every LEX section has dedicated hooks and code in the LE-CODE.
 +
 
 +
=== Dedicated code for the CANN, FEAT and CTDN sections ===
 +
 
 +
The [[LEX_(File_Format)#FEAT|FEAT section]] of a LEX file is a pure metadata section. LE-CODE does not contain any implementation code for this section. This section was requested by MrBean as a kind of signal to CTGP (or other distributions) which particular special features (LEX or otherwise) a track uses. The full spec of this section will eventually be put on Tockdom as well when it's fully finished; the state in progress can be found in the source code of the SZS tools.
 +
 
 +
The [[LEX_(File_Format)#CANN|CANN section]] also has no dedicated code elsewhere, the code for the cannon speeds is already included in the parser above.
 +
 
 +
The [[LEX_(File_Format)#CTDN|CTDN section]] is not used at all in the LE-CODE, it's only useful for distributions that support the Countdown race mode. It was requested by [[User:Kazuki|Kazuki]] and you may find a proper implementation at [[Countdown Mode Beta8 Mod]].
 +
 
 +
=== Dedicated code for the SET1 section ===
 +
 
 +
The [[LEX_(File_Format)#SET1|SET1 section]] of a LEX file contains multiple short settings that don't really need their own section. This section will probably be extended in the future, but only in a compatible way.
 +
 
 +
As for the currently defined parameters, `START-ITEM` is only metadata that can be used by external tools, and `ITEM-POS-FACTOR` and `APPLY-ONLINE-SEC` are implemented in the LE-CODE.
 +
 
 +
<spoiler text="ITEM-POS-FACTOR">
 +
 
 +
In online races, items will only work within the coordinates of around ±131000, because there's only so many bits in the network protocol for the item position. With this setting, a track creator can define the coordinate stretch factors. By setting that to 2, each coordinate will be divided by 2 prior to sending it over the network, and multiplied by 2 after receiving it from the network. This reduces the accuracy of item coordinates, but it makes them work on the whole map.
 +
 
 +
The code for that is written in Assembly:
 +
 
 +
<pre>
 +
.extern lex_set1_data;
 +
.globl itemrange_receive_X_mod
  
     const lex_header_t *hd = (lex_header_t*)data;
+
itemrange_receive_X_mod:
     if (memcmp(hd->magic,LEX_BIN_MAGIC,4))
+
    lis r5, lex_set1_data@ha;
return ERR_INVALID_DATA;
+
     lfs f1, lex_set1_data@l(r5); // load factor
 +
     fmuls f0, f0, f1; // multiply f0 with factor
 +
    stfs f0, 0(r4); // store new value
 +
    blr;  
  
     if ( ntohs(hd->major_version) != LEX_MAJOR_VERSION )
+
.globl itemrange_send_X_mod
return ERR_INVALID_VERSION;
+
itemrange_send_X_mod:
 +
    lis r5, lex_set1_data@ha;
 +
     lfs f1, lex_set1_data@l(r5); // load factor
 +
    lfs f0, 0(r4); // load original value to send
 +
    fdivs f0, f0, f1; // divide by factor
 +
    blr;  
  
     const uint size = ntohl(hd->size);
+
.globl itemrange_receive_Y_mod
     const uint off  = ntohl(hd->stream_off);
+
itemrange_receive_Y_mod:
     if ( (size&3) || size > data_size || (off&3) || off + 4 > size )
+
    lis r5, (lex_set1_data+4)@ha;
return ERR_INVALID_DATA;
+
    lfs f1, (lex_set1_data+4)@l(r5); // load factor
 +
    fmuls f0, f0, f1; // multiply f0 with factor
 +
    stfs f0, 0(r4); // store new value
 +
    blr;
 +
      
 +
.globl itemrange_send_Y_mod
 +
itemrange_send_Y_mod:
 +
    lis r5, (lex_set1_data+4)@ha;
 +
    lfs f1, (lex_set1_data+4)@l(r5); // load factor
 +
    lfs f0, 0(r4); // load original value to send
 +
    fdivs f0, f0, f1; // divide by factor
 +
     blr;
 +
   
 +
   
 +
.globl itemrange_receive_Z_mod
 +
itemrange_receive_Z_mod:
 +
    lis r5, (lex_set1_data+8)@ha;
 +
     lfs f1, (lex_set1_data+8)@l(r5); // load factor
 +
    fmuls f0, f0, f1; // multiply f0 with factor
 +
    stfs f0, 0(r4); // store new value
 +
    blr;
 +
 
 +
 
 +
.globl itemrange_send_Z_mod
 +
itemrange_send_Z_mod:
 +
    lis r5, (lex_set1_data+8)@ha;
 +
    lfs f1, (lex_set1_data+8)@l(r5); // load factor
 +
    lfs f0, 0(r4); // load original value to send
 +
    fdivs f0, f0, f1; // divide by factor
 +
    blr;  
  
    return ScanLexElements((u8*)data+off,size-off,func,user_ptr);
 
}
 
 
</pre>
 
</pre>
  
=== ScanLexElements() ===
+
These are the patches necessary to trigger the above ASM code:
 +
 
 
<pre>
 
<pre>
enumError ScanLexElements
+
    // fix for receiving items:
( const void *data, uint data_size, lex_stream_func func, const void *user_ptr )
+
    // shot_drop is at PAL 0x8079b4ac
{
+
    code_patcher_BL(&EVENTDATA_item_position_SHOT_DROP, 0x1ac, &itemrange_receive_X_mod); // X
     if ( !data || !data_size || (data_size&3) )
+
    code_patcher_BL(&EVENTDATA_item_position_SHOT_DROP, 0x2b0, &itemrange_receive_Y_mod); // Y
return ERR_INVALID_DATA;
+
    code_patcher_BL(&EVENTDATA_item_position_SHOT_DROP, 0x3b0, &itemrange_receive_Z_mod); // Z
 +
 
 +
    // tail_destroy is at PAL 0x8079c960
 +
    code_patcher_BL(&EVENTDATA_item_position_TAIL_DESTROY, 0x1ac, &itemrange_receive_X_mod); // X
 +
    code_patcher_BL(&EVENTDATA_item_position_TAIL_DESTROY, 0x2b0, &itemrange_receive_Y_mod); // Y
 +
    code_patcher_BL(&EVENTDATA_item_position_TAIL_DESTROY, 0x3b0, &itemrange_receive_Z_mod); // Z
 +
 
 +
 
 +
    // fix for sending items:
 +
    code_patcher_BL(&EVENTDATA_item_position_SHOT_DROP, 0x13c, &itemrange_send_X_mod); // X
 +
    code_patcher_BL(&EVENTDATA_item_position_SHOT_DROP, 0x23c, &itemrange_send_Y_mod); // Y
 +
     code_patcher_BL(&EVENTDATA_item_position_SHOT_DROP, 0x33c, &itemrange_send_Z_mod); // Z
 +
 
 +
    code_patcher_BL(&EVENTDATA_item_position_TAIL_DESTROY, 0x13c, &itemrange_send_X_mod); // X
 +
    code_patcher_BL(&EVENTDATA_item_position_TAIL_DESTROY, 0x23c, &itemrange_send_Y_mod); // Y
 +
    code_patcher_BL(&EVENTDATA_item_position_TAIL_DESTROY, 0x33c, &itemrange_send_Z_mod); // Z
 +
</pre>
 +
 
 +
 
 +
 
 +
</spoiler>
  
    enumError max_err = ERR_OK;
+
<spoiler text="APPLY-ONLINE-SEC">
  
    uint off = 0;
+
The time limit for online races is set at PAL 8053f3b8 - r3 ends up being the time limit in milliseconds. The `set_online_time_limit_by_lex` that's called after parsing the LEX file. It modifes the existing ASM instructions at this address to modify the timeout value to the value set in the LEX file data:
    while ( off < data_size )
 
    {
 
const lex_element_t *s = (lex_element_t*)( (u8*)data + off );
 
  
const u32 magic = ntohl(s->magic);
+
<pre>
if (!magic)
+
    // Convert to milliseconds and split to hi and lo word
    break;
+
    const unsigned int time = seconds * 1000;
 +
    const unsigned short hi = (time >> 16) & 0xffff;
 +
    const unsigned short lo = (time & 0xffff);
  
const uint size = ntohl(s->size);
+
    // Create ASM instructions
off += sizeof(lex_element_t) + size;
+
    const unsigned int asm_hi = 0x3c600000 | hi; // lis r3, time@h;
if ( (size&3) || off > data_size )
+
    const unsigned int asm_lo = 0x60640000 | lo; // ori r4, r3, time@l;
    return ERR_INVALID_DATA;
 
  
if (func)
+
    // Patch
{
+
    code_patcher(&mod_online_time_limit_addr, 0, asm_hi);
    const int err = func(magic,s->data,size,user_ptr);
+
    code_patcher(&mod_online_time_limit_addr, 4, asm_lo);
    if (err<0)
 
return -err;
 
    if ( max_err < err )
 
max_err = err;
 
}
 
    }
 
    return max_err;
 
}
 
 
</pre>
 
</pre>
  
 +
If necessary, the patch will also modify the timer watchdog at PAL 8053f474 (this is set to the time limit plus 30s). This prevents hard disconnections after a timer of 5:56 is reached.
 +
 +
</spoiler>
 +
 +
 +
=== Dedicated code for other sections ===
 +
 +
The LE-CODE also has dedicated code for the HIPT section (rules on when to hide the position tracker in a race), RITP section (rules to randomize which item routes are taken by Bullet Bills and Red shells) and TEST/DEV1 sections.
 +
 +
Implementations for the HIPT and RITP section in LE-CODE will be added later - these are more deeply integrated with the rest of the code than I had though so that'll take some more work to extract these.
 +
 +
Implementations of the TEST section aren't really useful without support for [[Extended presence flags]], so that code will also be added later.
 +
 +
The DEV1 section has no proper implementation - it's the section type we are using when adding new features (before defining an actual section) or for quick testing. It randomly changes whenever we need it to, and no actual released track should contain such a section.
  
 
[[category:C]]
 
[[category:C]]
 
[[category:LE-CODE]]
 
[[category:LE-CODE]]

Latest revision as of 07:12, 13 April 2024

LE-CODE Logo (Vertical).png

While the specifications for LEX files have always been publicly available here on Tockdom some people expressed concerns about implementation differences when implemening LEX themselves, so we're making the LEX-related code parts from LE-CODE available here. All the code on this particular Tockdom page is under the MIT license "Copyright 2023 Wiimm & Leseratte" so there should be no licensing issues putting it into any kind of distribution, open-source or closed-source.

Structures

lex_header_t

typedef struct lex_header_t
{
    char	magic[4];	// always LEX_BIN_MAGIC
    u16		major_version;	// usually LEX_MAJOR_VERSION
    u16		minor_version;	// usually LEX_MINOR_VERSION
    u32		size;		// size of this file (header+streams)
    u32		element_off;	// offset of first lex_element_t, 32-bit aligned
}
__attribute__ ((packed)) lex_header_t;

lex_element_t

typedef struct lex_element_t
{
    u32		magic;		// identification of section
    u32		size;		// size of 'data' (this header excluded)
    u8		data[];		// section data, 32-bit aligned
}
__attribute__ ((packed)) lex_element_t;


Section-independant code

The main hook for LEX files is at 80512820, just before a track's KMP file is getting loaded. In that place we open the LEX file (if present), reset all modifications previously done (lex_file_reset), then start parsing the lex file (lex_file_parse).

    lex_ptr = open_szs_subfile(file_open_data_addr, 1, "course.lex", 0); // call to 805411fc
    lex_file_reset();
    lex_file_parse();
    set_online_time_limit_by_lex(lex_set1_data.apply_online_sec);

The lex_file_parse sets up various global variables that are then checked by other hooks around the code whenever needed (and has special handling for the cannon subsection CANN because of legacy reasons):

void lex_file_parse()


The lex_file_reset function is called every time a new track loads as well, and resets all modifications done by a LEX file so the next track starts with a clean state. The cannon speed modifications will be directly reset here, any other modifications just set a bunch of global variables to their proper state and the functions hooking into the game code will react accordingly:

void lex_file_reset()


Section-dependant code

Nearly every LEX section has dedicated hooks and code in the LE-CODE.

Dedicated code for the CANN, FEAT and CTDN sections

The FEAT section of a LEX file is a pure metadata section. LE-CODE does not contain any implementation code for this section. This section was requested by MrBean as a kind of signal to CTGP (or other distributions) which particular special features (LEX or otherwise) a track uses. The full spec of this section will eventually be put on Tockdom as well when it's fully finished; the state in progress can be found in the source code of the SZS tools.

The CANN section also has no dedicated code elsewhere, the code for the cannon speeds is already included in the parser above.

The CTDN section is not used at all in the LE-CODE, it's only useful for distributions that support the Countdown race mode. It was requested by Kazuki and you may find a proper implementation at Countdown Mode Beta8 Mod.

Dedicated code for the SET1 section

The SET1 section of a LEX file contains multiple short settings that don't really need their own section. This section will probably be extended in the future, but only in a compatible way.

As for the currently defined parameters, `START-ITEM` is only metadata that can be used by external tools, and `ITEM-POS-FACTOR` and `APPLY-ONLINE-SEC` are implemented in the LE-CODE.

ITEM-POS-FACTOR


APPLY-ONLINE-SEC


Dedicated code for other sections

The LE-CODE also has dedicated code for the HIPT section (rules on when to hide the position tracker in a race), RITP section (rules to randomize which item routes are taken by Bullet Bills and Red shells) and TEST/DEV1 sections.

Implementations for the HIPT and RITP section in LE-CODE will be added later - these are more deeply integrated with the rest of the code than I had though so that'll take some more work to extract these.

Implementations of the TEST section aren't really useful without support for Extended presence flags, so that code will also be added later.

The DEV1 section has no proper implementation - it's the section type we are using when adding new features (before defining an actual section) or for quick testing. It randomly changes whenever we need it to, and no actual released track should contain such a section.