If you appreciate the work done within the wiki, please consider supporting The Cutting Room Floor on Patreon. Thanks for all your support!

Quake (2021)

From The Cutting Room Floor
Jump to navigation Jump to search

Title Screen

Quake

Developers: id Software, Night Dive Studios, MachineGames
Publisher: Bethesda Softworks
Platforms: Windows, PlayStation 4, Xbox One, Nintendo Switch, PlayStation 5, Xbox Series X
Released internationally: August 20, 2021 (Win/PS4/XBO/Switch), October 12, 2021 (PS5/XBX)[1]


AreasIcon.png This game has unused areas.
SourceIcon.png This game has uncompiled source code.
GraphicsIcon.png This game has unused graphics.
ModelsIcon.png This game has unused models.
TextIcon.png This game has unused text.


Quake is a story about a homicidal man who likes to kill Lovecraft-inspired monsters through barely connected level sets. He also likes to murder his friends (who respawn as soon as they are killed) in castles that have a large Jesus crucified in them for some reason. This 25th Anniversary version adds improved graphics, a new expansion pack, cross-platform multiplayer and curated add-ons, including, uh, Quake 64. Oh yeah, and it's the first digital re-release of the game to finally include the Trent Reznor soundtrack.

Hmmm...
To do:
  • Look for anything unused in the Dimension of the Machine expansion.
  • Discuss game updates.

Test Levels

A number of test levels, largely intended for testing Bot AI behavior, are still present in the maps folder of id1/pak0.pak.

Hmmm...
To do:
Take new screenshots with sv_cheats 1 and nav_edit 1 enabled

test/mals_combatbox

Quake2021 mals combatbox.png

A square arena bordered by pillars, blocking the line of sight of various monsters seen from Episode 1, with two Enforcers right in front of the player back at the start. Some weapons, health and ammo are also present.

The "Mal" in the filename references John "Maleficus" Dean, an AI programmer at Id who wrote the remaster's bot system.

test/test_aiming

Quake2021 test aiming.png

A seemingly-empty, square map.

test/test_barrierjump

Quake2021 test barrierjump.png

A room with varyingly-tall barriers, leading up to a door-textured barrier you can't jump over. A Rocket Launcher sits in a second room beyond the barrier.

test/test_button

Quake2021 test button.png

A corridor with a door, and a button for opening it. On the other wise of the door is a Rocket Launcher with a pile of ammo.

test/test_characters

Quake2021 test characters.png

A very large, empty room with a Rocket Launcher and a pile of ammo in the center.

test/test_door

Quake2021 test door.png

Similar to test_button, but without the button. Instead, the door can be opened by shooting it.

test/test_invis

Quake2021 test invis.png

Similar to test_aiming, but littered with Rings of Shadows.

test/test_laser

Quake2021 test laser.png

An empty, unlit corridor with wall textures that have fullbright elements.

test/test_nodes

Quake2021 test nodes.png

An empty, rectangular room with a devil-head texture on one side of the room. Presumably used for testing bot AI.

test/test_obstacles

Quake2021 test obstacles.png

An obstacle course! Features various obstacles to jump over, several invisible teleporters for if you fail a jump, and a jump pad right at the end. Walking into the slipgate teleports you back to the start.

test/test_powerup

Quake2021 test powerup.png

An unlit, square room with two Quad Damages, and two Pentagrams of Protection, sitting ominously.

test/test_rockets

Quake2021 test rockets.png

An empty, rectangular room with a Rocket Launcher and a pile of ammo at one end.

test/test_shadow

Quake2021 test shadow.png

Two greyboxed areas showcasing the real-time lighting showcased in the Dimension of the Machine maps.

test/test_walkoffledge

Quake2021 test walkoffledge.png

A corridor with a short staircase with a ledge that drops down - not far enough to get hurt, but far enough that you can't jump back up naturally - leading to a Rocket Launcher and a pile of ammo.

test_ctf

Quake2021 test ctf.png

Included in the PAK file for the Threewave CTF mode added on 18 August 2022 (ctf/pak0.pak). Just an empty square room with two flags. No bot AI nodes are present. Weirdly, you always seem to start inside the ceiling/sky, stuck and unable to move.

Unused Localization Strings

Ending Text

As found in QuakeEX.kpf/localization/loc_english.txt...

qc_finale_e1_shareware = "As the corpse of the monstrous entity\nChthon sinks back into the lava whence\nit rose, you grip the Rune of Earth\nMagic tightly. Now that you have\nconquered the Dimension of the Doomed,\nrealm of Earth Magic, you are ready to\ncomplete your task in the other three\nhaunted lands of Quake. Or are you? If\nyou don't register Quake, you'll never\nknow what awaits you in the Realm of\nBlack Magic, the Netherworld, and the\nElder World!"

