Initial commit
This commit is contained in:
commit
3304b53c41
38 changed files with 6573 additions and 0 deletions
81
tests/Unit/Image/RendererTest.php
Normal file
81
tests/Unit/Image/RendererTest.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
use Doom\Image\Renderer;
|
||||
use Doom\Picture\Picture;
|
||||
use Intervention\Image\Encoders\PngEncoder;
|
||||
|
||||
/**
|
||||
* @return array<int, array{0:int,1:int,2:int}>
|
||||
*/
|
||||
function testPalette(): array
|
||||
{
|
||||
$palette = array_fill(0, 256, [0, 0, 0]);
|
||||
$palette[1] = [255, 0, 0];
|
||||
$palette[2] = [0, 255, 0];
|
||||
|
||||
return $palette;
|
||||
}
|
||||
|
||||
test('renders a patch to PNG bytes using configured palette', function (): void {
|
||||
$picture = new Picture(
|
||||
width: 2,
|
||||
height: 2,
|
||||
left: 0,
|
||||
top: 0,
|
||||
posts: [
|
||||
0 => [
|
||||
['y' => 0, 'topdelta' => 0, 'pixels' => [1]],
|
||||
],
|
||||
1 => [
|
||||
['y' => 1, 'topdelta' => 1, 'pixels' => [2]],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$png = (string) (new Renderer(testPalette()))
|
||||
->createPicture()
|
||||
->render($picture)
|
||||
->encode(new PngEncoder());
|
||||
|
||||
expect(substr($png, 0, 8))->toBe("\x89PNG\r\n\x1A\n");
|
||||
|
||||
$image = imagecreatefromstring($png);
|
||||
expect($image)->not->toBeFalse();
|
||||
|
||||
$topLeft = imagecolorsforindex($image, imagecolorat($image, 0, 0));
|
||||
expect([$topLeft['red'], $topLeft['green'], $topLeft['blue'], $topLeft['alpha']])->toBe([255, 0, 0, 0]);
|
||||
|
||||
$bottomLeft = imagecolorsforindex($image, imagecolorat($image, 0, 1));
|
||||
expect($bottomLeft['alpha'])->toBe(127);
|
||||
|
||||
$bottomRight = imagecolorsforindex($image, imagecolorat($image, 1, 1));
|
||||
expect([$bottomRight['red'], $bottomRight['green'], $bottomRight['blue'], $bottomRight['alpha']])->toBe([0, 255, 0, 0]);
|
||||
|
||||
imagedestroy($image);
|
||||
});
|
||||
|
||||
test('renders with default palette when custom one is not provided', function (): void {
|
||||
$picture = new Picture(
|
||||
width: 1,
|
||||
height: 1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
posts: [
|
||||
0 => [
|
||||
['y' => 0, 'topdelta' => 0, 'pixels' => [1]],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$png = (string) (new Renderer())
|
||||
->createPicture()
|
||||
->render($picture)
|
||||
->encode(new PngEncoder());
|
||||
$image = imagecreatefromstring($png);
|
||||
expect($image)->not->toBeFalse();
|
||||
|
||||
$pixel = imagecolorsforindex($image, imagecolorat($image, 0, 0));
|
||||
expect([$pixel['red'], $pixel['green'], $pixel['blue']])->toBe([31, 23, 11]);
|
||||
|
||||
imagedestroy($image);
|
||||
});
|
||||
90
tests/Unit/Image/TextureTest.php
Normal file
90
tests/Unit/Image/TextureTest.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
use Doom\Image\Renderer;
|
||||
use Doom\Picture\Picture;
|
||||
use Intervention\Image\Encoders\PngEncoder;
|
||||
|
||||
/**
|
||||
* @return array<int, array{0:int,1:int,2:int}>
|
||||
*/
|
||||
function imageTestPalette(): array
|
||||
{
|
||||
$palette = array_fill(0, 256, [0, 0, 0]);
|
||||
$palette[1] = [255, 0, 0];
|
||||
$palette[2] = [0, 255, 0];
|
||||
|
||||
return $palette;
|
||||
}
|
||||
|
||||
function testPatchPicture(): Picture
|
||||
{
|
||||
return new Picture(
|
||||
width: 2,
|
||||
height: 2,
|
||||
left: 0,
|
||||
top: 0,
|
||||
posts: [
|
||||
0 => [
|
||||
['y' => 0, 'topdelta' => 0, 'pixels' => [1]],
|
||||
],
|
||||
1 => [
|
||||
['y' => 1, 'topdelta' => 1, 'pixels' => [2]],
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test('can blit a Picture into a texture and encode png', function (): void {
|
||||
$texture = (new Renderer(imageTestPalette()))->createTexture(4, 3);
|
||||
$texture->blit(testPatchPicture(), 1, 1);
|
||||
|
||||
$png = (string) $texture->image()->encode(new PngEncoder());
|
||||
expect(substr($png, 0, 8))->toBe("\x89PNG\r\n\x1A\n");
|
||||
|
||||
$image = imagecreatefromstring($png);
|
||||
expect($image)->not->toBeFalse();
|
||||
|
||||
$pixel = imagecolorsforindex($image, imagecolorat($image, 1, 1));
|
||||
expect([$pixel['red'], $pixel['green'], $pixel['blue'], $pixel['alpha']])->toBe([255, 0, 0, 0]);
|
||||
|
||||
$transparent = imagecolorsforindex($image, imagecolorat($image, 0, 0));
|
||||
expect($transparent['alpha'])->toBe(127);
|
||||
|
||||
imagedestroy($image);
|
||||
});
|
||||
|
||||
test('can blit a png file into a texture and write it', function (): void {
|
||||
$sourcePath = tempnam(sys_get_temp_dir(), 'doom-src-');
|
||||
$outPath = tempnam(sys_get_temp_dir(), 'doom-out-');
|
||||
if ($sourcePath === false || $outPath === false) {
|
||||
throw new RuntimeException('Failed to allocate temp files.');
|
||||
}
|
||||
|
||||
$sourcePng = $sourcePath . '.png';
|
||||
$targetPng = $outPath . '.png';
|
||||
@unlink($sourcePath);
|
||||
@unlink($outPath);
|
||||
|
||||
try {
|
||||
$sourceTexture = (new Renderer(imageTestPalette()))->createTexture(2, 2);
|
||||
$sourceTexture->blit(testPatchPicture());
|
||||
$sourceTexture->image()->encode(new PngEncoder())->save($sourcePng);
|
||||
|
||||
$targetTexture = (new Renderer())->createTexture(6, 4);
|
||||
$targetTexture->blit($sourcePng, 2, 1);
|
||||
$targetTexture->image()->encode(new PngEncoder())->save($targetPng);
|
||||
|
||||
expect(is_file($targetPng))->toBeTrue();
|
||||
|
||||
$image = imagecreatefrompng($targetPng);
|
||||
expect($image)->not->toBeFalse();
|
||||
|
||||
$pixel = imagecolorsforindex($image, imagecolorat($image, 2, 1));
|
||||
expect([$pixel['red'], $pixel['green'], $pixel['blue']])->toBe([255, 0, 0]);
|
||||
|
||||
imagedestroy($image);
|
||||
} finally {
|
||||
@unlink($sourcePng);
|
||||
@unlink($targetPng);
|
||||
}
|
||||
});
|
||||
27
tests/Unit/Picture/DoomPaletteTest.php
Normal file
27
tests/Unit/Picture/DoomPaletteTest.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Doom\Picture\DoomPalette;
|
||||
|
||||
test('provides a valid default doom palette', function (): void {
|
||||
$palette = DoomPalette::default();
|
||||
|
||||
expect($palette->toArray())->toHaveCount(256);
|
||||
expect($palette->colorAt(0))->toBe([0, 0, 0]);
|
||||
expect($palette->colorAt(1))->toBe([31, 23, 11]);
|
||||
});
|
||||
|
||||
test('extracts palette data from PLAYPAL bytes', function (): void {
|
||||
$palette0 = str_repeat("\x00\x00\x00", 256);
|
||||
$palette1 = str_repeat("\x10\x20\x30", 256);
|
||||
|
||||
$palette = DoomPalette::fromPlaypal($palette0 . $palette1, 1);
|
||||
|
||||
expect($palette->toArray())->toHaveCount(256);
|
||||
expect($palette->colorAt(0))->toBe([16, 32, 48]);
|
||||
expect($palette->colorAt(255))->toBe([16, 32, 48]);
|
||||
});
|
||||
|
||||
test('rejects invalid PLAYPAL palette selection', function (): void {
|
||||
expect(fn () => DoomPalette::fromPlaypal("\x00", 0))->toThrow(RuntimeException::class);
|
||||
expect(fn () => DoomPalette::fromPlaypal(str_repeat("\x00", 768), -1))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
16
tests/Unit/Picture/PaletteTest.php
Normal file
16
tests/Unit/Picture/PaletteTest.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
use Doom\Picture\Palette;
|
||||
|
||||
test('creates palette from bytes and roundtrips', function (): void {
|
||||
$bytes = str_repeat("\x01\x02\x03", 256);
|
||||
$palette = Palette::fromBytes($bytes);
|
||||
|
||||
expect($palette->colorAt(0))->toBe([1, 2, 3]);
|
||||
expect($palette->colorAt(255))->toBe([1, 2, 3]);
|
||||
expect($palette->toBytes())->toBe($bytes);
|
||||
});
|
||||
|
||||
test('rejects invalid palette byte length', function (): void {
|
||||
expect(fn () => Palette::fromBytes("\x00"))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
25
tests/Unit/Texture/PNamesParserTest.php
Normal file
25
tests/Unit/Texture/PNamesParserTest.php
Normal 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);
|
||||
});
|
||||
135
tests/Unit/Texture/TextureResolverTest.php
Normal file
135
tests/Unit/Texture/TextureResolverTest.php
Normal 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);
|
||||
});
|
||||
69
tests/Unit/Texture/TextureXParserTest.php
Normal file
69
tests/Unit/Texture/TextureXParserTest.php
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue