PocketMine-MP 5.15.1 git-fb9a74e8799c71ed8292cfa53abe7a4c9204629d
BlockStateUpgradeSchemaUtils.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\data\bedrock\block\upgrade;
25
37use Symfony\Component\Filesystem\Path;
38use function array_key_last;
39use function array_map;
40use function array_values;
41use function assert;
42use function count;
43use function get_debug_type;
44use function gettype;
45use function implode;
46use function is_object;
47use function is_string;
48use function json_decode;
49use function json_encode;
50use function ksort;
51use function sort;
52use function str_pad;
53use function strval;
54use function usort;
55use const JSON_THROW_ON_ERROR;
56use const SORT_NUMERIC;
57use const STR_PAD_LEFT;
58
60
61 public static function describe(BlockStateUpgradeSchema $schema) : string{
62 $lines = [];
63 $lines[] = "Renames:";
64 foreach($schema->renamedIds as $rename){
65 $lines[] = "- $rename";
66 }
67 $lines[] = "Added properties:";
68 foreach(Utils::stringifyKeys($schema->addedProperties) as $blockName => $tags){
69 foreach(Utils::stringifyKeys($tags) as $k => $v){
70 $lines[] = "- $blockName has $k added: $v";
71 }
72 }
73
74 $lines[] = "Removed properties:";
75 foreach(Utils::stringifyKeys($schema->removedProperties) as $blockName => $tagNames){
76 foreach($tagNames as $tagName){
77 $lines[] = "- $blockName has $tagName removed";
78 }
79 }
80 $lines[] = "Renamed properties:";
81 foreach(Utils::stringifyKeys($schema->renamedProperties) as $blockName => $tagNames){
82 foreach(Utils::stringifyKeys($tagNames) as $oldTagName => $newTagName){
83 $lines[] = "- $blockName has $oldTagName renamed to $newTagName";
84 }
85 }
86 $lines[] = "Remapped property values:";
87 foreach(Utils::stringifyKeys($schema->remappedPropertyValues) as $blockName => $remaps){
88 foreach(Utils::stringifyKeys($remaps) as $tagName => $oldNewList){
89 foreach($oldNewList as $oldNew){
90 $lines[] = "- $blockName has $tagName value changed from $oldNew->old to $oldNew->new";
91 }
92 }
93 }
94 return implode("\n", $lines);
95 }
96
97 public static function tagToJsonModel(Tag $tag) : BlockStateUpgradeSchemaModelTag{
99 if($tag instanceof IntTag){
100 $model->int = $tag->getValue();
101 }elseif($tag instanceof StringTag){
102 $model->string = $tag->getValue();
103 }elseif($tag instanceof ByteTag){
104 $model->byte = $tag->getValue();
105 }else{
106 throw new \UnexpectedValueException("Unexpected value type " . get_debug_type($tag));
107 }
108
109 return $model;
110 }
111
112 private static function jsonModelToTag(BlockStateUpgradeSchemaModelTag $model) : Tag{
113 return match(true){
114 isset($model->byte) && !isset($model->int) && !isset($model->string) => new ByteTag($model->byte),
115 !isset($model->byte) && isset($model->int) && !isset($model->string) => new IntTag($model->int),
116 !isset($model->byte) && !isset($model->int) && isset($model->string) => new StringTag($model->string),
117 default => throw new \UnexpectedValueException("Malformed JSON model tag, expected exactly one of 'byte', 'int' or 'string' properties")
118 };
119 }
120
121 public static function fromJsonModel(BlockStateUpgradeSchemaModel $model, int $schemaId) : BlockStateUpgradeSchema{
122 $result = new BlockStateUpgradeSchema(
123 $model->maxVersionMajor,
124 $model->maxVersionMinor,
125 $model->maxVersionPatch,
126 $model->maxVersionRevision,
127 $schemaId
128 );
129 $result->renamedIds = $model->renamedIds ?? [];
130 $result->renamedProperties = $model->renamedProperties ?? [];
131 $result->removedProperties = $model->removedProperties ?? [];
132
133 foreach(Utils::stringifyKeys($model->addedProperties ?? []) as $blockName => $properties){
134 foreach(Utils::stringifyKeys($properties) as $propertyName => $propertyValue){
135 $result->addedProperties[$blockName][$propertyName] = self::jsonModelToTag($propertyValue);
136 }
137 }
138
139 $convertedRemappedValuesIndex = [];
140 foreach(Utils::stringifyKeys($model->remappedPropertyValuesIndex ?? []) as $mappingKey => $mappingValues){
141 foreach($mappingValues as $k => $oldNew){
142 $convertedRemappedValuesIndex[$mappingKey][$k] = new BlockStateUpgradeSchemaValueRemap(
143 self::jsonModelToTag($oldNew->old),
144 self::jsonModelToTag($oldNew->new)
145 );
146 }
147 }
148
149 foreach(Utils::stringifyKeys($model->remappedPropertyValues ?? []) as $blockName => $properties){
150 foreach(Utils::stringifyKeys($properties) as $property => $mappedValuesKey){
151 if(!isset($convertedRemappedValuesIndex[$mappedValuesKey])){
152 throw new \UnexpectedValueException("Missing key from schema values index $mappedValuesKey");
153 }
154 $result->remappedPropertyValues[$blockName][$property] = $convertedRemappedValuesIndex[$mappedValuesKey];
155 }
156 }
157
158 foreach(Utils::stringifyKeys($model->remappedStates ?? []) as $oldBlockName => $remaps){
159 foreach($remaps as $remap){
160 if(isset($remap->newName) === isset($remap->newFlattenedName)){
161 throw new \UnexpectedValueException("Expected exactly one of 'newName' or 'newFlattenedName' properties to be set");
162 }
163
164 $result->remappedStates[$oldBlockName][] = new BlockStateUpgradeSchemaBlockRemap(
165 array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->oldState ?? []),
166 $remap->newName ?? new BlockStateUpgradeSchemaFlattenedName(
167 $remap->newFlattenedName->prefix,
168 $remap->newFlattenedName->flattenedProperty,
169 $remap->newFlattenedName->suffix,
170 $remap->newFlattenedName->flattenedValueRemaps ?? [],
171 ),
172 array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->newState ?? []),
173 $remap->copiedState ?? []
174 );
175 }
176 }
177
178 return $result;
179 }
180
181 private static function buildRemappedValuesIndex(BlockStateUpgradeSchema $schema, BlockStateUpgradeSchemaModel $model) : void{
182 if(count($schema->remappedPropertyValues) === 0){
183 return;
184 }
185 $dedupMapping = [];
186 $dedupTableMap = [];
187
188 $orderedRemappedValues = $schema->remappedPropertyValues;
189 ksort($orderedRemappedValues);
190 foreach(Utils::stringifyKeys($orderedRemappedValues) as $blockName => $remaps){
191 ksort($remaps);
192 foreach(Utils::stringifyKeys($remaps) as $propertyName => $remappedValues){
193 $remappedValuesMap = [];
194 foreach($remappedValues as $oldNew){
195 $remappedValuesMap[$oldNew->old->toString()] = $oldNew;
196 }
197 ksort($remappedValuesMap);
198
199 if(isset($dedupTableMap[$propertyName])){
200 foreach($dedupTableMap[$propertyName] as $k => $dedupValuesMap){
201 if(count($remappedValuesMap) !== count($dedupValuesMap)){
202 continue;
203 }
204
205 foreach(Utils::stringifyKeys($remappedValuesMap) as $oldHash => $remappedOldNew){
206 if(
207 !isset($dedupValuesMap[$oldHash]) ||
208 !$remappedOldNew->old->equals($dedupValuesMap[$oldHash]->old) ||
209 !$remappedOldNew->new->equals($dedupValuesMap[$oldHash]->new)
210 ){
211 continue 2;
212 }
213 }
214
215 //we found a match
216 $dedupMapping[$blockName][$propertyName] = $k;
217 continue 2;
218 }
219 }
220
221 //no match, add the values to the table
222 $dedupTableMap[$propertyName][] = $remappedValuesMap;
223 $dedupMapping[$blockName][$propertyName] = array_key_last($dedupTableMap[$propertyName]);
224 }
225 }
226
227 $modelTable = [];
228 foreach(Utils::stringifyKeys($dedupTableMap) as $propertyName => $mappingSet){
229 foreach($mappingSet as $setId => $valuePairs){
230 $newDedupName = $propertyName . "_" . str_pad(strval($setId), 2, "0", STR_PAD_LEFT);
231 foreach($valuePairs as $pair){
232 $modelTable[$newDedupName][] = new BlockStateUpgradeSchemaModelValueRemap(
233 BlockStateUpgradeSchemaUtils::tagToJsonModel($pair->old),
234 BlockStateUpgradeSchemaUtils::tagToJsonModel($pair->new),
235 );
236 }
237 }
238 }
239 $modelDedupMapping = [];
240 foreach(Utils::stringifyKeys($dedupMapping) as $blockName => $properties){
241 foreach(Utils::stringifyKeys($properties) as $propertyName => $dedupTableIndex){
242 $modelDedupMapping[$blockName][$propertyName] = $propertyName . "_" . str_pad(strval($dedupTableIndex), 2, "0", STR_PAD_LEFT);
243 }
244 }
245
246 ksort($modelTable);
247 ksort($modelDedupMapping);
248 foreach(Utils::stringifyKeys($dedupMapping) as $blockName => $properties){
249 ksort($properties);
250 $dedupMapping[$blockName] = $properties;
251 }
252
253 $model->remappedPropertyValuesIndex = $modelTable;
254 $model->remappedPropertyValues = $modelDedupMapping;
255 }
256
257 public static function toJsonModel(BlockStateUpgradeSchema $schema) : BlockStateUpgradeSchemaModel{
258 $result = new BlockStateUpgradeSchemaModel();
259 $result->maxVersionMajor = $schema->maxVersionMajor;
260 $result->maxVersionMinor = $schema->maxVersionMinor;
261 $result->maxVersionPatch = $schema->maxVersionPatch;
262 $result->maxVersionRevision = $schema->maxVersionRevision;
263
264 $result->renamedIds = $schema->renamedIds;
265 ksort($result->renamedIds);
266
267 $result->renamedProperties = $schema->renamedProperties;
268 ksort($result->renamedProperties);
269 foreach(Utils::stringifyKeys($result->renamedProperties) as $blockName => $properties){
270 ksort($properties);
271 $result->renamedProperties[$blockName] = $properties;
272 }
273
274 $result->removedProperties = $schema->removedProperties;
275 ksort($result->removedProperties);
276 foreach(Utils::stringifyKeys($result->removedProperties) as $blockName => $properties){
277 sort($properties); //yes, this is intended to sort(), not ksort()
278 $result->removedProperties[$blockName] = $properties;
279 }
280
281 foreach(Utils::stringifyKeys($schema->addedProperties) as $blockName => $properties){
282 $addedProperties = [];
283 foreach(Utils::stringifyKeys($properties) as $propertyName => $propertyValue){
284 $addedProperties[$propertyName] = self::tagToJsonModel($propertyValue);
285 }
286 ksort($addedProperties);
287 $result->addedProperties[$blockName] = $addedProperties;
288 }
289 if(isset($result->addedProperties)){
290 ksort($result->addedProperties);
291 }
292
293 self::buildRemappedValuesIndex($schema, $result);
294
295 foreach(Utils::stringifyKeys($schema->remappedStates) as $oldBlockName => $remaps){
296 $keyedRemaps = [];
297 foreach($remaps as $remap){
299 array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->oldState),
300 is_string($remap->newName) ?
301 $remap->newName :
303 $remap->newName->prefix,
304 $remap->newName->flattenedProperty,
305 $remap->newName->suffix,
306 $remap->newName->flattenedValueRemaps
307 ),
308 array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->newState),
309 $remap->copiedState
310 );
311 if(count($modelRemap->copiedState) === 0){
312 unset($modelRemap->copiedState); //avoid polluting the JSON
313 }
314 $key = json_encode($modelRemap);
315 assert(!isset($keyedRemaps[$key]));
316 if(isset($keyedRemaps[$key])){
317 continue;
318 }
319 $keyedRemaps[$key] = $modelRemap;
320 }
322 //remaps with more specific criteria must come first
323 $filterSizeCompare = count($b->oldState ?? []) <=> count($a->oldState ?? []);
324 if($filterSizeCompare !== 0){
325 return $filterSizeCompare;
326 }
327 //remaps with the same number of criteria should be sorted alphabetically, but this is not strictly necessary
328 return json_encode($a->oldState ?? []) <=> json_encode($b->oldState ?? []);
329 });
330 $result->remappedStates[$oldBlockName] = array_values($keyedRemaps);
331 }
332 if(isset($result->remappedStates)){
333 ksort($result->remappedStates);
334 }
335
336 return $result;
337 }
338
344 public static function loadSchemas(string $path, int $maxSchemaId) : array{
345 $iterator = new \RegexIterator(
346 new \FilesystemIterator(
347 $path,
348 \FilesystemIterator::KEY_AS_FILENAME | \FilesystemIterator::SKIP_DOTS
349 ),
350 '/^(\d{4}).*\.json$/',
351 \RegexIterator::GET_MATCH,
352 \RegexIterator::USE_KEY
353 );
354
355 $result = [];
356
358 foreach($iterator as $matches){
359 $filename = $matches[0];
360 $schemaId = (int) $matches[1];
361
362 if($schemaId > $maxSchemaId){
363 continue;
364 }
365
366 $fullPath = Path::join($path, $filename);
367
368 $raw = Filesystem::fileGetContents($fullPath);
369
370 try{
371 $schema = self::loadSchemaFromString($raw, $schemaId);
372 }catch(\RuntimeException $e){
373 throw new \RuntimeException("Loading schema file $fullPath: " . $e->getMessage(), 0, $e);
374 }
375
376 $result[$schemaId] = $schema;
377 }
378
379 ksort($result, SORT_NUMERIC);
380 return $result;
381 }
382
383 public static function loadSchemaFromString(string $raw, int $schemaId) : BlockStateUpgradeSchema{
384 try{
385 $json = json_decode($raw, false, flags: JSON_THROW_ON_ERROR);
386 }catch(\JsonException $e){
387 throw new \RuntimeException($e->getMessage(), 0, $e);
388 }
389 if(!is_object($json)){
390 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
391 }
392
393 $jsonMapper = new \JsonMapper();
394 $jsonMapper->bExceptionOnMissingData = true;
395 $jsonMapper->bExceptionOnUndefinedProperty = true;
396 $jsonMapper->bStrictObjectTypeChecking = true;
397 try{
398 $model = $jsonMapper->map($json, new BlockStateUpgradeSchemaModel());
399 }catch(\JsonMapper_Exception $e){
400 throw new \RuntimeException($e->getMessage(), 0, $e);
401 }
402
403 return self::fromJsonModel($model, $schemaId);
404 }
405}
static fileGetContents(string $fileName, bool $useIncludePath=false, $context=null, int $offset=0, ?int $length=null)
Definition: Filesystem.php:305
static stringifyKeys(array $array)
Definition: Utils.php:605