The alternate version of Episode 1's finale from the original Shareware demo. The 2021 remaster does not have a demo, therefore...

map_unregistered = "You haven't registered Quake!\n\nCall 1-800-idgames to unlock\nthe full game from CD-ROM\nor for mail delivery."

Another shareware hangover from the START map.

qc_ks_dragon = "{0} was fried by a Dragon\n"

The unused obituary in the original Quake for getting killed by the unused Dragon enemy. Filed under strings for Scourge of Armagon, for some reason, despite Scourge not having a dragon.

qc_ks_shub = "{0} became one with Shub-Niggurath\n"

The unused obituary in the original Quake for somehow getting killed by the final boss, whose damage is instead handled by a generic trigger.

Interface Strings

Various strings intended for the menus and the Bethesda.net UI will never surface. Some of these strings were also unused in the 2020 Doom 64 remaster.

The separated game thread/render thread architecture of the KEX Engine was removed during development of Quake, rendering this would-be menu option obsolete:

m_game_thread = "Threaded Game Renderer"

An error intended to appear if a user's Bethesda.net email address could not be successfully retrieved. Instead the field will appear blank.

bnet_no_email           = "Could not retrieve email address"

Messages intended for a control that would auto-populate a user's email address based on console account information. This was never implemented and does not appear in either the Xbox One nor the PlayStation 4 version of the game.

bnet_qa_af_tooltip_xb   = "Pre-fill with your Microsoft Account email address for use in Bethesda.net account creation."
bnet_qa_af_tooltip_xs   = "Pre-fill with your Microsoft Account email address for use in Bethesda.net account creation."
bnet_qa_af_tooltip_ws   = "Pre-fill with your Microsoft Account email address for use in Bethesda.net account creation."
bnet_qa_af_tooltip_ps   = "Use your Account for PlayStation™Network email address. Your data will be sent to ZeniMax Media in the United States and used to link your account for PlayStation™Network to your Bethesda.net account."
bnet_qa_af_tooltip_pr   = "Use your Account for PlayStation™Network email address. Your data will be sent to ZeniMax Media in the United States and used to link your account for PlayStation™Network to your Bethesda.net account."

Strings used by the Slayers Club integration feature of Doom 64, which is not used by Quake; instead, rewards such as the Doom Eternal Quake skin are unlocked automatically when the user links their account.

bnet_sc_title           = "SLAYERS CLUB"
bnet_sc_body            = "Thanks for logging into Bethesda.net! As a Slayers Club member you'll receive exclusive in-game items for DOOM Eternal. Choose the button button below to claim your club reward."
bnet_sc_redeem          = "Claim Your Reward"
bnet_sc_success         = "You're all set. Your exclusive digital items have been unlocked. Thanks for playing!"

Strings for a dialog invoked by a Bethesda Store DRM check, which is not used by Quake, even in the Bethesda Store version:

bnet_error_drm          = "This game requires validation.\n\nPlease log in to the Bethesda.net Launcher with an active internet connection and try again."
bnet_error_if_persists  = "If the problem persists, please visit https://help.bethesda.net.
bnet_error_validation   = "Validation Required"
bnet_retry              = "Retry"
bnet_exit_game          = "Exit Game"

Strings for a profile conflict resolution dialog, which is never evoked; profile resolution occurs automatically and silently in the background instead:

bnet_profile_merge_prof1   = "PROFILE 1"
bnet_profile_merge_prof2   = "PROFILE 2"
bnet_profile_merge_time    = "Time"
bnet_profile_merge_points  = "Points"
bnet_profile_merge_title   = "SELECT SESSION"
bnet_profile_merge_desc    = "Bethesda.net has detected an existing session for this profile. Please select the session you wish to keep. Information from the other session will be lost."
bnet_profile_merge_confirm = "CONFIRM SELECTION"

Unused Models

Hmmm...
To do:
Add renders

The remastered models in id1/pak0.pak/progs also include models for the health and ammo pickups. However, these don't appear to be loaded by the game, instead using their original BSP-based forms.

Of note is that the health boxes have their design slightly altered in these remasters, to replace the red cross with a similarly-sized glowing square. The health boxes in the final release are not altered, apparently due to a quirk of the Red Cross's copyright on their design (Apparently it has to be on a white background!)

Texture WADs

Hmmm...
To do:
Scour for any other unused junk

A selection of texture WADs (used for level editors - BSP files embed their textures when compiled for ease of distribution) were mistakenly included in the PAK file and are present in id1/pak0.pak/gfx:

  • all.wad
  • base.wad
  • items.wad
  • jr_med.wad
  • medieval.wad
  • metal.wad
  • rogue.wad
  • start.wad
  • textures.wad
  • tim.wad
  • wizard.wad

