Levels

Level Format

A binary format for storing level data.

Overview

KulaQuest uses a binary format for storing level data. This format does not have a file extension, and is mostly the same across all versions of the game with slight differences. Levels are not stored on disc as standalone files, but are instead stored as part of the world's .PAK file (the first demo uses an older .KUB format instead).

Structure

The format is comprised of the following sections:

  • Identifiers for every block in the grid
  • The number of blocks and records
  • Records
  • Optional level settings
struct LevelData
Offset(h)SizeTypeDescription
0x0000078608i16[34 * 34 * 34]Block grid data
0x133102i16Block count
0x133122i16Padding
0x133142i16Record count
0x13316256 * record_countRecord[record_count]Records
-256LevelSettingsLevel settings (optional)

Block Grid Data

Every level in the game is a fixed 34x34x34 grid of blocks, each identified by a value inside the block data section that determines its type. The first block at coordinates (0, 0, 0) begins on the top-left-back corner of the grid, and the last block at coordinates (33, 33, 33) ends at the bottom-right-front corner of the grid:

Level grid layout

The axes in the current level editor are not accurate to the game's coordinate system. As a result, the level editor should not be used as a reference for coordinate-related work.

The first 78,608 bytes of the file contain an array of 16-bit integers, one for each block in the grid. As referenced above, the first value in this section corresponds to the first block in the level (coordinates (0, 0, 0)), with the last value corresponding to the last block in the level. A cell at grid coordinates (x, y, z) can be accessed using the following formula:

int index = x * 34*34 + y * 34 + z;  // == x * 1156 + y * 34 + z
int byteOffset = index * 2;

Each 16-bit value represents what kind of block is at that position in the grid:

ValueBlock Type
-1Empty, no block here.
0A plain block with no special properties; i.e. a block that doesn't contain any objects or is of a specific type.
1A fire block, which is just a plain block but with fire patches on all sides. No objects or properties.
2Same as above, but with ice patches on all sides.
3Same as above, but with invisible patches on all sides.
4Same as above, but with acid patches on all sides.
5 and aboveA special block that references a record, see below.
-2Reserved for laser segments, in-memory usage only.

Any value greater than or equal to 5 indicates a block that contains a record, such as a block that contains items and/or objects, a crumbling block, etc. The first block of this type must always start at 5, and is incremented for each subsequent block that contains a record.

Coordinate System

Two types of coordinate systems are used in this format — block coordinates and entity coordinates. Block coordinates reference positions in the grid, where each unit corresponds to one block. Entity coordinates are used for records that require sub-block precision, such as the moving block's current position and the player's position. Entity coordinates use the same 16-bit format as block coordinates, but at a finer scale: 1 block unit = 512 entity units. The upper bits of an entity coordinate give the block index, and the lower 9 bits (0-511) represent the sub-block position within that block.

Conversion between the two systems:

// Block to entity (center of block)
short entityCoord = blockCoord * 512;

// Entity to block (rounded to nearest block)
short blockCoord = (entityCoord + 256) >> 9;

Here's an example of both systems referencing the same position: a player positioned on top of block (17, 16, 17):

  • 11 00 10 00 11 00 - Block coordinate (17, 16, 17)
  • 00 22 00 20 9C 20 - Entity coordinate (8704, 8192, 8348)

The following structures will be used to represent both systems throughout this document:

struct BlockPosition6 bytes
Offset(h)SizeTypeDescription
+0x002i16X position (1 unit = 1 block)
+0x022i16Y position (1 unit = 1 block)
+0x042i16Z position (1 unit = 1 block)
struct EntityPosition6 bytes
Offset(h)SizeTypeDescription
+0x002i16X position (1 unit = 1/512 of a block)
+0x022i16Y position (1 unit = 1/512 of a block)
+0x042i16Z position (1 unit = 1/512 of a block)

Direction Table

The following table will be used throughout this document to indicate a direction based on a value.

ValueDirection
0Z-
1X+
2Y+
3Y-
4X-
5Z+

Block and Record Count

Immediately after the block grid data begins these 16-bit values:

struct BlockAndRecordCount6 bytes
Offset(h)SizeTypeDescription
0x133102i16Block count
0x133122i16Padding
0x133142i16Record count

