Skip to content

Commit a981685

Browse files
committed
[sync] Update embedded LibSerializer from standalone
1 parent f88bc32 commit a981685

5 files changed

Lines changed: 640 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace imperazim\serializer;
6+
7+
use pocketmine\block\tile\Container;
8+
use pocketmine\math\Vector3;
9+
use pocketmine\world\World;
10+
use pocketmine\world\Position;
11+
12+
/**
13+
* Serializes container (chest/barrel/etc.) contents with position.
14+
*
15+
* Usage:
16+
* $json = ChestSerializable::jsonSerialize($world, $position);
17+
* ChestSerializable::jsonDeserialize($json, $world);
18+
*/
19+
final class ChestSerializable implements Serializable {
20+
21+
/**
22+
* Serializes a container's inventory at a position to JSON.
23+
*
24+
* @param object $object Unused (interface compliance). Use the static helpers instead.
25+
* @return string JSON string
26+
*/
27+
public static function jsonSerialize(object $object): string {
28+
throw new \BadMethodCallException('Use ChestSerializable::serialize() with World and Vector3 parameters.');
29+
}
30+
31+
/**
32+
* Deserializes a container from JSON string.
33+
*
34+
* @param string $jsonString JSON data
35+
* @return object|null Decoded array as object, or null on failure
36+
*/
37+
public static function jsonDeserialize(string $jsonString): ?object {
38+
$data = json_decode($jsonString, false);
39+
if (!is_object($data) || !isset($data->items)) {
40+
return null;
41+
}
42+
return $data;
43+
}
44+
45+
/**
46+
* Serializes a container's inventory at a position.
47+
*
48+
* @param World $world World containing the container
49+
* @param Vector3 $position Block position
50+
* @return string|null JSON string or null if not a container
51+
*/
52+
public static function serialize(World $world, Vector3 $position): ?string {
53+
$tile = $world->getTile($position);
54+
if (!$tile instanceof Container) return null;
55+
56+
$inventory = $tile->getInventory();
57+
$items = [];
58+
foreach ($inventory->getContents() as $slot => $item) {
59+
$items[$slot] = json_decode(ItemSerializable::jsonSerialize($item) ?? '{}', true);
60+
}
61+
62+
$pos = new Position($position->x, $position->y, $position->z, $world);
63+
$data = [
64+
'position' => json_decode(PositionSerializable::jsonSerialize($pos), true),
65+
'block' => json_decode(BlockSerializable::jsonSerialize($world->getBlock($position)), true),
66+
'items' => $items,
67+
'size' => $inventory->getSize(),
68+
];
69+
70+
return json_encode($data, JSON_THROW_ON_ERROR);
71+
}
72+
73+
/**
74+
* Restores a container's inventory from JSON string.
75+
*
76+
* @param string $jsonString JSON data
77+
* @param World $world Target world
78+
* @return bool True if restored successfully
79+
*/
80+
public static function deserialize(string $jsonString, World $world): bool {
81+
$data = json_decode($jsonString, true);
82+
if (!is_array($data) || !isset($data['position'], $data['items'])) return false;
83+
84+
$pos = PositionSerializable::jsonDeserialize(json_encode($data['position']));
85+
if ($pos === null) return false;
86+
87+
$tile = $world->getTile($pos);
88+
if (!$tile instanceof Container) return false;
89+
90+
$inventory = $tile->getInventory();
91+
$inventory->clearAll();
92+
93+
foreach ($data['items'] as $slot => $itemData) {
94+
$item = ItemSerializable::jsonDeserialize($itemData);
95+
if ($item !== null) {
96+
$inventory->setItem((int) $slot, $item);
97+
}
98+
}
99+
100+
return true;
101+
}
102+
103+
/**
104+
* Serializes all containers in a region.
105+
*
106+
* @param World $world Target world
107+
* @param Vector3 $min Minimum corner
108+
* @param Vector3 $max Maximum corner
109+
* @return string JSON array of serialized containers
110+
*/
111+
public static function serializeRegion(World $world, Vector3 $min, Vector3 $max): string {
112+
$containers = [];
113+
for ($x = (int) $min->x; $x <= (int) $max->x; $x++) {
114+
for ($y = (int) $min->y; $y <= (int) $max->y; $y++) {
115+
for ($z = (int) $min->z; $z <= (int) $max->z; $z++) {
116+
$json = self::serialize($world, new Vector3($x, $y, $z));
117+
if ($json !== null) {
118+
$containers[] = json_decode($json, true);
119+
}
120+
}
121+
}
122+
}
123+
return json_encode($containers, JSON_THROW_ON_ERROR);
124+
}
125+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace imperazim\serializer;
6+
7+
/**
8+
* Compact binary format for serializing large data (inventories, skins).
9+
* Smaller and faster than JSON for high-volume data.
10+
*
11+
* Usage:
12+
* $binary = CompactFormat::encode($data);
13+
* $data = CompactFormat::decode($binary);
14+
* // Compare sizes
15+
* echo strlen(json_encode($data)) . " vs " . strlen($binary);
16+
*/
17+
final class CompactFormat {
18+
19+
/**
20+
* Encodes data to compact binary format (gzip-compressed msgpack-like).
21+
*
22+
* @param mixed $data Data to encode (must be JSON-serializable)
23+
* @return string Binary string
24+
*/
25+
public static function encode(mixed $data): string {
26+
$json = json_encode($data, JSON_THROW_ON_ERROR);
27+
$compressed = gzcompress($json, 6);
28+
if ($compressed === false) {
29+
throw new \RuntimeException("Failed to compress data");
30+
}
31+
// Prefix with version byte + uncompressed length for validation
32+
$header = pack('CN', 1, strlen($json)); // version=1, 4-byte length
33+
return $header . $compressed;
34+
}
35+
36+
/**
37+
* Decodes compact binary format back to data.
38+
*
39+
* @param string $binary Binary string from encode()
40+
* @return mixed Decoded data
41+
* @throws \RuntimeException On decode failure
42+
*/
43+
public static function decode(string $binary): mixed {
44+
if (strlen($binary) < 5) {
45+
throw new \RuntimeException("Invalid compact format: too short");
46+
}
47+
48+
$header = unpack('Cversion/NoriginalLength', substr($binary, 0, 5));
49+
if ($header['version'] !== 1) {
50+
throw new \RuntimeException("Unknown compact format version: {$header['version']}");
51+
}
52+
53+
$decompressed = gzuncompress(substr($binary, 5));
54+
if ($decompressed === false) {
55+
throw new \RuntimeException("Failed to decompress data");
56+
}
57+
58+
if (strlen($decompressed) !== $header['originalLength']) {
59+
throw new \RuntimeException("Data length mismatch after decompression");
60+
}
61+
62+
return json_decode($decompressed, true, 512, JSON_THROW_ON_ERROR);
63+
}
64+
65+
/**
66+
* Encodes and returns as base64 (safe for text storage).
67+
*
68+
* @param mixed $data Data to encode
69+
* @return string Base64 encoded string
70+
*/
71+
public static function encodeBase64(mixed $data): string {
72+
return base64_encode(self::encode($data));
73+
}
74+
75+
/**
76+
* Decodes from base64 compact format.
77+
*
78+
* @param string $base64 Base64 string from encodeBase64()
79+
* @return mixed Decoded data
80+
*/
81+
public static function decodeBase64(string $base64): mixed {
82+
$binary = base64_decode($base64, true);
83+
if ($binary === false) {
84+
throw new \RuntimeException("Invalid base64 string");
85+
}
86+
return self::decode($binary);
87+
}
88+
89+
/**
90+
* Compares sizes between JSON and compact format.
91+
*
92+
* @param mixed $data Data to compare
93+
* @return array{json: int, compact: int, ratio: float}
94+
*/
95+
public static function compareSizes(mixed $data): array {
96+
$json = json_encode($data);
97+
$compact = self::encode($data);
98+
return [
99+
'json' => strlen($json),
100+
'compact' => strlen($compact),
101+
'ratio' => round(strlen($compact) / max(1, strlen($json)), 3),
102+
];
103+
}
104+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace imperazim\serializer;
6+
7+
/**
8+
* Handles migration of serialized data between schema versions.
9+
*
10+
* Usage:
11+
* DataVersioning::register("items", 1, 2, function(array $data) {
12+
* $data['durability'] = $data['damage'] ?? 0; // renamed field
13+
* unset($data['damage']);
14+
* return $data;
15+
* });
16+
* $migrated = DataVersioning::migrate("items", $data, fromVersion: 1, toVersion: 2);
17+
*/
18+
final class DataVersioning {
19+
20+
/** @var array<string, array<string, \Closure>> "schema:fromV->toV" => transformer */
21+
private static array $transformers = [];
22+
23+
/** @var array<string, int> schema => latest version */
24+
private static array $latestVersions = [];
25+
26+
/**
27+
* Registers a version transformer.
28+
*
29+
* @param string $schema Schema name (e.g. "items", "player_data")
30+
* @param int $fromVersion Source version
31+
* @param int $toVersion Target version (must be fromVersion + 1)
32+
* @param \Closure $transformer fn(array $data): array — transforms data
33+
*/
34+
public static function register(string $schema, int $fromVersion, int $toVersion, \Closure $transformer): void {
35+
$key = "{$schema}:{$fromVersion}->{$toVersion}";
36+
self::$transformers[$key] = $transformer;
37+
38+
$current = self::$latestVersions[$schema] ?? 0;
39+
if ($toVersion > $current) {
40+
self::$latestVersions[$schema] = $toVersion;
41+
}
42+
}
43+
44+
/**
45+
* Migrates data from one version to another, applying all intermediate transformers.
46+
*
47+
* @param string $schema Schema name
48+
* @param array $data Data to migrate
49+
* @param int $fromVersion Current version of the data
50+
* @param int|null $toVersion Target version (null = latest)
51+
* @return array Migrated data
52+
* @throws \RuntimeException If a required transformer is missing
53+
*/
54+
public static function migrate(string $schema, array $data, int $fromVersion, ?int $toVersion = null): array {
55+
$toVersion ??= self::getLatestVersion($schema);
56+
57+
if ($fromVersion >= $toVersion) {
58+
return $data; // Already up to date
59+
}
60+
61+
for ($v = $fromVersion; $v < $toVersion; $v++) {
62+
$key = "{$schema}:{$v}->" . ($v + 1);
63+
if (!isset(self::$transformers[$key])) {
64+
throw new \RuntimeException("Missing transformer for {$key}");
65+
}
66+
$data = (self::$transformers[$key])($data);
67+
}
68+
69+
return $data;
70+
}
71+
72+
/**
73+
* Wraps data with version metadata for storage.
74+
*
75+
* @param string $schema Schema name
76+
* @param array $data Data to wrap
77+
* @param int|null $version Version number (null = latest)
78+
* @return array Versioned data: ['_version' => int, '_schema' => string, 'data' => array]
79+
*/
80+
public static function wrap(string $schema, array $data, ?int $version = null): array {
81+
return [
82+
'_schema' => $schema,
83+
'_version' => $version ?? self::getLatestVersion($schema),
84+
'data' => $data,
85+
];
86+
}
87+
88+
/**
89+
* Unwraps and migrates versioned data to the latest version.
90+
*
91+
* @param array $wrapped Wrapped data from wrap()
92+
* @return array Migrated data (without metadata)
93+
*/
94+
public static function unwrap(array $wrapped): array {
95+
$schema = $wrapped['_schema'] ?? '';
96+
$version = $wrapped['_version'] ?? 1;
97+
$data = $wrapped['data'] ?? $wrapped;
98+
99+
return self::migrate($schema, $data, $version);
100+
}
101+
102+
/**
103+
* Gets the latest registered version for a schema.
104+
*
105+
* @param string $schema Schema name
106+
* @return int Latest version (1 if none registered)
107+
*/
108+
public static function getLatestVersion(string $schema): int {
109+
return self::$latestVersions[$schema] ?? 1;
110+
}
111+
112+
/**
113+
* Checks if data needs migration.
114+
*
115+
* @param array $wrapped Wrapped versioned data
116+
* @return bool True if data version < latest version
117+
*/
118+
public static function needsMigration(array $wrapped): bool {
119+
$schema = $wrapped['_schema'] ?? '';
120+
$version = $wrapped['_version'] ?? 1;
121+
return $version < self::getLatestVersion($schema);
122+
}
123+
}

0 commit comments

Comments
 (0)