Quake2021-itemsbackpack.png This unused placeholder texture, "backpack", is present in items.wad, and consists of a slightly squished Doom backpack, presumably from when the backpack item was still a BSP box instead of a .mdl model.

Source Code

In the included Quake 64 mod, q64/pak0.pak/src.txt contains a piece of source code for what appears to be an internal utility to read and work with the Quake 64 ROM data.


#include "kexlib.h"
#include "quakeex.h"

/*
=============================================================================

DECOMPRESSION

=============================================================================
*/

int         wad_numlumps2;
lumpinfo_t*  wad_lumps2;
byte*        wad_base2;

/*=======*/
/* TYPES */
/*=======*/

typedef struct {
    int var0;
    int var1;
    int var2;
    int var3;
    kexArray<uint8_t> write;
    byte *read;
    byte *readPos;
} decoder_t;

/*=========*/
/* GLOBALS */
/*=========*/

static short ShiftTable[6] = {4, 6, 8, 10, 12, 14}; // 8005D8A0

static int tableVar01[18];      // 800B2250

static short *PtrEvenTbl;       // 800B2298
static short *PtrOddTbl;        // 800B229C
static short *PtrNumTbl1;       // 800B22A0
static short *PtrNumTbl2;       // 800B22A4

static short DecodeTable[2524]; // 800B22A8
static short array01[1258];     // 800B3660

static decoder_t decoder;       // 800B4034
static byte *allocPtr;          // 800B4054

static int OVERFLOW_READ;       // 800B4058
static int OVERFLOW_WRITE;      // 800B405C

/*
============================================================================

DECODE BASED ROUTINES

============================================================================
*/

/*
========================
=
= GetDecodeByte
=
========================
*/

static byte GetDecodeByte(void) // 8002D1D0
{
    if((int)(decoder.readPos - decoder.read) >= OVERFLOW_READ)
    {
        return -1;
    }

    return *decoder.readPos++;
}

/*
========================
=
= WriteOutput
=
========================
*/

static void WriteOutput(byte outByte) // 8002D214
{
    decoder.write.Push(outByte);
}

/*
========================
=
= WriteBinary
= routine required for encoding
=
========================
*/

static void WriteBinary(int binary) // 8002D288
{
    decoder.var3 = (decoder.var3 << 1);

    if(binary != 0)
    {
        decoder.var3 = (decoder.var3 | 1);
    }

    decoder.var2 = (decoder.var2 + 1);
    if(decoder.var2 == 8)
    {
        WriteOutput((byte)decoder.var3);
        decoder.var2 = 0;
    }
}

/*
========================
=
= DecodeScan
=
========================
*/

static int DecodeScan(void) // 8002D2F4
{
    int resultbyte;

    resultbyte = decoder.var0;

    decoder.var0 = (resultbyte - 1);
    if((resultbyte < 1))
    {
        resultbyte = GetDecodeByte();

        decoder.var1 = resultbyte;
        decoder.var0 = 7;
    }

    resultbyte = (0 < (decoder.var1 & 0x80));
    decoder.var1 = (decoder.var1 << 1);

    return resultbyte;
}

/*
========================
=
= MakeExtraBinary
= routine required for encoding
=
========================
*/

static void MakeExtraBinary(int binary, int shift) // 8002D364
{
    int i;

    i = 0;
    if(shift > 0)
    {
        do
        {
            WriteBinary(binary & 1);
            binary = (binary >> 1);
        } while(++i != shift);
    }
}

/*
========================
=
= RescanByte
=
========================
*/

static int RescanByte(int byte) // 8002D3B8
{
    int shift;
    int i;
    int resultbyte;

    resultbyte = 0;
    i = 0;
    shift = 1;

    if(byte <= 0)
        return resultbyte;

    do
    {
        if(DecodeScan() != 0)
        {
            resultbyte |= shift;
        }

        i++;
        shift = (shift << 1);
    } while(i != byte);

    return resultbyte;
}

/*
========================
=
= WriteEndCode
= routine required for encoding
=
========================
*/

static void WriteEndCode(void) // 8002D424
{
    if(decoder.var2 > 0)
    {
        WriteOutput((byte)(decoder.var3 << (8 - decoder.var2)) & 0xff);
    }
}

/*
========================
=
= InitDecodeTable
=
========================
*/

