1
0
Fork 0

Initial commit

This commit is contained in:
Henrik Hautakoski 2026-04-22 16:41:48 +02:00
commit 3304b53c41
38 changed files with 6573 additions and 0 deletions

View file

@ -0,0 +1,25 @@
<?php
use Doom\Texture\PNamesParser;
test('parses PNAMES lump names', function (): void {
$data = pack('V', 3)
. pack('a8', 'BROWN1')
. pack('a8', 'STARTAN3')
. pack('a8', 'BIGDOOR2');
$pNames = PNamesParser::parseBytes($data);
expect($pNames)->toHaveCount(3);
expect($pNames)->toBe(['BROWN1', 'STARTAN3', 'BIGDOOR2']);
expect($pNames[1] ?? null)->toBe('STARTAN3');
expect($pNames[10] ?? null)->toBeNull();
expect(array_search('BIGDOOR2', $pNames, true))->toBe(2);
expect(array_search('MISSING', $pNames, true))->toBeFalse();
});
test('throws on truncated PNAMES lump', function (): void {
$data = pack('V', 2) . pack('a8', 'ONLYONE');
expect(fn () => PNamesParser::parseBytes($data))->toThrow(RuntimeException::class);
});

View file

@ -0,0 +1,135 @@
<?php
use Doom\Texture\TextureResolver;
use Doom\Wad\File;
use Doom\Wad\Types\Lump;
function s16(int $value): string
{
return pack('v', $value & 0xFFFF);
}
/**
* @param array<int, array{originX:int, originY:int, patch:int, stepDir:int, colorMap:int}> $patches
*/
function textureRecord(string $name, bool $masked, int $width, int $height, array $patches): string
{
$data = pack('a8VvvVv', $name, $masked ? 1 : 0, $width, $height, 0, count($patches));
foreach ($patches as $patch) {
$data .= s16($patch['originX']);
$data .= s16($patch['originY']);
$data .= pack('v', $patch['patch']);
$data .= pack('v', $patch['stepDir']);
$data .= pack('v', $patch['colorMap']);
}
return $data;
}
/**
* @param string[] $records
*/
function textureXData(array $records): string
{
$count = count($records);
$offset = 4 + ($count * 4);
$offsets = [];
$body = '';
foreach ($records as $record) {
$offsets[] = $offset;
$body .= $record;
$offset += strlen($record);
}
$data = pack('V', $count);
foreach ($offsets as $itemOffset) {
$data .= pack('V', $itemOffset);
}
return $data . $body;
}
/**
* @param string[] $names
*/
function pnamesData(array $names): string
{
$data = pack('V', count($names));
foreach ($names as $name) {
$data .= pack('a8', $name);
}
return $data;
}
function appendLump(File $wad, string $name, string $data): void
{
$index = $wad->lumps->count();
$wad->lumps->push(new Lump(
offset: 0,
size: strlen($data),
name: strtoupper($name),
data: $data,
index: $index,
));
}
test('resolves textures using PNAMES and patch lumps', function (): void {
$wad = new File();
appendLump($wad, 'PNAMES', pnamesData(['PATCHA', 'PATCHB']));
appendLump($wad, 'TEXTURE1', textureXData([
textureRecord('TEXA', false, 64, 64, [
['originX' => 0, 'originY' => 0, 'patch' => 0, 'stepDir' => 0, 'colorMap' => 0],
]),
textureRecord('TEXB', false, 32, 16, [
['originX' => 8, 'originY' => -2, 'patch' => 0, 'stepDir' => 0, 'colorMap' => 0],
]),
]));
appendLump($wad, 'TEXTURE2', textureXData([
textureRecord('TEXA', true, 128, 32, [
['originX' => 4, 'originY' => 5, 'patch' => 1, 'stepDir' => 0, 'colorMap' => 0],
['originX' => 0, 'originY' => 0, 'patch' => 5, 'stepDir' => 0, 'colorMap' => 0],
]),
]));
appendLump($wad, 'PATCHA', '');
$resolved = $wad->resolveTextures();
expect($resolved)->toHaveCount(2);
expect($resolved)->toHaveKey('TEXA');
expect($resolved)->toHaveKey('TEXB');
$texA = $resolved['TEXA'];
expect($texA->masked)->toBeTrue();
expect($texA->width)->toBe(128);
expect($texA->height)->toBe(32);
expect($texA->patches)->toHaveCount(2);
expect($texA->patches[0]->patchName)->toBe('PATCHB');
expect($texA->patches[0]->lumpIndex)->toBeNull();
expect($texA->patches[0]->isResolved())->toBeFalse();
expect($texA->patches[1]->patchName)->toBeNull();
$texB = TextureResolver::forWad($wad)->resolveByName('texb');
expect($texB)->not->toBeNull();
expect($texB->patches[0]->patchName)->toBe('PATCHA');
expect($texB->patches[0]->lumpIndex)->toBe(3);
expect($texB->patches[0]->isResolved())->toBeTrue();
});
test('throws when PNAMES lump is missing', function (): void {
$wad = new File();
appendLump($wad, 'TEXTURE1', textureXData([
textureRecord('TEXA', false, 16, 16, [
['originX' => 0, 'originY' => 0, 'patch' => 0, 'stepDir' => 0, 'colorMap' => 0],
]),
]));
expect(fn () => $wad->resolveTextures())->toThrow(RuntimeException::class);
});

View file

@ -0,0 +1,69 @@
<?php
use Doom\Texture\TextureXParser;
function le16s(int $value): string
{
return pack('v', $value & 0xFFFF);
}
test('parses TEXTUREX definitions and patch references', function (): void {
$headerSize = 4 + (2 * 4);
$offset0 = $headerSize;
$offset1 = $offset0 + 32;
$texture0 = pack('a8VvvVv', 'BRICK1', 0, 64, 128, 0, 1)
. le16s(8)
. le16s(-4)
. pack('v', 3)
. pack('v', 0)
. pack('v', 0);
$texture1 = pack('a8VvvVv', 'METAL2', 1, 32, 32, 0, 2)
. le16s(0)
. le16s(0)
. pack('v', 7)
. pack('v', 0)
. pack('v', 0)
. le16s(12)
. le16s(4)
. pack('v', 8)
. pack('v', 0)
. pack('v', 0);
$data = pack('V', 2)
. pack('V', $offset0)
. pack('V', $offset1)
. $texture0
. $texture1;
$textureX = TextureXParser::parseBytes($data);
expect($textureX->numTextures)->toBe(2);
expect($textureX->offsets)->toBe([$offset0, $offset1]);
expect($textureX->textures)->toHaveCount(2);
$first = $textureX->textures[0];
expect($first->name)->toBe('BRICK1');
expect($first->masked)->toBeFalse();
expect($first->width)->toBe(64);
expect($first->height)->toBe(128);
expect($first->patches)->toHaveCount(1);
expect($first->patches[0]->originX)->toBe(8);
expect($first->patches[0]->originY)->toBe(-4);
expect($first->patches[0]->patch)->toBe(3);
$second = $textureX->firstTextureByName('metal2');
expect($second)->not->toBeNull();
expect($second->masked)->toBeTrue();
expect($second->patches)->toHaveCount(2);
expect($second->patches[1]->originX)->toBe(12);
expect($second->patches[1]->originY)->toBe(4);
expect($second->patches[1]->patch)->toBe(8);
});
test('throws on invalid texture offset', function (): void {
$data = pack('V', 1) . pack('V', 999);
expect(fn () => TextureXParser::parseBytes($data))->toThrow(RuntimeException::class);
});