The block count does not have to be set correctly as it's not read in game. However, the record count is required as the game utilizes it to determine the number of records in the level. For an unknown reason, the padding value in-between is sometimes set to -1 in certain levels, and the block count value is sometimes set to a negative value.

Block Records

There are a lot of different types of special blocks used in game, each requiring additional information defined below towards the end of the file, right after the previous section. Each record is a chunk of 256 bytes. The game determines the type of record based on the first value in the record.

Object Block Record0-4

The following structure applies to blocks that contain objects, i.e. anything that is assigned to a specific side of a block, like items and traps. Moving, crumbling, flashing, and laser blocks follow a different structure, and have dedicated sections respectively below.

struct ObjectBlockRecord256 bytes
Offset(h)SizeTypeDescription
+0x002i16Block type (0-4)
+0x0232ObjectObject on the Negative Z side
+0x2232ObjectObject on the Positive X side
+0x4232ObjectObject on the Positive Y side
+0x6232ObjectObject on the Negative Y side
+0x8232ObjectObject on the Negative X side
+0xA232ObjectObject on the Positive Z side
+0xC256i16[28]Padding (-1)
+0xFA6BlockPositionThe block's position in the level

Each object contains 32 bytes of information pertaining to its type, various states, and appearance:

struct Object32 bytes
Offset(h)SizeTypeDescription
+0x002i16ID
+0x022i16Direction
+0x042i16Variant
+0x062i16State
+0x082i16Item state index (set automatically in memory)
+0x0A2i16Object reference 1 (buttons and transporters only)
+0x0C2i16Object reference 2 (buttons and transporters only)
+0x0E2i16Vertex buffer index (0)
+0x102i16Ground offset (demo only)
+0x122i16Rotation type (demo only)
+0x142i16Animation phase 1 (memory only, default: -1)
+0x162i16Animation phase 2 (memory only, default: -1)
+0x182i16Animation phase 3 (memory only, default: -1)
+0x1A2i16Rotation speed (demo only)
+0x1C2i16Animation counter (memory only, default: -1)
+0x1E2i16Animation state (memory only, default: -1)

Here is an interactive example of the block that contains the level exit from the first level of Hiro, starting at the file offset 0x13310:

013310h  14 00 00 00 06 00 00 00 07 00 00 00 00 00 02 00
013320h  FF FF FF FF FF FF 00 00 F4 01 01 00 FF FF FF FF
013330h  FF FF 1E 00 FF FF FF FF 00 00 FF FF FF FF 00 00
013340h  FF FF FF FF FF FF 00 00 FF FF FF FF FF FF FF FF
013350h  FF FF FF FF FF FF FF FF 00 00 FF FF FF FF 00 00
013360h  FF FF FF FF FF FF 00 00 FF FF FF FF FF FF FF FF
013370h  FF FF FF FF FF FF FF FF 00 00 FF FF FF FF 00 00
013380h  FF FF FF FF FF FF 00 00 FF FF FF FF FF FF FF FF
013390h  FF FF FF FF FF FF FF FF 00 00 FF FF FF FF 00 00
0133A0h  FF FF FF FF FF FF 00 00 FF FF FF FF FF FF FF FF
0133B0h  FF FF FF FF FF FF FF FF 00 00 FF FF FF FF 00 00
0133C0h  FF FF FF FF FF FF 00 00 FF FF FF FF FF FF FF FF
0133D0h  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0133E0h  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0133F0h  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
013400h  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
013410h  11 00 0C 00 11 00

A table documenting every object and its properties is available here.

Every object has a unique ID that is used to identify it, with some objects having different variants. For example, the coin has an id of 0x25, and has the 3 following variants:

  • Bronze
  • Gold
  • Silver

An object can have several states, such as a transporter or button being enabled or disabled. Items use this value in memory to determine whether it has been collected by the player, and is set to 1 in file. Finally, the direction value is utilized by certain directional objects, such as arrows and the player spawn. If a side of a block does not have an object, the object ID and state are set to 0, and the rest of the values are set to -1.

Animation Fields

Models in game can contain multiple vertex buffers, though the only object that utilizes this is the moving spike for its retracting animation (each frame is actually a separate vertex buffer). The vertex buffer index determines which buffer to use, and it must always be set to 0 in file, regardless of the number of vertex buffers the model contains.

The following fields are automatically set in memory and are not required:

FieldDescription
Item state indexIncremented from 0 after every collectable item.
Animation phases3 values used to keep track of various sin phases of an object's spinning animation.
Animation stateThe state of the object's animation.
Animation counterA counter for the object's animation.

In the alpha and beta versions of the game, the following values are required to be set, but are not read in later versions:

FieldDescription
Ground offsetDetermines how far an object is above the block.
Rotation typeDetermines the type of rotation an object uses.
Rotation speedDetermines how fast an object rotates.

Object Reference Fields

Lastly are the two object reference fields, used by transporters and buttons to determine what object(s) to toggle when the parent object is toggled. Each value encodes a block record index and a face index as a packed 16-bit integer:

// Encoding
short value = (blockRecordIndex << 4) | side;

// Decoding
short blockRecordIndex = value >> 4;
short side             = value & 0xF;

For example, the value D0 01 (0x01D0):

  • Record index: 0x01D0 >> 4 = 29
  • Side: 0x01D0 & 0xF = 0 (negative z face)

And 72 01 (0x0172):

  • Record index: 0x0172 >> 4 = 23
  • Side: 0x0172 & 0xF = 2 (positive x face)

When targeting a laser block, the side must be set to 6.

In the alpha and beta versions of the game, buttons toggled themselves when pressed and only contain the first slot. In all other versions of the games, buttons have two slots and do not toggle themselves when pressed — meaning one slot is often used to point back at the button itself. Only the second slot (object reference 1) can be used this way.

Moving Block Record5

A single block is placed at the starting position (current position), known as the origin. Based on the length, that many blocks including the origin will be placed along the positive direction of the axis. e.g. if the direction is set to Negative X, the additional blocks will still be placed in the Positive X direction.

For example, a length of 1 means only the origin block itself, so no additional blocks will be placed. If the length is 3 and the direction is set to 1, 2 blocks will be placed in the Positive X direction. The maximum length a moving block can be is 4, as the texture data the block stores in memory inside its record overwrites other information about the moving block, as well as the next record.

struct MovingBlockRecord256 bytes
Offset(h)SizeTypeDescription
+0x002i16Record type (5)
+0x022i16Direction
+0x042i16Axis
+0x062i16Unknown
+0x086BlockPositionPosition 1
+0x0E6BlockPositionPosition 2
+0x1412-Padding
+0x202i16Unknown
+0x222i16Length
+0x242i16Speed
+0x262i16Padding
+0x282i16Block ID
+0x2A196-Padding
+0xEE6EntityPositionCurrent position
+0xF46i16[3]Unknown (00 01 00 01 00 01)
+0xFA6BlockPositionPosition

The moving block contains 3 position values:

FieldDescription
Position 1One of two points that the block will move between during the level. This position must be before position 2 along the axis.
Position 2The other one of two points that the block will move between, and must be positioned after position 1.
Current PositionThe position the moving block will start at when the level is started, meaning it can start at a different point along the axis other than the two positions specified, though in all cases from the game, this position is the same as one of the two points above.

Moving blocks and their respective axis

In the diagram above, position 1 (green) is always first along the axis before position 2 (blue), regardless of what direction the block is set to.

The direction value specifies what direction the block will initially move towards on the axis, and follows the direction table above. Any value other than the ones in the table will cause the moving block to not move at all. The axis field specifies the axis the block will move along, and how the texture is wrapped onto the block. If this value is not set correctly, the block's collision will not work properly and will often crash the game.

ValueAxis
1X axis
2Y axis
5Z axis
-Any other value defaults to the Z axis.

The speed indicates how many times the current position is incremented or decremented per frame depending on if it's moving in a positive or negative direction, respectively. Lastly, the block ID is set to the value that represents this record in the block grid data section.

Crumbling Block Record6

struct CrumblingBlockRecord256 bytes
Offset(h)SizeTypeDescription
+0x002i16Record type (6)
+0x022i16State (1)
+0x046EntityPositionPosition (entity)
+0x0A240-Padding
+0xFA6BlockPositionPosition (block)

Crumbling blocks have the following state values, though they should be set to 1 in file:

ValueDescription
0The crumbling block is gone
1The crumbling block is active.
2The crumbling block is crumbling (unused).
3The crumbling block is crumbling.
-Any other value causes the crumbling block to still be active, but will not produce a sound when touched.

The crumbling block contains entity positioning, set to the exact block coordinate of the crumbling block, though it is not referenced in game. The 240 bytes of padding are usually set to -1, though in some odd cases this section will contain object data and other weird structures. At some point during the game's development, the crumbling block may have been intended to contain objects.

Flashing Block Record7

struct FlashingBlockRecord256 bytes
Offset(h)SizeTypeDescription
+0x002i16Record type (7)
+0x022i16Index (-1, memory only)
+0x042i16Sync
+0x062i16State (-1, memory only)
+0x082i16Counter (-1, memory only)
+0x0A240-Padding
+0xFA6BlockPositionPosition

The sync value is used to specify in what order of timing the flashing block will appear, similar to moving spikes. Occasionally, just like crumble blocks, the 240 bytes of padding may contain weird structures, but have no affect at all.

Laser Block8

struct LaserBlockRecord256 bytes
Offset(h)SizeTypeDescription
+0x002i16Record type (8)
+0x022i16Direction
+0x042i16Axis (unused)
+0x062i16Enabled
+0x086BlockPositionPosition 1
+0x0E6BlockPositionPosition 2
+0x1414-Padding
+0x222i16Unknown (1)
+0x244-Padding
+0x282i16Block ID
+0x2A2-Padding
+0x2C2i16Color
+0x2E2i16Object reference
+0x30202-Padding
+0xFA6BlockPositionPosition

Position 1 and 2 specify the two points of the laser, where position 1 must always come before on the axis than position 2, exactly like moving blocks. If one of the positions are set to a block that isn't actually present in the file, the game will automatically create a plain block there.

The rest of the fields are as follows:

FieldDescription
DirectionDirection of the laser, follows the direction table above.
ColorThe color of the laser, follows the same structure that transporters and buttons do, and can be viewed here.
EnabledWhether the laser is enabled or not upon starting the level.
Block IDSame as moving blocks, set to the value that represents this record in the block grid data section.
Object referenceSpecifies the object to toggle when the laser itself is toggled, exactly like transporters and buttons.

Level Flag Record9

The record is optional, and if set is always at the end directly behind the level information record. This record is used for setting flags for the level and does not tie to any block at all:

struct LevelFlagRecord256 bytes
Offset(h)SizeTypeDescription
+0x002i16Record type (9)
+0x022i16Hidden level
+0x042i16Farsighted invisible blocks
+0x06250-Padding

The structure is very simple and only has two flags, which are set to true or false depending if they're set to 1 or 0 respectively. The first flag specifies if the current level is a hidden level, and the second flag specifies if the current level uses farsighted invisible blocks (used in Haze). Similar to crumble and flashing blocks, this record usually contains a lot of weird structures in its padding, and sometimes this record is added to a level that doesn't use any of its flags.

Level Settings Record666

struct LevelSettingsRecord256 bytes
Offset(h)SizeTypeDescription
+0x002i16Record type (666)
+0x026BlockPositionLast block modified
+0x084i16[2]Unknown
+0x0C2i16Start time
+0x0E242-Padding

Some levels do not contain this record, notably the first alpha KulaQuest demo. This record is not required, so the game will default to specific values if this record is not present.

The only confirmed value in this record is the start time, which specifies the amount of time the level starts with in seconds. The game calculates the amount of time by multiplying this value by 50, which is the PAL's game framerate. For reference, the game decrements currentTime every frame of unpaused gameplay, and ends the level if it reaches 0.

It is theorized that the block position value is leftover metadata from the game's original level editor, signifying the last block that was modified in the level:

  1. A fruit was accidentally placed in Kula World's FINAL 3 level. When this fruit was removed in the next version of the game, the position value just so happened to update to the removed fruit's block position.
  2. A change was made for LEVEL 133 on the KulaQuest (Japan) release to the green gem where it was moved from the fire block to in front of the key. The unknown position value also updated to the block that the gem was moved to.
  3. In the first level of the game, this position value points to the block that contains the farther right bronze coin, which happened to be moved forward and changed from a gold to a bronze coin from earlier versions of the game.

These are just some examples, but one could reasonably conclude that this position value was likely metadata as it is never referenced from within the game's programming. It's also worth noting that this position value can be set to a block that isn't contained in the level, likely referencing a deleted block. The two unknown short values before the start time value are also ignored in game, but seem to be always in multiples of 5 and sometimes negative.

Version Differences

This format remains mostly the same across all versions of the game with slight differences:

  • In the alpha release, there is no level settings record.
  • In the beta release, the mere existence of the level flag record causes the level to be hidden, so no flags are required to be set.

Level Start Time

In Kula World and Roll Away, the starting level time is calculated by multiplying the field by 50, which is the framerate of the PAL version:

// SLUS_007.24: 0x35F5C
// currentTime: 0xA573C

// checks if the record type is 666, meaning the level settings record exists;
// if so, set currentTime to the records's 6th index (startingTime) * 50.
if (*recordData == 0x29a) {
    currentTime = recordData[6] * 0x32;
}

// if the level settings record doesn't exist, default to 4950.
else {
    currentTime = 0x1356;
}

Kula World and Roll Away function the same here, so Roll Away is used as an example.

In KulaQuest (Japan), the time is calculated by multiplying this value by 60, which is the framerate of the NTSC version. An additional check was added if the value is 5940, which is the maximum time value (99) and is set to 7140 if so to allow additional level time:

// SCPS_100.64: 0x362C4
// currentTime: 0xA12EC

// checks if the block's type is 666, meaning the level settings record exists;
// if so, set currentTime to the records's 6th index (startingTime) * 50.
if (*recordData == 0x29a) {
    currentTime = recordData[6] * 0x3c;
}

// if the level settings record doesn't exist, default to 4950.
else {
    currentTime = 0x1be4;
}

// if the currentTime was set to 5940, default to 7140 to allow more time.
if (currentTime == 0x1734) {
    currentTime = 0x1be4;
}

Oddities

Most levels in the game have the fruit set to a Banana (0x2E), as the fruit is automatically set based on how many you have collected, except for the first alpha release where the order was set manually. However, some levels in later versions still have other fruit set, perhaps because they were originally early levels or on accident:

ReleaseLevelFruit
Main ReleasesINCA/LEVEL 42Watermelon
Main ReleasesHILLS/LEVEL 18Pumpkin
Main ReleasesHILLS/LEVEL 19Strawberry
Main ReleasesARCTIC/LEVEL 53Pumpkin
Main ReleasesFIELD/LEVEL 83Apple
Main ReleasesHAZE/LEVEL 116Watermelon
Main ReleasesMARS/LEVEL 122Watermelon
Kula World and Roll AwayMARS/HIDDEN 9Pumpkin
Main ReleasesHELL/LEVEL 137Watermelon
Main ReleasesHELL/LEVEL 146Watermelon
Kula WorldHELL/BONUS 30Pumpkin
Beta (1998-01-30) and Inca VariantLEVEL 1Watermelon
Beta (1998-01-30) and Inca VariantLEVEL 2Strawberry
Beta (1998-01-30) and Inca VariantLEVEL 3Watermelon
Hyper PlayStation Re-mix 1999 No. 9LEVEL 2Strawberry

LEVEL 125 from Mars is the only level in all releases that contain inaccessible objects hidden by another block, consisting of a couple of fire patches hidden inside a few blocks:

Screenshot of the level showing a fire patch hidden in a block.Screenshot of the level showing another fire patch hidden in a different block.

Some levels contain slow moving stars that face the wrong direction, causing them to initially move in the air when the level loads:

Screenshot of the level showing a slow moving star set to the wrong direction.

Screenshot of the level showing another slow moving star set to the wrong direction.

In the last bonus level in Kula World, every star is incorrectly set to the Positive X direction. This issue can be observed in the playthrough video PS1 Kula World 1998 - No Commentary by GameGamer, at 2:56:27.

Screenshot of the bonus level showing all of the slow moving stars set to the wrong direction.

Also, this is also the only level in any release after the alpha that does not contain the level settings record.


In Atlantis, LEVEL 94 is incorrectly spelled "LECEL 94".

For an unknown reason, the captivator and time pause patches have their direction value set to 4 in the OBJ LEVEL. Additionally, no objects have their direction set to 0 except for capture pods, who's direction value behaves unpredictably.

On this page