static void InitDecodeTable(void) // 8002D468
{
    int evenVal, oddVal, incrVal;

    short *curArray;
    short *incrTbl;
    short *evenTbl;
    short *oddTbl;

    tableVar01[15] = 3;
    tableVar01[16] = 0;
    tableVar01[17] = 0;

    decoder.var0 = 0;
    decoder.var1 = 0;
    decoder.var2 = 0;
    decoder.var3 = 0;

    curArray = &array01[2];
    incrTbl = &DecodeTable[0x4F2];

    incrVal = 2;

    do
    {
        if(incrVal < 0)
        {
            *incrTbl = (short)((incrVal + 1) >> 1);
        }
        else
        {
            *incrTbl = (short)(incrVal >> 1);
        }

        *curArray++ = 1;
        incrTbl++;
    } while(++incrVal < 1258);

    oddTbl  = &DecodeTable[0x279];
    evenTbl = &DecodeTable[1];

    evenVal = 2;
    oddVal = 3;

    do
    {
        *oddTbl++ = (short)oddVal;
        oddVal += 2;

        *evenTbl++ = (short)evenVal;
        evenVal += 2;

    } while(oddVal < 1259);

    tableVar01[0] = 0;

    incrVal = (1 << ShiftTable[0]);
    tableVar01[6] = (incrVal - 1);
    tableVar01[1] = incrVal;

    incrVal += (1 << ShiftTable[1]);
    tableVar01[7] = (incrVal - 1);
    tableVar01[2] = incrVal;

    incrVal += (1 << ShiftTable[2]);
    tableVar01[8] = (incrVal - 1);
    tableVar01[3] = incrVal;

    incrVal += (1 << ShiftTable[3]);
    tableVar01[9] = (incrVal - 1);
    tableVar01[4] = incrVal;

    incrVal += (1 << ShiftTable[4]);
    tableVar01[10] = (incrVal - 1);
    tableVar01[5] = incrVal;

    incrVal += (1 << ShiftTable[5]);
    tableVar01[11] = (incrVal - 1);
    tableVar01[12] = (incrVal - 1);

    tableVar01[13] = tableVar01[12] + 64;
}

/*
========================
=
= CheckTable
=
========================
*/

static void CheckTable(int a0,int a1,int a2) // 8002D624
{
    int i;
    int idByte1;
    int idByte2;
    short *curArray;
    short *evenTbl;
    short *oddTbl;
    short *incrTbl;

    i = 0;
    evenTbl = &DecodeTable[0];
    oddTbl  = &DecodeTable[0x278];
    incrTbl = &DecodeTable[0x4F0];

    idByte1 = a0;

    do
    {
        idByte2 = incrTbl[idByte1];

        array01[idByte2] = (array01[a1] + array01[a0]);

        a0 = idByte2;

        if(idByte2 != 1)
        {
            idByte1 = incrTbl[idByte2];
            idByte2 = evenTbl[idByte1];

            a1 = idByte2;

            if(a0 == idByte2)
            {
                a1 = oddTbl[idByte1];
            }
        }

        idByte1 = a0;
    }while(a0 != 1);

    if(array01[1] != 0x7D0)
    {
        return;
    }

    array01[1] >>= 1;

    curArray = &array01[2];
    do
    {
        curArray[3] >>= 1;
        curArray[2] >>= 1;
        curArray[1] >>= 1;
        curArray[0] >>= 1;
        curArray += 4;
        i += 4;
    } while(i != 1256);
}

/*
========================
=
= DecodeByte
=
========================
*/

static void DecodeByte(int tblpos) // 8002D72C
{
    int incrIdx;
    int evenVal;
    int idByte1;
    int idByte2;
    int idByte3;
    int idByte4;

    short *evenTbl;
    short *oddTbl;
    short *incrTbl;
    short *tmpIncrTbl;

    evenTbl = &DecodeTable[0];
    oddTbl  = &DecodeTable[0x278];
    incrTbl = &DecodeTable[0x4F0];

    idByte1 = (tblpos + 0x275);
    array01[idByte1] += 1;

    if(incrTbl[idByte1] != 1)
    {
        tmpIncrTbl = &incrTbl[idByte1];
        idByte2 = *tmpIncrTbl;

        if(idByte1 == evenTbl[idByte2])
        {
            CheckTable(idByte1, oddTbl[idByte2], idByte1);
        }
        else
        {
            CheckTable(idByte1, evenTbl[idByte2], idByte1);
        }

        do
        {
            incrIdx = incrTbl[idByte2];
            evenVal = evenTbl[incrIdx];

            if(idByte2 == evenVal)
            {
                idByte3 = oddTbl[incrIdx];
            }
            else
            {
                idByte3 = evenVal;
            }

            if(array01[idByte3] < array01[idByte1])
            {
                if(idByte2 == evenVal)
                {
                    oddTbl[incrIdx] = (short)idByte1;
                }
                else
                {
                    evenTbl[incrIdx] = (short)idByte1;
                }

                evenVal = evenTbl[idByte2];

                if(idByte1 == evenVal)
                {
                    idByte4 = oddTbl[idByte2];
                    evenTbl[idByte2] = (short)idByte3;
                }
                else
                {
                    idByte4 = evenVal;
                    oddTbl[idByte2] = (short)idByte3;
                }

                incrTbl[idByte3] = (short)idByte2;

                *tmpIncrTbl = (short)incrIdx;
                CheckTable(idByte3, idByte4, idByte4);

                tmpIncrTbl = &incrTbl[idByte3];
            }

            idByte1 = *tmpIncrTbl;
            tmpIncrTbl = &incrTbl[idByte1];

            idByte2 = *tmpIncrTbl;
        } while (idByte2 != 1);
    }
}

/*
========================
=
= StartDecodeByte
=
========================
*/

static int StartDecodeByte(void) // 8002D904
{
    int lookup;
    short *evenTbl;
    short *oddTbl;

    lookup = 1;

    evenTbl = &DecodeTable[0];
    oddTbl  = &DecodeTable[0x278];

    while(lookup < 0x275)
    {
        if(DecodeScan() == 0)
        {
            lookup = evenTbl[lookup];
        }
        else
        {
            lookup = oddTbl[lookup];
        }
    }

    lookup = (lookup + -0x275);
    DecodeByte(lookup);

    return lookup;
}

/*
========================
=
= DecodeD64
=
= Exclusive Doom 64
=
========================
*/

void DecodeD64(uint8_t *input) // 8002DFA0
{
    int copyPos, storePos;
    int dec_byte, resc_byte;
    int incrBit, copyCnt, shiftPos, j;

    InitDecodeTable();

    OVERFLOW_READ = D_MAXINT;
    OVERFLOW_WRITE = D_MAXINT;

    incrBit = 0;

    decoder.read = input;
    decoder.readPos = input;
    decoder.write.Empty();

    allocPtr = (byte*)Z_Malloc(tableVar01[13]);

    dec_byte = StartDecodeByte();

    while(dec_byte != 256)
    {
        if(dec_byte < 256)
        {
            /* Decode the data directly using binary data code */

            WriteOutput((byte)(dec_byte & 0xff));
            allocPtr[incrBit] = (byte)dec_byte;

            /* Resets the count once the memory limit is exceeded in allocPtr,
                so to speak resets it at startup for reuse */
            incrBit += 1;
            if(incrBit == tableVar01[13])
            {
                incrBit = 0;
            }
        }
        else
        {
            /* Decode the data using binary data code,
                a count is obtained for the repeated data,
                positioning itself in the root that is being stored in allocPtr previously. */

            /*  A number is obtained from a range from 0 to 5,
                necessary to obtain a shift value in the ShiftTable*/
            shiftPos = (dec_byte + -257) / 62;

            /*  get a count number for data to copy */
            copyCnt  = (dec_byte - (shiftPos * 62)) + -254;

            /*  To start copying data, you receive a position number
                that you must sum with the position of table tableVar01 */
            resc_byte = RescanByte(ShiftTable[shiftPos]);

            /*  with this formula the exact position is obtained
                to start copying previously stored data */
            copyPos = incrBit - ((tableVar01[shiftPos] + resc_byte) + copyCnt);

            if(copyPos < 0)
            {
                copyPos += tableVar01[13];
            }

            storePos = incrBit;

            for(j = 0; j < copyCnt; j++)
            {
                /* write the copied data */
                WriteOutput(allocPtr[copyPos]);

                /* save copied data at current position in memory allocPtr */
                allocPtr[storePos] = allocPtr[copyPos];

                storePos++; /* advance to next allocPtr memory block to store */
                copyPos++;  /* advance to next allocPtr memory block to copy */

                /* reset the position of storePos once the memory limit is exceeded */
                if(storePos == tableVar01[13])
                {
                    storePos = 0;
                }

                /* reset the position of copyPos once the memory limit is exceeded */
                if(copyPos == tableVar01[13])
                {
                    copyPos = 0;
                }
            }

            /* Resets the count once the memory limit is exceeded in allocPtr,
                so to speak resets it at startup for reuse */
            incrBit += copyCnt;
            if(incrBit >= tableVar01[13])
            {
                incrBit -= tableVar01[13];
            }
        }

        dec_byte = StartDecodeByte();
    }

    Z_Free(allocPtr);
}
/*
=============
W_GetLumpinfo2
=============
*/
lumpinfo_t*  W_GetLumpinfo2(char* name)
{
    int     i;
    lumpinfo_t*  lump_p;
    char    clean[16];

    W_CleanupName(name, clean);

    for(lump_p=wad_lumps2, i=0 ; i<wad_numlumps2 ; i++,lump_p++)
    {
        if(!strcmp(clean, lump_p->name))
        {
            return lump_p;
        }
    }

    Sys_Error("W_GetLumpinfo2: %s not found", name);
    return nullptr;
}

