Xbox assets (optional)¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4) and the PS2 disc's
WARRIORS.DIR / .WAD, compared with the NTSC-U Xbox disc: a full (Redump-style) image of 7,825,162,240 bytes whose
default.xbe has SHA1 fe246a74501cd10d2ce89ca5dbda5080d2b2edcc. No runtime claims. Survey date 2026-10-04.
Addresses written XBE 0x... are virtual addresses in that default.xbe (base 0x00010000), not in
SLUS_212.15. The executable was only read as far as the archive and its name hash needed (strings, headers and
four short functions); no game logic was disassembled here. Whether the executable helps reverse engineering is
measured on Xbox executable.
Purpose¶
The decision (a maintainer, 2026-10-07): Coney installs from the player's discs, and the Xbox disc is the preferred
source of assets, the PS2 disc filling the gaps. At install coney-tools extract converts the assets to open
formats outside the repository; given the Xbox disc as well, it takes each asset from whichever disc has the better
version. The PS2 version stays the only reference for behaviour, and level scripts, data tables and game text are
not taken from either disc. This page says what is on the Xbox disc, how its archive, names and graphics formats
work, how each kind of asset corresponds to the PS2's, and the rule that picks a disc per asset.
The short answer, kind by kind (Asset kinds): textures and movies gain; nothing else does. 23,210 of the PS2's 26,308 textures have a matching Xbox texture that is larger, nearly all twice the width and height. The 16 movies have 1280 × 720 versions, 15 of them as long as the PS2's. Models, animations, collision and the rest are the same data (the models re-encoded as float triangle lists), and the sound is the same recordings at the same or a lower sample rate, so those stay the PS2 disc's.
Original structure¶
The disc¶
The image is an XGD1 disc: the game partition (XDVDFS) starts at byte 0x18300000, where sector 32 holds the
MICROSOFT*XBOX*MEDIA volume descriptor. The file tree has 2 directories and 431 files, 5,620,148,362 bytes in all:
| Path | Files | Bytes | What |
|---|---|---|---|
default.xbe |
1 | 4,636,672 | The executable |
XBoxWad.idx |
1 | 169,700 | The archive index, see The archive |
Main.wad0-Main.wad3 |
4 | 958,392,320 | Archive volumes: resources, scenes, scripts, streamed worlds |
Paks.wad0-Paks.wad2 |
3 | 775,854,080 | Archive volumes: 883 packs |
System.wad0 |
1 | 1,679,360 | Archive volume: the global pack and a scene list |
audio/xbox000.xwb-xbox368.xwb |
369 | 2,217,542,292 | XACT wave banks |
audio/xbox000.xsb |
1 | 4,544,708 | XACT sound bank |
audio/dsstdfx.bin, Warriors.bin, version.txt |
3 | 33,854 | DSP effects image and two small files |
bik/*.bik |
48 | 1,657,295,376 | Bink movies: the PS2's 16, each in three versions |
The PS2 disc, for comparison, has one archive (WARRIORS.WAD, 1.5 GB), its sound in IOP/BFW.SND and
IOP/MUSIC.SND (2.4 GB) and 16 movies in PSS/ (Recon).
The executable¶
| Field | Value |
|---|---|
| Title, title ID | The Warriors, 0x5454009B; region 1 (North America); certificate version 3 |
| Build | header time stamp 2005-09-25 21:34:01 UTC, certificate 2005-09-28 |
| Debug paths | d:\Warriors\Build\rundata_XBox\WarriorsDVD.exe, d:\Warriors\Source\XBox\Final DVD no deploy\XBox.pdb |
| Sections | .text (0x34befc bytes), .rdata, .data, and library sections D3D, D3DX, XGRPH, DSOUND, XACTENG, BINK*, WMADEC, XPP, DOLBY |
| Libraries | XDK 1.0.5849 for all nine: XAPILIB, D3D8, D3DX8, XGRAPHC, DSOUND, XACTENG, XBOXKRNL, LIBCPMT, LIBCMT |
| Middleware | Lua 4.0.1 (the version string), Bink (its own sections), XACT for sound, WMA decoding for custom soundtracks |
There is no RenderWare in the Xbox build. None of the PS2's RenderWare strings (Renderware, Renderware Calloc,
the DevRWGeneric.cpp and DevRWDebug.cpp paths) occur in the XBE, and no file in the Xbox archive contains a
RenderWare stream (below). The graphics device is the port's own Direct3D 8 code: GraphicsDeviceXBox,
GraphicsTextureXBox, GraphicsResourceXBox. Evidence: inferred, from the strings and the data.
Source paths. The XBE embeds 149 .cpp and .inl paths (plus 5 headers) of the form
\Warriors\Source\<Subsystem>\<File>.cpp, against the PS2's 153 (Source map); 135 are shared.
The same string scan finds 152 of the PS2's 153.
- Xbox only (14):
Device/XBox/(DS_XBoxDevice,DS_XBoxFileSys,DS_XBoxStreamFileSys,fileio/XBoxstream,sound/XBoxAudioDevice),Graphics/Devices/XBox/(GraphicsDeviceXBox,GraphicsResourceXBox,GraphicsShadowXBox,ParticleSystemXBox,XBoxParticleSystem),World/XBox/(WorldLevelXBox,WorldManagerXBox),Scripting/ScriptLuaXBox.inlandWadFile/WadFile.cpp, the Xbox archive reader. - PS2 only (17 of this scan's 152):
Device/ps2/(includingfileio/DVDWadIndexPS2.cppandfileio/RockWadIndexPS2.cpp),World/ps2/, the RenderWare device (DevRWGeneric.cpp,DevRWDebug.cpp),FileIO/FS_TCPSocketFileSys.cpp,Warriors/W_PS2SaveSystem.cpp,Scripting/ScriptLua.inlandlua-4.0.1/src/lmem.cpp.
So everything above the platform layer (game modes, AI, scenes, scripting, the chunk system) is the same code; the file system, renderer, world streaming and sound are platform-specific. Evidence: inferred.
How the executable compares with SLUS_212.15 for reverse engineering (function counts, anchors, decompiler
output, library signatures, mapping functions between the builds) is on Xbox executable.
Data¶
All values little-endian, as on the PS2.
The archive¶
The Xbox uses neither WARRIORS.DIR/.WAD nor loose files: it has one index, XBoxWad.idx, over eight archive
volumes (System.wad0, Main.wad0-3, Paks.wad0-2). WadFile.cpp replaces the PS2's DVDWadIndexPS2.cpp.
struct XboxWadHeader { // at 0x00
uint32_t volumeCount; // 8
uint32_t entryCount; // 10,587
uint32_t unknown; // 0x00100000
uint32_t volumesOffset; // 0x14
uint32_t entriesOffset; // 0x134 = 0x14 + 8 * 36
};
struct XboxWadVolume { // 36 bytes, volumeCount of them
char name[32]; // "System.wad0", "Main.wad0", ...; NUL-terminated, the rest is build-tool garbage
uint32_t minusOne; // 0xffffffff
};
struct XboxWadEntry { // 16 bytes, entryCount of them
uint32_t location; // bits 24-31: volume index; bits 0-23: offset in 1,024-byte units
uint32_t size; // bytes
uint32_t zero; // 0 in every entry
uint32_t nameHash; // see "Name hash" below
};
0x134 + 10,587 × 16 = 169,700, the file's size. All 10,587 entries lie inside their volume, at 2,048-byte
boundaries, in ascending offset order within each volume. Volume use: System.wad0 2 entries, Main.wad* 9,702,
Paks.wad* 883. Evidence: the lookup loop (16-byte entries, hash at +0x0c, count from the header's second
word) is confirmed (code) at XBE 0x000704a0; the path D:\XBoxWad.idx is loaded at XBE 0x0033f880; the rest of
the layout is inferred from the data, which it fits exactly.
Name hash¶
The Xbox hashes ee_files\<name> (backslash, no leading ./), lowercased, with the same standard CRC-32 as the
PS2 (Name hashing):
Unlike the PS2, the Xbox folder is not flat: packs live in ee_files\paks\ and standalone animations in
ee_files\anims\. So the PS2 hash of a name never equals the Xbox hash (0 of 10,701 PS2 hashes occur in the Xbox
index), but the name does carry over.
Evidence: confirmed (code). XBE 0x0006c4f0 copies the prefix ee_files\ in front of the name, normalises the
path (XBE 0x00070170, which splits on \, / and :) and looks it up; XBE 0x00068590 is the hash: CRC-32 with
the table at 0x004a7bf0, initial value 0xffffffff, final complement, each character case-folded before it is
hashed. Corroborated by the name overlap below.
Name overlap¶
Of the 3,990 PS2 names recovered so far (WAD contents), 3,957 exist on the Xbox:
| PS2 kind | PS2 names | On the Xbox | Xbox name |
|---|---|---|---|
Scene records (.scn) |
2,760 | 2,760 | ee_files\<name> |
Animations (.anm) |
563 | 563 | ee_files\anims\<name> |
Lua bytecode (.lua) |
275 | 275 | ee_files\<name> |
Packs (.pak) |
236 | 231 | ee_files\paks\<name> |
Levels (.lev) |
64 | 64 | ee_files\<name> |
Object lists (_objs.txt) |
63 | 63 | ee_files\<name> |
scene_list.cnk, the global .glr |
2 | 2 | ee_files\<name> |
Sound banks and indexes (.msb, .msd) |
26 | 0 | none: the Xbox uses XACT |
Memory card icon (.ico) |
1 | 0 | none |
The five packs not found belong to levels 60, 64 and 100. Adding the Xbox-only scene records (by their own names)
and levels gives 4,038 names, which cover 4,041 of the 10,587 entries: three hashes occur twice in the index
(paks\global.pak in both System.wad0 and Paks.wad0, scene_list.cnk in both System.wad0 and Main.wad0,
and one scene record stored twice at two different sizes). As on the PS2, the named entries from index 210 on are
in name order. Evidence: inferred (hash matches; with these three fixed prefixes chance matches are negligible).
By resource. The standalone models and textures are unnamed on both discs, but the chunk container's resource hashes are the same on both (they hash the resource's own name, with no folder): 2,092 texture resources, all 1,541 PS2 model resources, 616 of 617 animations and all 53 characters have an Xbox resource with the same hash. This, not the WAD name, is what a resolver should key on.
Kinds of entry¶
| Kind | PS2 entries | Xbox entries | Xbox bytes | Same bytes as the PS2? |
|---|---|---|---|---|
| Pack | 881 | 884 | 776,567,280 | no: their graphics chunks differ |
| Resource: texture dictionaries (with or without a particle page) | 2,085 | 2,062 | 138,983,792 | no: Xbox textures |
| Resource: models (plain and skinned) | 1,477 | 1,477 | 20,586,960 | no: Xbox models |
| Resource: animations | 564 | 564 | 2,600,960 | no: same sizes, different keyframe bytes (below) |
| Resource: characters | 53 | 53 | 7,532,016 | no |
Resource: levels (.lev) |
64 | 95 | 44,488,240 | no |
| Resource: global, scene list | 3 | 4 | 1,525,008 | no |
Scene records (.scn) |
2,765 | 2,814 | 200,615,808 | all 2,765 PS2 records identical; 49 more on the Xbox |
| Lua 4.0 bytecode | 467 | 503 | 10,931,421 | all 467 PS2 scripts identical; 36 more on the Xbox |
| Object lists and other text | 63 | 81 | 1,154,288 | 74 identical; includes the font metrics (METRICS1) |
| Streamed world | 2,229 | 2,048 | 516,969,472 | no: a different split (below) |
Sound banks (.msb, .msd) |
42 | 0 | moved to XACT | |
XACT sound bank (SDBK) |
0 | 1 | 4,544,708 | identical to audio/xbox000.xsb |
24-bit bitmap (BM, 256 × 128) |
1 | 1 | 98,358 | identical |
| Font metrics, memory card icon, older RenderWare streams | 7 | 0 | font metrics counted as text above | |
| Total | 10,701 | 10,587 | 1,726,598,311 |
Evidence: inferred: every entry is assigned by the same structural tests as on the PS2 (the container parse,
the Lua header, the scene-record name, magic numbers), and "identical" means equal SHA1 against some PS2 entry.
(The bitmap was counted among the PS2's sound banks on WAD contents; it
is one 256 × 128 24-bit BM image.)
Formats, kind by kind¶
Chunk container: the same. Packs (marker 0xDE686795), resources and chunks use the PS2's 16-byte headers
unchanged (WAD contents); all 884 packs and 4,255 standalone resources
parse to their exact size (43,104 resources, 108,836 chunks). The XBE's chunk-type table (XBE 0x00458360, 84
records of 12 bytes and a terminator, found through its Renderware Texture Dic name) has the same 84 names in
the same order as the PS2's at 0x0050b2e0, and the same types have onLoaded and readFromStream handlers.
The 29 types the WAD uses are the same on both. Evidence: inferred (table read from the XBE's data, as on
Chunk system).
Graphics chunks: Xbox-only formats, no RenderWare. On the PS2, types 0x15 (world), 0x2a (texture dictionary)
and 0x47 (model) hold RenderWare streams. On the Xbox they hold the port's own resources, each starting with
{u32 kind, u32 tag, ...}:
| Chunk | First words | Chunks |
|---|---|---|
0x2a texture |
0, 0x6f |
18,245 |
0x47 model, unskinned |
2, 0x6d |
18,993 |
0x47 model, skinned (with 0x28 bone offsets) |
1, 0x76 |
2,370 |
0x47 other |
3, 0x67 |
157 |
0x15 world (sector BSP) |
6, 0x66 |
95 |
| Streamed world entries | 4, 0x6f / 5, 0x6a |
1,969 / 79 |
Bytes 0x20-0x7f of a texture chunk, and similar stretches elsewhere, are uninitialised memory of the build tool
(stack and heap addresses), so these are memory images written by the tools. Evidence: inferred.
Textures (0x2a). One texture per chunk, laid out as: a 32-byte header {0, 0x6f, 1, dataSize, 0x20, 0x10,
pointer, 0}, 96 bytes of tool garbage, dataSize bytes of pixel data (every mip level), then a 64-byte
descriptor: an Xbox Direct3D texture header (Common = 0x00040001, Data = 0, Lock = 0, Format, Size = 0),
0xee filler, and u16 width, u16 height at +0x28; then 0xee filler to the chunk's end. The Format word's
bits 8-15 are the D3D format, 16-19 the mip count, 20-23 and 24-27 log2 of width and height; in all 18,245 chunks
they agree with the stored width and height. Every texture is block-compressed: 0x0C DXT1, 0x0E DXT2/3, 0x0F
DXT4/5, which are not swizzled on the Xbox. Evidence: inferred (all 18,245 parse; the format numbers are the
XDK's).
Models (0x47) and worlds (0x15). Decoded far enough to compare: Models, Levels and the
streamed world.
Animations, characters, collision, paths. Non-graphics chunks are the same data, often byte for byte: character
data (0x08, 53 of 53), anim ranges (0x45, 53 of 53), bone offsets (0x28, 153 of 153), particle pages (0x4c,
537 of 541), path data (0x40), subtitles (0x51), occluders (0x53) and most collision chunks are identical in
the 30 levels compared. Keyframes (0x00) have the same sizes but different bytes in 551 of 616; the level header
(0x17) grows from 48 to 128 bytes. Evidence: inferred (SHA1 per chunk, matched by resource hash).
Lua. All 503 scripts carry the PS2's header 1B 4C 75 61 40 01 04 04 04 20 06 09 08 (WAD
contents), and the 467 PS2 scripts are byte-identical on the Xbox.
Scene records. Same layout; all 2,765 PS2 records identical. The 49 extra records are cutscenes of two levels.
Levels and the streamed world. The Xbox has 95 level resources against 64: all 64 of the PS2's, plus 31 that
the PS2 does not ship: 2 front-end or arena levels, 14 test levels (named test...) and 15 more level<N>. The
streamed world is
split differently: 1,969 + 79 entries on the Xbox, 1,911 sector-atomics files + 159 world streams + 159 manifests on
the PS2 (Level files), with no manifests on the Xbox. A level's world can
only be taken whole from one disc; its names are under Levels and the streamed world. Evidence:
inferred.
Sound. XACT replaces the PS2's sound banks and IOP files: 369 wave banks (WBND version 3; 368 streaming)
hold 23,523 waves, all Xbox ADPCM except 2 PCM, plus one sound bank (SDBK, also stored in the archive). How they
map to the PS2's sounds: Sound, speech and music. Evidence: inferred (header fields per the XDK's XACT
bank layout).
Movies. The PS2's 16 movies exist on the Xbox in three versions each: Movies. The XBE's %s_hd,
%s_w, %s_ls_%d_w and %s_w_pal format strings show the same three-way choice for loading screens.
Quality of matched textures¶
Over the 2,092 texture resources on both discs, comparing the first instance of each; 2,027 hold the same number of textures on both (65 differ: 35 have two more on the Xbox, 30 one fewer).
| PS2 | Xbox | |
|---|---|---|
| Textures compared | 2,027 | 2,027 |
| Same width and height | 1,413 | 1,413 |
| Twice the width and height | 612 | |
| Larger in another way / smaller | 2 / 0 | |
| Total pixels | 128,302,496 | 155,473,872 (× 1.21) |
| Largest side | 512 | 1,024 (one texture) |
| Textures with a side of 512 or more | 370 | 424 |
| Pixel format | palettised: 8-bit (1,967 of all 2,205), 4-bit (238) | DXT1 (1,484 of all 2,312), DXT2/3 (375), DXT4/5 (453) |
| Mipmaps | full chains (1 to 10 levels) | |
Bytes of 0x2a chunks, every instance |
330,158,928 | 501,984,640 |
coney-tools xbox textures --ps2 reproduces the 2,092 shared resources, the 2,027 with equal counts and the 35 / 30
that differ. Over the first instance of each of the 2,092 it counts 2,222 Xbox textures (DXT1 1,403, DXT2/3 375,
DXT4/5 444) and 2,182 PS2 ones, so the pixel-format row's totals (2,312 and 2,205) were taken over a set the tool does
not reproduce; the format shares are close either way.
So about 30% of the textures double in resolution; the rest are the PS2 images re-encoded from 4- and 8-bit palettes to DXT, which trades palette banding for block artefacts rather than adding detail. Evidence: inferred.
Resource images¶
Every Xbox graphics object is a memory image the build tools wrote: a texture chunk (0x2a), a model chunk
(0x47), a sector BSP chunk (0x15), and the streamed world's files (below). One layout serves them all:
| Offset | Type | Meaning |
|---|---|---|
+0x00 |
u32 | kind: 0 texture, 1 skinned model, 2 model, 3 model with a frame tree, 4 world sector, 5 world level, 6 sector BSP |
+0x04 |
u32 | tag: 0x6f, 0x76, 0x6d, 0x67, 0x6f, 0x6a, 0x66 in the same order |
+0x08 |
u32 | number of Direct3D resource headers |
+0x0c |
u32 | dataSize: bytes of GPU data from +0x80 (texels of every mip level, vertices, indices) |
+0x10 |
u32 | bytes of the resource headers after the data (padded to 16) |
+0x14 |
u32 | bytes of the object after the headers |
+0x18-+0x7f |
build-tool memory (stack and heap addresses, log text) |
The headers are the XDK's: a texture's is 20 bytes (Common, Data, Lock, Format, Size), a vertex or index
buffer's 12 (Common, Data, Lock); Common bits 16-18 give the type (0 vertex buffer, 1 index buffer, 4
texture) and Data is the offset into the data block. Evidence: confirmed (code): the 0x47 reader (XBE
0x0009d030, entry 0x47 of the chunk-type table at XBE 0x00458360, also used by 0x09 and 0x25) loads the
image and dispatches on kind 1, 2 or 3; XBE 0x000bb190 walks the +0x08 headers after the data, registering a
texture (20 bytes) or vertex buffer (12) and rebasing an index buffer's Data (12); the object starts
dataSize + headersSize after +0x80. The kind and tag pairs and the sizes are inferred from the data, which they
fit in every graphics chunk and world file.
A texture's Format word holds the D3D format (bits 8-15), the mip count (16-19) and log2 of the width (20-23) and
height (24-27). Every texture on the disc is block-compressed and unswizzled: 0x0C DXT1, 0x0E DXT2/3, 0x0F
DXT4/5. The 0x0F ones are DXT5, straight alpha: in 141 of 142 same-size matched textures with partly
transparent texels the colours equal the PS2's, not the PS2's multiplied by alpha (inferred); the 0x0E ones have
no partly transparent texels to measure.
Asset kinds, Xbox against PS2¶
| Kind | Xbox format | Correspondence | Coverage | Gain | Coney takes |
|---|---|---|---|---|---|
| Textures (resources) | DXT1/3/5 images, no names | resource hash, then position, checked by content | 578 of 2,343 larger | 2× width and height | Xbox when larger |
| Textures (streamed world) | DXT images in .xlev / .xsec |
level name, then content | 22,632 of 23,965 larger | 2× (33 more) | Xbox when larger |
| Models | float triangle lists | resource hash | 1,541 of 1,541 | none: same meshes | PS2 |
| Animations | the PS2's chunks | resource hash | 616 of 617 | none: same keys | PS2 (behaviour) |
| Levels, collision, paths | the PS2's chunks | resource hash, level name | 64 of 64 | none | PS2 (behaviour) |
| World geometry | Xbox vertex and index buffers | level name | every PS2 level | none known | PS2 |
| Sound effects and speech | XACT, Xbox ADPCM | name hash in the sound list | 23,147 of 25,395 | none: same rates | PS2 |
| Music | XACT, one wave bank a track | track name | 345 of 345 | none: 26,500 Hz against 30,000 | PS2 |
| Movies | Bink i, three versions each |
file name | 16 of 16 | 1280 × 720 | Xbox _hd when as long |
Textures¶
Xbox textures carry no names, so each PS2 texture is matched by content
(repo:python/src/coney_tools/xbox_match.py): an Xbox candidate is shrunk to
the PS2 texture's size with a box filter, and the score is the mean colour difference weighted by both alphas plus the
mean alpha difference (0-255 scale). Right pairs score about 5-15 (palette against DXT, plus the detail a larger texture
adds; the colours carry no systematic offset: a fit over level99's matched texels gives Xbox = 0.91 × PS2 + 7 to 12 per
channel); unrelated ones 25 and more. A low score alone is not enough: shrunk far enough, any texture of about the same
colour scores low against a small or blurry PS2 one. So a match also needs the same structure: the correlation of the
two textures' alpha-weighted luminance, at the PS2 size, of 0.5 or more (right pairs reach 0.9 and more; same-coloured
wrong ones lie near 0). A match needs a score under 20, that structure (unless the PS2 texture is flat) and the same
aspect ratio. The candidates of a resource's dictionary are the textures of the Xbox resource with the same hash (the
one at the same position first, when both hold the same number); those of a streamed-world dictionary are all textures
of the level's .xlev and .xsec files, since the PS2 splits a level's world differently (s and d halves, world
streams and sector parts).
The rule: the Xbox texture replaces the PS2 one when it matches and is larger. One of the same size is the PS2 image
re-encoded from a palette to DXT blocks and stays PS2. One more than twice as wide needs a structure of 0.9 or more (a
flat PS2 texture is never replaced by one): without that test, 183 world textures took an Xbox texture 4 to 32 times as
wide, and by eye most were another picture of the same colour (a wooden box for a gravel ground); with it 33 remain, a
fire-truck texture (128 to 512) and one concrete texture at 4 × 4 in 31 world files. Over the NTSC-U pair (coney-tools
extract --xbox, 2026-10-07):
| Resource dictionaries | Streamed world | All | |
|---|---|---|---|
| PS2 textures | 2,343 | 23,965 | 26,308 |
| Replaced by a larger Xbox texture | 578 | 22,632 | 23,210 |
| ... twice the width and height | 578 | 22,599 | 23,177 |
| ... 4 to 16 times | 0 | 33 | 33 |
| Pixels of the replaced textures, PS2 | 8,656,656 | 92,966,024 | 101,622,680 |
| Pixels of the replaced textures, Xbox | 34,626,624 | 372,094,496 | 406,721,120 |
| Kept: the Xbox match is the same size | 1,711 | ||
| Kept: no candidate matches | 1,354 | ||
| Kept: no Xbox candidates (resource not on the Xbox) | 33 |
Evidence: inferred (measured by the tool; samples scoring 10-36 were compared by eye: right pairs up to about 18, wrong ones from about 25; the structure limits from 300 random 2× matches, 299 right and kept, and the 183 wider ones, compared by eye).
Models¶
Kind 2 (1,413 distinct resources, all single-mesh) has a 0x20-byte object after one vertex and one index buffer header:
| Offset | Type | Meaning | Evidence |
|---|---|---|---|
+0x04 |
u32 | FVF: 0x152 (position, normal, colour, one UV; 1,105), 0x252 (two UV sets; 210), 0x52 (no UV; 98) |
confirmed (code) at XBE 0x000bd3c0 (set as the vertex shader); counts inferred |
+0x08 |
u8 | primitive type, 5 (triangle list) in all | confirmed (code) at XBE 0x000bd3c0 |
+0x09 |
u8 | vertex stride: 36, 44, 28 for the FVFs above | confirmed (code) at XBE 0x000bd3c0 |
+0x0a |
u16 | 0 (814) or 1 (599); no reader found | inferred |
+0x0c |
u16 | triangles (indices = 3 ×) | confirmed (code) at XBE 0x000bd3c0 |
+0x10 |
f32 | bounding radius | speculative |
+0x18, +0x1c |
ptr | the vertex and index buffer headers (rebased on load) | confirmed (code) at XBE 0x000bd4e0 |
Vertices are float positions, float normals, a D3DCOLOR and float UVs. The texture is not in the model: the draw
binds the one its caller passes (XBE 0x000bd3c0), as the PS2 takes the first texture of the dictionary the Object
or Character List gives the model.
Kind 1 (153 skinned characters) has a 0x820-byte object: 32 matrices of 64 bytes at +0x04, the triangle count
(u16) at +0x804 and the headers of two vertex streams and an index buffer at +0x808, +0x80c, +0x810
(confirmed (code): XBE 0x000bec60 rebases them; XBE 0x000beca0 draws stream 0 with stride 0x20 and stream 1
with stride 0x12, loading one matrix per bone into vertex shader constants from c60, three registers each).
Stream 0 is a float position, a packed normal (11:11:10 signed; unit length in every vertex sampled) and two float
pairs, the first the UV; stream 1 is three s16 bone references (bone × 3, a constant-register offset; −3 for an
unused one) and three float weights summing to 1. Inferred from the data, except the strides and the draw.
Kind 3 (6 resources, in levels) is a tree of 0x68-byte nodes (+0x50, +0x54, +0x58: three node indices, −1 for
none; +0x60, +0x64: vertex and index buffer), rebased by XBE 0x000b83a0 (confirmed (code)).
Against the PS2. Each PS2 model is one atomic with one material too. Of 1,382 kind-2 pairs, 1,200 have the same triangle count, 104 more on the Xbox and 78 fewer, most of these an 82-triangle mesh standing in for many different PS2 models; of 153 skinned pairs, 145 are the same. In all, 321,262 Xbox triangles against 308,036 (kind 2) and 183,893 against 184,029 (kind 1). The Xbox meshes are the PS2's at float precision as triangle lists: no gain, so models stay PS2. Evidence: inferred (counted against the PS2 clumps of the same resource hash).
Animations¶
Keyframe chunks (0x00) match by resource hash (616 of 617) and have the same sizes. 71 are identical; in 326 only
byte +1 of the 8-byte keys differs, which the sampler does not read (Animation data);
in the other 290 the differences also reach the 24-byte events at the end, mostly their bytes +0x0c-+0x17 (a
transform's rotation and fields not traced). Animations are behaviour (events drive effects and paired moves), so the
PS2's stay. Evidence: inferred (byte comparison).
Levels and the streamed world¶
The streamed world is named ee_files\sectors\<level>.xlev (79 entries, kind 5) and
ee_files\sectors\<level>_<n>.xsec (1,969, kind 4): all 2,048 world entries take these names, and one more .xsec
name hashes to a texture dictionary resource. Evidence: confirmed (name hash); the names were read from
build-tool log text left in the images (Writing sector: rundata_XBox/ee_files/sectors/level1_0.xsec, Saving BSP:
.../sectors/level1.xlev). A .xlev holds the level's textures and an object of sector records; a .xsec holds a
sector's vertex and index buffers (stride 28: float position, packed normal, colour, float UV; inferred) and its own
textures. Level resources, collision and paths are the PS2's data (Formats, kind by kind).
The world geometry is not converted: the PS2 world is the reference and the Xbox one models the same city; only its
textures are taken.
Sound, speech and music¶
The Xbox sound list (chunk 0x29 of the global resource) has 23,523 records of 12 bytes {u32 hash, u32 sound
index, u16 wave bank, u8 volume, u8 class}, sorted by hash. The hash is the PS2's (CRC-32 of the name), so a PS2
sound finds its Xbox sound by name: 23,147 of the PS2's 25,395 hashes are on the Xbox, and 376 Xbox hashes are not on
the PS2. The records per bank equal each bank's wave count (bank 0: 542, 1: 1,014, 2: 19,276 speech and voices,
3: 2,228, 4: 99, then one each for 5-368). The sound index is global (it runs past a bank's wave count) and the sound
bank xbox000.xsb (XACT SDBK version 11, 23,523 entries) maps it to a wave; that table is not decoded. A debug copy
of the list, in another instance of the global resource, has the last 16 characters of each name in front of each
record (28 bytes; the PS2's has the same, 32 bytes). Evidence: inferred (the counts and the shared hashes).
The waves are Xbox ADPCM (4 bits a sample, like the PS2's ADPCM), mono at the PS2's rates (22,050 and 22,500 Hz for
most effects and speech). The music is 364 one-wave banks, stereo at 26,500 Hz (two at 30,000), named
vags/xboxmusic/music/<track> in the music list (chunk 0x31, the PS2's 345 records with that prefix), against the
PS2's 30,000, 44,100 and 32,250 Hz. The same recordings at no higher quality: the sound stays PS2.
Movies¶
Each of the 16 PS2 movies (640 × 448, Bink i, 48,000 Hz stereo) has three Xbox versions: <name>.bik (640 × 480,
4:3), <name>_w.bik (640 × 480, a 16:9 picture squeezed into 4:3) and <name>_hd.bik (1280 × 720, 16:9; 640 × 480
for trailer). The 16:9 versions are rendered wider, not cropped: at the same frame the _hd picture shows more of
the scene at both sides than the PS2's (compared by eye on l9_in; inferred). Most Xbox movies have 44,100 Hz audio;
plogo, silent on the PS2, has a track. The frame counts and rates match the PS2's to 0.1 s except l51_in (609.7 s
on the PS2, 617.7 s on the Xbox), a different cut. Evidence: inferred (Bink headers; frames by eye).
The rule: _hd replaces the PS2 movie when it has more pixels and lasts as long (to 0.5 s), since the subtitles are
timed against the PS2's. Over the NTSC-U pair 15 of the 16 are replaced (14 at 1280 × 720, trailer at 640 × 480);
l51_in stays the PS2's. A 16:9 movie needs the player to keep the file's aspect ratio (Movies).
Behaviour¶
How the Xbox finds a file, for comparison with File I/O: a name is prefixed with ee_files\,
normalised, hashed (case-folded CRC-32) and found by a linear scan of the 10,587 entries, as DVDWadIndex::Find
does on the PS2. Evidence: confirmed (code) at XBE 0x0006c4f0, 0x000704a0 and 0x00068590. Nothing else
about the port's behaviour is in scope: it is not a reference for Coney.
Coney's implementation¶
The survey as a check (done). coney-tools xbox (The coney-tools command line)
reads the disc as this page describes and prints counts only: python/src/coney_tools/xdvdfs.py (the XDVDFS reader:
full image, XISO or folder), chunks.py (the chunk container, shared with the PS2 archive) and xbox.py (the index,
the name hash, the kinds of entry, the resource index and the texture-chunk header). Run against the NTSC-U disc on
2026-10-04 it reproduces this page's counts: 431 files in 2 directories (5,620,148,362 bytes), 10,587 entries
(1,726,598,311 bytes) and every row of Kinds of entry; 43,104 resources and 108,836 chunks; the
graphics chunks by first words; 18,245 texture chunks (501,984,640 bytes), all readable; the resource-hash overlap
(2,092 texture dictionaries, 1,541 models, 616 of 617 animations, 53 characters); the 2,027 equal-count texture
resources; and 4,038 names (3,957 from the PS2's names). The rest of this section is the plan for the engine.
Policy. The PS2 disc is required and stays the reference for behaviour. The Xbox disc is optional, supplied by the player like the PS2 one, and only ever replaces assets, at install: never scripts, scenes, levels, collision, animations or anything the simulation reads. Its extra content (31 levels, 49 scene records, 36 scripts; most of the levels are test levels) is ignored.
Extraction (done). coney-tools extract [DISC] OUT_DIR --xbox XBOX_DISC
(guide) reads the Xbox disc straight from the image (XDVDFS, then XBoxWad.idx
and its volumes) and resolves per asset as Asset kinds says: the PS2 stages write everything, then
each Xbox texture that matches and is larger is written over the PS2 file at the same path, and each _hd movie that
is as long is copied in place of the PS2 one; the record in textures/index.json or movies/index.json gets
"source": "xbox" and keeps the PS2 size. The folder therefore has the same files with or without the Xbox disc, and
nothing reading it needs to know which disc an asset came from. Code:
repo:python/src/coney_tools/xbox_gfx.py (resource images, DXT),
repo:python/src/coney_tools/xbox_match.py (matching and the rule),
repo:python/src/coney_tools/xbox_source.py (the disc as a source) and
repo:python/src/coney_tools/extract_xbox.py (the hooks).
What the engine needs. The engine reads the discs' own formats, not the extracted PNGs, so to draw an Xbox
texture it needs: the XDVDFS reader and XBoxWad.idx, the texture
map to know which PS2 texture to swap, the resource image reader and DXT1/3/5
textures (a DXT raster or a CPU decode). A 16:9 movie needs the movie player to keep the file's aspect ratio rather
than 4:3.
Resource index¶
Finding an Xbox resource by its hash means parsing every pack and standalone resource of the archive (about 1 GB),
so the result is kept in a file, xbox-resources.bin (coney-tools xbox index, or extract --xbox-index). It is
Coney's own format, and the layout the engine's port reads. All values little-endian:
struct XboxResourceIndexHeader { // 32 bytes
char magic[4]; // "CXRI"
uint32_t version; // 1
uint32_t recordCount;
uint8_t indexSha1[20]; // SHA-1 of the XBoxWad.idx it was built from; another value means rebuild
};
struct XboxResourceIndexRecord { // 24 bytes, recordCount of them, sorted by (resourceHash, chunkType)
uint32_t resourceHash; // the chunk container's resource hash, the same on both discs
uint16_t chunkType; // 0x2a texture, 0x47 model, ...
uint16_t volume; // index into XBoxWad.idx's volume list
uint64_t offset; // byte offset of the chunk's data (after its 16-byte header) in that volume
uint32_t size; // bytes of chunk data
uint32_t entry; // the XBoxWad.idx entry holding it
};
Only the first instance of each resource hash is kept (packs repeat shared resources), every chunk of it, and the
chunks of one key stay in resource order (a texture dictionary's 0x2a chunks are its textures in order). Over the
NTSC-U disc: 10,445 records (250,712 bytes) for 4,218 resource hashes; 2,312 of them are texture chunks. Code:
repo:python/src/coney_tools/xbox_index.py.
Texture map¶
Which Xbox texture replaces which PS2 one is found by decoding and comparing both (Textures), far too
slow for loading a level, so it is done once, at install, and kept in xbox-textures.bin (coney-tools xbox
texture-map, or extract --xbox-texture-map). A reader looks a PS2 texture up by the dictionary it was loaded from
and its name, and gets where the Xbox texture lies in the archive volumes. All values little-endian:
struct XboxTextureMapHeader { // 56 bytes
char magic[4]; // "CXTM"
uint32_t version; // 1
uint32_t recordCount;
uint32_t reserved; // 0
uint8_t ps2DirSha1[20]; // SHA-1 of the PS2 disc's WARRIORS.DIR it was made from
uint8_t xboxIdxSha1[20]; // SHA-1 of the Xbox disc's XBoxWad.idx it was made from
};
struct XboxTextureMapRecord { // 32 bytes, recordCount of them, sorted by (kind, ps2Key, nameHash)
uint32_t ps2Key; // kind 0: the PS2 dictionary's resource hash; kind 1: the WARRIORS.DIR name hash
// of the streamed-world file (`_sec.wld`, `_ms<n>.sec`) holding the dictionary
uint32_t nameHash; // CRC-32 of the PS2 texture's name, lower-cased
uint16_t kind; // 0 resource dictionary, 1 streamed-world dictionary
uint16_t textureNumber; // the texture's position among the Xbox resource image's texture headers
uint16_t volume; // index into XBoxWad.idx's volume list
uint16_t reserved; // 0
uint64_t imageOffset; // byte offset of the Xbox resource image in that volume
uint32_t imageSize; // bytes of the resource image
uint16_t width, height; // the Xbox texture's size
};
A key that two PS2 textures share with different Xbox textures (two variants of one resource, a name used twice in one dictionary) is left out, so a reader never takes the wrong one. Over the NTSC-U pair: 23,210 records (742,776 bytes), one per replaced texture, none left out. Code: repo:python/src/coney_tools/xbox_texture_map.py.
Open questions¶
- DXT2/3. Whether the
0x0Etextures are premultiplied (DXT2) or not (DXT3): none of the matched ones has partly transparent texels to tell (the0x0Fones are straight DXT5). - The index header's third word (
0x00100000) and the volumes' trailing0xffffffff: unused by the lookup; probably a volume size limit and a flag. - Unnamed Xbox entries. 4,498 of 10,587 (models, textures, characters, 652 packs, 227 scripts, 18 text
entries) once the 2,048 world files are named; the subfolder scheme (
paks\,anims\,sectors\) suggests the rest also live in subfolders. - Sound index to wave. The table in
xbox000.xsbthat maps the sound list's index to a wave bank entry; only needed if an Xbox sound is ever wanted. - Model fields. The kind-2 model's
+0x0a(0 or 1) and the skinned vertex's second float pair. - Unmatched PS2 textures. 1,354 PS2 textures with Xbox candidates find no match: art the Xbox port changed, or candidates in another level's files.