Skip to content

Commit

Permalink
add support for uncompressed and LZ4 compressed chunks
Browse files Browse the repository at this point in the history
  • Loading branch information
KurtThiemann committed Jan 25, 2024
1 parent 7ccec35 commit 7a0fc84
Show file tree
Hide file tree
Showing 9 changed files with 453 additions and 145 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea/
vendor/
test.php
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Currently, only the Minecraft Anvil world format (Minecraft Java Edition) is sup
composer require aternos/thanos
```

To work with LZ4 compressed chunks (Minecraft 1.20.5+), you should also install the [PHP LZ4 extension](https://github.com/kjdev/php-ext-lz4).

## Usage

### CLI tool
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"aternos/nbt": "^v1.9.0",
"aternos/taskmaster": "^1.0"
},
"suggest": {
"ext-lz4": "Support for LZ4 compressed chunks (Minecraft 1.20.5+)"
},
"autoload": {
"psr-4": {
"Aternos\\Thanos\\": "src/"
Expand Down
86 changes: 60 additions & 26 deletions src/Chunk/AnvilChunk.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Aternos\Thanos\Chunk;

use Aternos\Thanos\Reader\LZ4BlockReader;
use Aternos\Thanos\Reader\RawReader;
use Aternos\Thanos\Reader\ReaderInterface;
use Aternos\Thanos\Reader\ZlibReader;
use Exception;

Expand Down Expand Up @@ -55,9 +58,9 @@ class AnvilChunk implements ChunkInterface
protected ?int $lastUpdate = null;

/**
* @var ZlibReader
* @var ReaderInterface
*/
protected ZlibReader $zlibReader;
protected ReaderInterface $reader;

/**
* @var bool
Expand Down Expand Up @@ -106,12 +109,42 @@ public function __construct($file, int $offset, array $regionPosition, int $regi
$this->yPos = intdiv($this->regionFileIndex, 32);

$this->readHeader();
$this->zlibReader = new ZlibReader(
$this->file,
$this->compression === 1 ? ZLIB_ENCODING_GZIP : ZLIB_ENCODING_DEFLATE,
$this->dataOffset,
$this->length - 5
);

$dataLength = $this->length - 5;
switch ($this->compression) {
case 1:
$this->reader = new ZlibReader(
$this->file,
ZLIB_ENCODING_GZIP,
$this->dataOffset,
$dataLength
);
break;
case 2:
$this->reader = new ZlibReader(
$this->file,
ZLIB_ENCODING_DEFLATE,
$this->dataOffset,
$dataLength
);
break;
case 3:
$this->reader = new RawReader(
$this->file,
$this->dataOffset,
$dataLength
);
break;
case 4:
$this->reader = new LZ4BlockReader(
$this->file,
$this->dataOffset,
$dataLength
);
break;
default:
throw new Exception("Unknown chunk compression type.");
}
}

/**
Expand All @@ -122,13 +155,13 @@ public function __construct($file, int $offset, array $regionPosition, int $regi
protected function readHeader(): void
{
$rawValue = unpack('N', fread($this->file, 4));
if($rawValue === false) {
if ($rawValue === false) {
throw new Exception("Failed to read chunk length.");
}
$this->length = $rawValue['1'] + 4;

$rawValue = unpack('C', fread($this->file, 1));
if($rawValue === false) {
if ($rawValue === false) {
throw new Exception("Failed to read chunk compression.");
}
$this->compression = $rawValue['1'];
Expand Down Expand Up @@ -164,10 +197,10 @@ public function getLength(): int
public function getInhabitedTime(): int
{
if ($this->inhabitedTime === null) {
$this->zlibReader->rewind();
$this->reader->rewind();
$data = $this->readAfter(hex2bin('04000D') . 'InhabitedTime', 8);
$rawData = $data !== null ? unpack('J', $data) : false;
if($rawData === false) {
if ($rawData === false) {
return -1;
}
$this->inhabitedTime = $rawData['1'];
Expand All @@ -187,18 +220,19 @@ public function getInhabitedTime(): int
*/
protected function readAfter(
string $str,
int $length,
int $limit = 1024 * 1024 * 10
): ?string {
$startPointer = $this->zlibReader->tell();
int $length,
int $limit = 1024 * 1024 * 10
): ?string
{
$startPointer = $this->reader->tell();
$strPointer = 0;
$valuePos = -1;
while (
!$this->zlibReader->eof()
&& $this->zlibReader->tell() < $startPointer + $limit
!$this->reader->eof()
&& $this->reader->tell() < $startPointer + $limit
) {
$data = $this->zlibReader->read(2048);
$dataStart = $this->zlibReader->tell() - strlen($data);
$data = $this->reader->read(2048);
$dataStart = $this->reader->tell() - strlen($data);
$pos = strpos($data, $str);
if ($pos !== false) {
$valuePos = $dataStart + $pos + strlen($str);
Expand All @@ -219,8 +253,8 @@ protected function readAfter(
if ($valuePos === -1) {
return null;
}
$this->zlibReader->seek($valuePos);
return $this->zlibReader->read($length);
$this->reader->seek($valuePos);
return $this->reader->read($length);
}

/**
Expand Down Expand Up @@ -285,10 +319,10 @@ public function isSaved(): bool
public function getLastUpdate(): int
{
if ($this->lastUpdate === null) {
$this->zlibReader->rewind();
$this->reader->rewind();
$data = $this->readAfter(hex2bin('04000A') . 'LastUpdate', 8);
$rawData = $data !== null ? unpack('J', $data) : false;
if($rawData === false) {
if ($rawData === false) {
return -1;
}
$this->lastUpdate = $rawData['1'];
Expand Down Expand Up @@ -338,14 +372,14 @@ public function getGlobalXPos(): int
*/
public function getGlobalYPos(): int
{
return $this->regionPosition[1] * 32 + $this->yPos;
return $this->regionPosition[1] * 32 + $this->yPos;
}

/**
* @return void
*/
public function close(): void
{
$this->zlibReader->reset();
$this->reader->reset();
}
}
175 changes: 175 additions & 0 deletions src/Reader/BufferedReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

namespace Aternos\Thanos\Reader;

use Exception;

/**
* @package Aternos\Thanos\Reader
*/
abstract class BufferedReader implements ReaderInterface
{
/**
* @var int
*/
protected int $offset;

/**
* @var int
*/
protected int $length;

/**
* @var int
*/
protected int $resourcePointer;

/**
* @var int
*/
protected int $pointer = 0;

/**
* @var string
*/
protected string $data = '';

/**
* @var resource
*/
protected $resource;

/**
* ZlibReader constructor.
*
* @param $resource
* @param int $offset
* @param int $length
*/
public function __construct(
$resource,
int $offset,
int $length
) {
$this->offset = $offset;
$this->resourcePointer = $offset;
$this->length = $length;
$this->resource = $resource;
}

/**
* Read $length bytes of data
*
* @param int $length
* @return string
* @throws Exception
*/
public function read(int $length): string
{
$readLength = max(
$length - (strlen($this->data) - $this->pointer),
0
);

if ($readLength > 0) {
$chunk = "";
while (strlen($chunk) < $readLength && $this->getRemainingRawLength() > 0) {
$chunk .= $this->getRawChunk($readLength - strlen($chunk));
}
$this->data .= $chunk;
}

$data = substr($this->data, $this->pointer, $length);
$this->pointer += strlen($data);

return $data;
}

/**
* @param int $length
* @return string
* @throws Exception
*/
protected function readRaw(int $length): string
{
if ($length <= 0) {
return '';
}
fseek($this->resource, $this->resourcePointer);
$rawData = fread(
$this->resource,
min(
$length,
$this->getRemainingRawLength()
)
);
if($rawData === false) {
throw new Exception("Failed to read compressed input data.");
}

$this->resourcePointer = ftell($this->resource) ?: $this->resourcePointer + strlen($rawData);
return $rawData;
}

/**
* @return int
*/
protected function getRemainingRawLength(): int
{
return $this->offset + $this->length - $this->resourcePointer;
}

/**
* Read and uncompress a chunk of data
* $length is just a suggestion, the actual length of the returned data may be longer or shorter
*
* @param int $length
* @return string
*/
protected abstract function getRawChunk(int $length): string;

/**
* Set pointer position to $offset
*
* @param int $offset
*/
public function seek(int $offset): void
{
$this->pointer = max($offset, 0);
}

/**
* Set pointer position to 0
*
*/
public function rewind(): void
{
$this->pointer = 0;
}

/**
* @inheritDoc
*/
public function reset(): void
{
$this->data = '';
$this->pointer = 0;
$this->resourcePointer = $this->offset;
}

public function eof(): bool
{
return ($this->resourcePointer >= $this->offset + $this->length || feof($this->resource))
&& $this->pointer >= strlen($this->data);
}

/**
* Get current pointer position
*
* @return int
*/
public function tell(): int
{
return $this->pointer;
}
}
Loading

0 comments on commit 7a0fc84

Please sign in to comment.