/*
=============
W_GetLumpName2
=============
*/
void* W_GetLumpName2(char* name)
{
    lumpinfo_t*  lump;

    lump = W_GetLumpinfo2(name);

    return (void*)(wad_base2 + lump->filepos);
}

/*
=============
W_GetLumpNum2
=============
*/
void* W_GetLumpNum2(int num)
{
    lumpinfo_t*  lump;

    if(num < 0 || num > wad_numlumps2)
    {
        Sys_Error("W_GetLumpNum2: bad number: %i", num);
    }

    lump = wad_lumps2 + num;

    return (void*)(wad_base2 + lump->filepos);
}

/*
=============
W_CheckNumForName2
Returns -1 if name not found.
=============
*/
int W_CheckNumForName2(const char* name)
{
    int i = -1;
    lumpinfo_t*  lump_p;
    char    clean[16];

    W_CleanupName(name, clean);

    for(lump_p=wad_lumps2, i=0 ; i<wad_numlumps2 ; i++,lump_p++)
    {
        if(!kexStr::StrCaseCmp(clean, lump_p->name))
        {
            return i;
        }
    }

    return -1;
}

/*
=============
W_GetNumForName2
Calls W_CheckNumForName, but bombs out if not found.
=============
*/
int W_GetNumForName2(const char* name)
{
    int i;

    i = W_CheckNumForName2(name);

    if(i == -1)
    {
        kexError("W_GetNumForName2: %s not found!", name);
    }

    return i;
}

/*
====================
W_LoadWadFile2
====================
*/
void W_LoadWadFile2(const char* filename)
{
    lumpinfo_t*      lump_p;
    wadinfo_t*       header;
    int             i;
    int             infotableofs;

    kexPrintf("W_LoadWadFile2: Adding %s...\n", filename);

    wad_base2 = COM_LoadHunkFile(filename);
    if(!wad_base2)
    {
        kexWarning("W_LoadWadFile2: couldn't load %s\n", filename);
        return;
    }

    header = (wadinfo_t*)wad_base2;

    if(header->identification[0] != 'W'
            || header->identification[1] != 'A'
            || header->identification[2] != 'D'
            || header->identification[3] != '2')
    {
        kexWarning("Wad file %s doesn't have WAD2 id\n",filename);
        return;
    }

    wad_numlumps2 = kexEndian::SwapLE32(header->numlumps);
    infotableofs = kexEndian::SwapLE32(header->infotableofs);
    wad_lumps2 = (lumpinfo_t*)(wad_base2 + infotableofs);

    for(i=0, lump_p = wad_lumps ; i<wad_numlumps2 ; i++,lump_p++)
    {
        lump_p->filepos = kexEndian::SwapLE32(lump_p->filepos);
        lump_p->size = kexEndian::SwapLE32(lump_p->size);
        lump_p->disksize = kexEndian::SwapLE32(lump_p->disksize);
        W_CleanupName(lump_p->name, lump_p->name);
    }
}

struct qTexInfoEntry_s
{
    qTexInfoEntry_s() :
        index(-1),
        n64texlump(-1),
        dwOffsetToDataBlob(uint32_t(-1)),
        dwFileSize(0)
    {
    }

    int32_t index;
    int32_t n64texlump;
    uint32_t dwOffsetToDataBlob;
    size_t dwFileSize;
};

static void DescrambleTexture(byte* pData, const int width, const int height)
{
    const int size = (width * height)/2;
    constexpr int mask = 1;
    
    byte* pRover = pData;

    for(int i = 0; i < height; ++i)
    {
        kexAssert(pRover < (pData + (width * height)));
        if(i & mask)
        {
            for(int x = 0; x < width; x += 4)
            {
                int* pTmp = (int*)&pRover[x];
                *pTmp = kexEndian::SwapBE32(*pTmp);
            }
        }

        pRover += width;
    }

    pRover = pData;

    for(int i = 0; i < height; ++i)
    {
        kexAssert(pRover < (pData + (width * height)));
        if(i & mask)
        {
            for(int x = 0; x < width; x += 2)
            {
                int16_t* pTmp = (int16_t*)&pRover[x];
                *pTmp = kexEndian::SwapBE16(*pTmp);
            }
        }

        pRover += width;
    }
}

