PocketMine-MP 5.21.2 git-a6534ecbbbcf369264567d27e5ed70f7f5be9816
Loading...
Searching...
No Matches
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->flattenedProperties ?? []) as $blockName => $flattenRule){
159 $result->flattenedProperties[$blockName] = self::jsonModelToFlattenRule($flattenRule);
160 }
161
162 foreach(Utils::stringifyKeys($model->remappedStates ?? []) as $oldBlockName => $remaps){
163 foreach($remaps as $remap){
164 if(isset($remap->newName)){
165 $remapName = $remap->newName;
166 }elseif(isset($remap->newFlattenedName)){
167 $flattenRule = $remap->newFlattenedName;
168 $remapName = self::jsonModelToFlattenRule($flattenRule);
169 }else{
170 throw new \UnexpectedValueException("Expected exactly one of 'newName' or 'newFlattenedName' properties to be set");
171 }
172
173 $result->remappedStates[$oldBlockName][] = new BlockStateUpgradeSchemaBlockRemap(
174 array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->oldState ?? []),
175 $remapName,
176 array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->newState ?? []),
177 $remap->copiedState ?? []
178 );
179 }
180 }
181
182 return $result;
183 }
184
185 private static function buildRemappedValuesIndex(BlockStateUpgradeSchema $schema, BlockStateUpgradeSchemaModel $model) : void{
186 if(count($schema->remappedPropertyValues) === 0){
187 return;
188 }
189 $dedupMapping = [];
190 $dedupTableMap = [];
191
192 $orderedRemappedValues = $schema->remappedPropertyValues;
193 ksort($orderedRemappedValues);
194 foreach(Utils::stringifyKeys($orderedRemappedValues) as $blockName => $remaps){
195 ksort($remaps);
196 foreach(Utils::stringifyKeys($remaps) as $propertyName => $remappedValues){
197 $remappedValuesMap = [];
198 foreach($remappedValues as $oldNew){
199 $remappedValuesMap[$oldNew->old->toString()] = $oldNew;
200 }
201 ksort($remappedValuesMap);
202
203 if(isset($dedupTableMap[$propertyName])){
204 foreach($dedupTableMap[$propertyName] as $k => $dedupValuesMap){
205 if(count($remappedValuesMap) !== count($dedupValuesMap)){
206 continue;
207 }
208
209 foreach(Utils::stringifyKeys($remappedValuesMap) as $oldHash => $remappedOldNew){
210 if(
211 !isset($dedupValuesMap[$oldHash]) ||
212 !$remappedOldNew->old->equals($dedupValuesMap[$oldHash]->old) ||
213 !$remappedOldNew->new->equals($dedupValuesMap[$oldHash]->new)
214 ){
215 continue 2;
216 }
217 }
218
219 //we found a match
220 $dedupMapping[$blockName][$propertyName] = $k;
221 continue 2;
222 }
223 }
224
225 //no match, add the values to the table
226 $dedupTableMap[$propertyName][] = $remappedValuesMap;
227 $dedupMapping[$blockName][$propertyName] = array_key_last($dedupTableMap[$propertyName]);
228 }
229 }
230
231 $modelTable = [];
232 foreach(Utils::stringifyKeys($dedupTableMap) as $propertyName => $mappingSet){
233 foreach($mappingSet as $setId => $valuePairs){
234 $newDedupName = $propertyName . "_" . str_pad(strval($setId), 2, "0", STR_PAD_LEFT);
235 foreach($valuePairs as $pair){
236 $modelTable[$newDedupName][] = new BlockStateUpgradeSchemaModelValueRemap(
237 BlockStateUpgradeSchemaUtils::tagToJsonModel($pair->old),
238 BlockStateUpgradeSchemaUtils::tagToJsonModel($pair->new),
239 );
240 }
241 }
242 }
243 $modelDedupMapping = [];
244 foreach(Utils::stringifyKeys($dedupMapping) as $blockName => $properties){
245 foreach(Utils::stringifyKeys($properties) as $propertyName => $dedupTableIndex){
246 $modelDedupMapping[$blockName][$propertyName] = $propertyName . "_" . str_pad(strval($dedupTableIndex), 2, "0", STR_PAD_LEFT);
247 }
248 }
249
250 ksort($modelTable);
251 ksort($modelDedupMapping);
252 foreach(Utils::stringifyKeys($dedupMapping) as $blockName => $properties){
253 ksort($properties);
254 $dedupMapping[$blockName] = $properties;
255 }
256
257 $model->remappedPropertyValuesIndex = $modelTable;
258 $model->remappedPropertyValues = $modelDedupMapping;
259 }
260
261 private static function flattenRuleToJsonModel(BlockStateUpgradeSchemaFlattenInfo $flattenRule) : BlockStateUpgradeSchemaModelFlattenInfo{
263 $flattenRule->prefix,
264 $flattenRule->flattenedProperty,
265 $flattenRule->suffix,
266 $flattenRule->flattenedValueRemaps,
267 match($flattenRule->flattenedPropertyType){
268 StringTag::class => null, //omit for TAG_String, as this is the common case
269 ByteTag::class => "byte",
270 IntTag::class => "int",
271 default => throw new \LogicException("Unexpected tag type " . $flattenRule->flattenedPropertyType . " in flattened property type")
272 }
273 );
274 }
275
276 private static function jsonModelToFlattenRule(BlockStateUpgradeSchemaModelFlattenInfo $flattenRule) : BlockStateUpgradeSchemaFlattenInfo{
278 $flattenRule->prefix,
279 $flattenRule->flattenedProperty,
280 $flattenRule->suffix,
281 $flattenRule->flattenedValueRemaps ?? [],
282 match ($flattenRule->flattenedPropertyType) {
283 "string", null => StringTag::class,
284 "int" => IntTag::class,
285 "byte" => ByteTag::class,
286 default => throw new \UnexpectedValueException("Unexpected flattened property type $flattenRule->flattenedPropertyType, expected 'string', 'int' or 'byte'")
287 }
288 );
289 }
290
291 public static function toJsonModel(BlockStateUpgradeSchema $schema) : BlockStateUpgradeSchemaModel{
292 $result = new BlockStateUpgradeSchemaModel();
293 $result->maxVersionMajor = $schema->maxVersionMajor;
294 $result->maxVersionMinor = $schema->maxVersionMinor;
295 $result->maxVersionPatch = $schema->maxVersionPatch;
296 $result->maxVersionRevision = $schema->maxVersionRevision;
297
298 $result->renamedIds = $schema->renamedIds;
299 ksort($result->renamedIds);
300
301 $result->renamedProperties = $schema->renamedProperties;
302 ksort($result->renamedProperties);
303 foreach(Utils::stringifyKeys($result->renamedProperties) as $blockName => $properties){
304 ksort($properties);
305 $result->renamedProperties[$blockName] = $properties;
306 }
307
308 $result->removedProperties = $schema->removedProperties;
309 ksort($result->removedProperties);
310 foreach(Utils::stringifyKeys($result->removedProperties) as $blockName => $properties){
311 sort($properties); //yes, this is intended to sort(), not ksort()
312 $result->removedProperties[$blockName] = $properties;
313 }
314
315 foreach(Utils::stringifyKeys($schema->addedProperties) as $blockName => $properties){
316 $addedProperties = [];
317 foreach(Utils::stringifyKeys($properties) as $propertyName => $propertyValue){
318 $addedProperties[$propertyName] = self::tagToJsonModel($propertyValue);
319 }
320 ksort($addedProperties);
321 $result->addedProperties[$blockName] = $addedProperties;
322 }
323 if(isset($result->addedProperties)){
324 ksort($result->addedProperties);
325 }
326
327 self::buildRemappedValuesIndex($schema, $result);
328
329 foreach(Utils::stringifyKeys($schema->flattenedProperties) as $blockName => $flattenRule){
330 $result->flattenedProperties[$blockName] = self::flattenRuleToJsonModel($flattenRule);
331 }
332 if(isset($result->flattenedProperties)){
333 ksort($result->flattenedProperties);
334 }
335
336 foreach(Utils::stringifyKeys($schema->remappedStates) as $oldBlockName => $remaps){
337 $keyedRemaps = [];
338 foreach($remaps as $remap){
340 array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->oldState),
341 is_string($remap->newName) ? $remap->newName : self::flattenRuleToJsonModel($remap->newName),
342 array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->newState),
343 $remap->copiedState
344 );
345 if(count($modelRemap->copiedState) === 0){
346 unset($modelRemap->copiedState); //avoid polluting the JSON
347 }
348 $key = json_encode($modelRemap);
349 assert(!isset($keyedRemaps[$key]));
350 if(isset($keyedRemaps[$key])){
351 continue;
352 }
353 $keyedRemaps[$key] = $modelRemap;
354 }
356 //remaps with more specific criteria must come first
357 $filterSizeCompare = count($b->oldState ?? []) <=> count($a->oldState ?? []);
358 if($filterSizeCompare !== 0){
359 return $filterSizeCompare;
360 }
361 //remaps with the same number of criteria should be sorted alphabetically, but this is not strictly necessary
362 return json_encode($a->oldState ?? []) <=> json_encode($b->oldState ?? []);
363 });
364 $result->remappedStates[$oldBlockName] = array_values($keyedRemaps);
365 }
366 if(isset($result->remappedStates)){
367 ksort($result->remappedStates);
368 }
369
370 return $result;
371 }
372
378 public static function loadSchemas(string $path, int $maxSchemaId) : array{
379 $iterator = new \RegexIterator(
380 new \FilesystemIterator(
381 $path,
382 \FilesystemIterator::KEY_AS_FILENAME | \FilesystemIterator::SKIP_DOTS
383 ),
384 '/^(\d{4}).*\.json$/',
385 \RegexIterator::GET_MATCH,
386 \RegexIterator::USE_KEY
387 );
388
389 $result = [];
390
392 foreach($iterator as $matches){
393 $filename = $matches[0];
394 $schemaId = (int) $matches[1];
395
396 if($schemaId > $maxSchemaId){
397 continue;
398 }
399
400 $fullPath = Path::join($path, $filename);
401
402 $raw = Filesystem::fileGetContents($fullPath);
403
404 try{
405 $schema = self::loadSchemaFromString($raw, $schemaId);
406 }catch(\RuntimeException $e){
407 throw new \RuntimeException("Loading schema file $fullPath: " . $e->getMessage(), 0, $e);
408 }
409
410 $result[$schemaId] = $schema;
411 }
412
413 ksort($result, SORT_NUMERIC);
414 return $result;
415 }
416
417 public static function loadSchemaFromString(string $raw, int $schemaId) : BlockStateUpgradeSchema{
418 try{
419 $json = json_decode($raw, false, flags: JSON_THROW_ON_ERROR);
420 }catch(\JsonException $e){
421 throw new \RuntimeException($e->getMessage(), 0, $e);
422 }
423 if(!is_object($json)){
424 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
425 }
426
427 $jsonMapper = new \JsonMapper();
428 $jsonMapper->bExceptionOnMissingData = true;
429 $jsonMapper->bExceptionOnUndefinedProperty = true;
430 $jsonMapper->bStrictObjectTypeChecking = true;
431 try{
432 $model = $jsonMapper->map($json, new BlockStateUpgradeSchemaModel());
433 }catch(\JsonMapper_Exception $e){
434 throw new \RuntimeException($e->getMessage(), 0, $e);
435 }
436
437 return self::fromJsonModel($model, $schemaId);
438 }
439}