static void BuildQuake64BSPLevel(const kexStr& strOutputPath, const int lump)
{
    uint32_t dwDataPos[HEADER_LUMPS];
    uint32_t dwDataSize[HEADER_LUMPS];
    kexArray<byte> nLevelDataBlob;
    kexArray<byte> nTexturesDataBlob;
    kexBufferStreamGrowable cTexturesDataLump;
    kexTMap<qTexInfoEntry_s> nTexEntryMap;
    int32_t iCurrentTextureID;

    kexMemclr(dwDataPos);
    kexMemclr(dwDataSize);

    const int t_start = W_GetNumForName2("t_start")+1;
    const int scanLump = lump+1;
    iCurrentTextureID = 0;

    for(int i = scanLump; i < scanLump+14; ++i)
    {
        lumpinfo_t* pLump = &wad_lumps2[i];
        DecodeD64((byte*)W_GetLumpNum2(i));
        kexArray<byte> nData = decoder.write;

        int index = (i - lump)-1;
        if(index < HEADER_LUMPS)
        {
            if(index >= LUMP_TEXTURES)
            {
                index++;
            }

            if(index == LUMP_TEXINFO)
            {
                texinfo_t* pTexInfo = (texinfo_t*)nData.GetDataPtr();
                size_t count = pLump->size / sizeof(*pTexInfo);
                for(int t = 0; t < count; ++t)
                {
                    int texid = pTexInfo[t].miptex;
                    if(texid == -1)
                    {
                        continue;
                    }

                    lumpinfo_t* pTexLump = &wad_lumps2[t_start+texid];
                    kexStr strTextureName = pTexLump->name;

                    qTexInfoEntry_s* pEntry = nTexEntryMap.FindAdd(strTextureName);
                    if(pEntry->index == -1)
                    {
                        DecodeD64((byte*)W_GetLumpNum2(t_start+texid));
                        kexArray<byte> nTexData = decoder.write;
                        byte* pTexData = nTexData.GetDataPtr();

                        int16_t* pTmp = (int16_t*)pTexData;
                        int width = kexEndian::SwapBE16(pTmp[0]);
                        int height = kexEndian::SwapBE16(pTmp[1]);
                        int shift = kexEndian::SwapBE16(pTmp[2]);
                        
                        int oldwidth = width;
                        int oldheight = height;

                        width >>= shift;
                        height >>= shift;

                        if(pTexLump->name[0] != '*')
                        {
                            DescrambleTexture(pTexData+8, width, height);
                        }

                        kexImage cImage(pTexData+8, width, height, RTPF_R8);
                        const size_t dwMipTexSize = (44 + (width*height));

                        pEntry->dwOffsetToDataBlob = nTexturesDataBlob.Length();
                        nTexturesDataBlob.Resize(nTexturesDataBlob.Length() + dwMipTexSize);

                        kexBufferStreamDirect cTexWrite(&nTexturesDataBlob[pEntry->dwOffsetToDataBlob], dwMipTexSize);
                        for(size_t c = 0; c < strTextureName.Length(); ++c)
                        {
                            cTexWrite.Write8(strTextureName[c]);
                        }
                        for(size_t c = strTextureName.Length(); c < strTextureName.Length()+(16-strTextureName.Length()); ++c)
                        {
                            cTexWrite.Write8(0);
                        }

                        cTexWrite.Write32(width);
                        cTexWrite.Write32(height);
                        cTexWrite.Write32(shift);
                        cTexWrite.Write32(44);
                        cTexWrite.Write32(44);
                        cTexWrite.Write32(44);
                        cTexWrite.Write32(44);

                        for(int c = 0; c < (width*height); ++c)
                        {
                            cTexWrite.Write8(cImage.Data()[c]);
                        }

                        pEntry->n64texlump = t_start+texid;
                        pEntry->index = iCurrentTextureID;
                        pEntry->dwFileSize = dwMipTexSize;
                        iCurrentTextureID++;
                    }
                }

                for(int t = 0; t < count; ++t)
                {
                    int texid = pTexInfo[t].miptex;
                    lumpinfo_t* pTexLump = &wad_lumps2[t_start+texid];
                    kexStr strTextureName = pTexLump->name;
                        
                    qTexInfoEntry_s* pEntry = nTexEntryMap.GetValue(strTextureName);
                    if(!pEntry || pEntry->index <= -1)
                    {
                        pTexInfo[t].miptex = -1;
                        continue;
                    }

                    pTexInfo[t].miptex = pEntry->index;
                }

                cTexturesDataLump.Write32(iCurrentTextureID);
                for(int t = 0; t < iCurrentTextureID; ++t)
                {
                    typename kexTMap<qTexInfoEntry_s>::Iterator cItr(nTexEntryMap);
                    typename kexTMap<qTexInfoEntry_s>::hashType_t* pKey = nullptr;

                    while((pKey = cItr.GetNext()))
                    {
                        if(pKey->GetValue().index == t)
                        {
                            cTexturesDataLump.WriteU32(pKey->GetValue().dwOffsetToDataBlob + (4*iCurrentTextureID) + 4);
                            break;
                        }
                    }
                }

                for(const auto& someByte : nTexturesDataBlob)
                {
                    cTexturesDataLump.Write8(someByte);
                }

                kexAutoFileWrite cTexturesWrite(kexStr::Format("%s/%sTEXTURES.lmp", strOutputPath.c_str(), wad_lumps2[lump].name));
                if(cTexturesWrite.IsValid())
                {
                    cTexturesWrite->Write(cTexturesDataLump.Buffer(), cTexturesDataLump.BufferLength());
                }
            }

            dwDataPos[index] = nLevelDataBlob.Length() + 124;
            dwDataSize[index] = pLump->size;
            for(int j = 0; j < pLump->size; ++j)
            {
                nLevelDataBlob.Push(nData[j]);
            }
        }
    }

    kexAutoFileWrite cBspWrite(kexStr::Format("%s/%s.bsp", strOutputPath.c_str(), wad_lumps2[lump].name));
    if(cBspWrite.IsValid())
    {
        int32_t iOffsetFromTextureData = 0;

        int32_t iScratch = BSPVERSION_QUAKE64;
        cBspWrite->Write((byte*)&iScratch, 4);
        for(int j = 0; j < HEADER_LUMPS; ++j)
        {
            if(j == LUMP_TEXTURES)
            {
                iOffsetFromTextureData = cTexturesDataLump.BufferLength();
                iScratch = dwDataPos[j+1];
                cBspWrite->Write((byte*)&iScratch, 4);
                iScratch -= 124;
                nLevelDataBlob.Insert(iScratch, cTexturesDataLump.BufferLength());
                kexMemcpy(&nLevelDataBlob[iScratch], cTexturesDataLump.Buffer(), cTexturesDataLump.BufferLength());
                iScratch = (int32_t)cTexturesDataLump.BufferLength();

                cBspWrite->Write((byte*)&iScratch, 4);
                continue;
            }

            iScratch = dwDataPos[j];
            iScratch += iOffsetFromTextureData;

            cBspWrite->Write((byte*)&iScratch, 4);
            cBspWrite->Write((byte*)&dwDataSize[j], 4);
        }

        cBspWrite->Write(nLevelDataBlob.GetDataPtr(), nLevelDataBlob.Length());
    }
}

COMMAND(loadquake64wad)
{
    W_LoadWadFile2("Quake64.wad");
    if(!wad_base2 || wad_numlumps2 <= 0)
    {
        return;
    }

    kexStr strOutputPath = kexStr::FormatPath("q64output");
    if(!kexPlatform::cFile->MakeDirectory(strOutputPath.c_str()))
    {
        return;
    }

    for(int i = 0; i< wad_numlumps2 ; i++)
    {
        lumpinfo_t* lump = &wad_lumps2[i];
        if( !strcmp(lump->name, "NEND") ||
           (lump->name[0] == 'N' &&
           (lump->name[0] == 'N' &&
            lump->name[1] == 'E' &&
           (lump->name[2] >= '1' && lump->name[2] <= '9') &&
            lump->name[3] == 'M' &&
           (lump->name[4] >= '1' && lump->name[4] <= '9') &&
            lump->name[5] == '\0')))
        {
            BuildQuake64BSPLevel(strOutputPath, i);
            continue;
        }
        else if(lump->size == 0)
        {
            continue;
        }
    }
}

Additional mods

By inputting the command line parameter +ui_addonsBaseURL "https://kexquake.s3.amazonaws.com" it's possible to obtain more custom third-party mods for the game, possibly awaiting curation. (Last updated Oct 19, 2023)

Mod name Status
Beyond Belief Added on September 29, 2022[2]
Contract Revoked Added on July 13, 2023[3]
Dark Triad Not yet added
Deathmatch Dimension Added on August 10, 2023[4]
Elder World Jam Not yet added
Empire of Disorder Not yet added
Epochs of Enmity Not yet added
Euclid's Nightmare Not yet added
Honey Added on December 2, 2021[5]
IKSPQ Not yet added
Insomnia Not yet added
Koohoo Retro Jam Not yet added
Map Jam X: Insomnia Not yet added
Operation: Urth Majik Not yet added
Quake 64 Added with the base game
Realm of Tiddles Not yet added
Rubicon 2 Added on August 18, 2022[6]
Sacrilege Not yet added
Spiritworld Added on November 30, 2023[7]
Squire of Time Not yet added
Terra Added on March 29, 2022[8]
The Punishment Due Added on May 31, 2022[9]
Underdark Overbright & Copper Added on February 9, 2022[10]

References