PocketMine-MP 5.23.3 git-976fc63567edab7a6fb6aeae739f43cf9fe57de4
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 assert;
41use function count;
42use function get_debug_type;
43use function gettype;
44use function implode;
45use function is_object;
46use function is_string;
47use function json_decode;
48use function json_encode;
49use function ksort;
50use function sort;
51use function str_pad;
52use function strval;
53use function usort;
54use const JSON_THROW_ON_ERROR;
55use const SORT_NUMERIC;
56use const STR_PAD_LEFT;
57
59
60 public static function describe(BlockStateUpgradeSchema $schema) : string{
61 $lines = [];
62 $lines[] = "Renames:";
63 foreach($schema->renamedIds as $rename){
64 $lines[] = "- $rename";
65 }
66 $lines[] = "Added properties:";
67 foreach(Utils::stringifyKeys($schema->addedProperties) as $blockName => $tags){
68 foreach(Utils::stringifyKeys($tags) as $k => $v){
69 $lines[] = "- $blockName has $k added: $v";
70 }
71 }
72
73 $lines[] = "Removed properties:";
74 foreach(Utils::stringifyKeys($schema->removedProperties) as $blockName => $tagNames){
75 foreach($tagNames as $tagName){
76 $lines[] = "- $blockName has $tagName removed";
77 }
78 }
79 $lines[] = "Renamed properties:";
80 foreach(Utils::stringifyKeys($schema->renamedProperties) as $blockName => $tagNames){
81 foreach(Utils::stringifyKeys($tagNames) as $oldTagName => $newTagName){
82 $lines[] = "- $blockName has $oldTagName renamed to $newTagName";
83 }
84 }
85 $lines[] = "Remapped property values:";
86 foreach(Utils::stringifyKeys($schema->remappedPropertyValues) as $blockName => $remaps){
87 foreach(Utils::stringifyKeys($remaps) as $tagName => $oldNewList){
88 foreach($oldNewList as $oldNew){
89 $lines[] = "- $blockName has $tagName value changed from $oldNew->old to $oldNew->new";
90 }
91 }
92 }
93 return implode("\n", $lines);
94 }
95
96 public static function tagToJsonModel(Tag $tag) : BlockStateUpgradeSchemaModelTag{
98 if($tag instanceof IntTag){
99 $model->int = $tag->getValue();
100 }elseif($tag instanceof StringTag){
101 $model->string = $tag->getValue();
102 }elseif($tag instanceof ByteTag){
103 $model->byte = $tag->getValue();
104 }else{
105 throw new \UnexpectedValueException("Unexpected value type " . get_debug_type($tag));
106 }
107
108 return $model;
109 }
110
111 private static function jsonModelToTag(BlockStateUpgradeSchemaModelTag $model) : Tag{
112 return match(true){
113 isset($model->byte) && !isset($model->int) && !isset($model->string) => new ByteTag($model->byte),
114 !isset($model->byte) && isset($model->int) && !isset($model->string) => new IntTag($model->int),
115 !isset($model->byte) && !isset($model->int) && isset($model->string) => new StringTag($model->string),
116 default => throw new \UnexpectedValueException("Malformed JSON model tag, expected exactly one of 'byte', 'int' or 'string' properties")
117 };
118 }
119
120 public static function fromJsonModel(BlockStateUpgradeSchemaModel $model, int $schemaId) : BlockStateUpgradeSchema{
121 $result = new BlockStateUpgradeSchema(
122 $model->maxVersionMajor,
123 $model->maxVersionMinor,
124 $model->maxVersionPatch,
125 $model->maxVersionRevision,
126 $schemaId
127 );
128 $result->renamedIds = $model->renamedIds ?? [];
129 $result->renamedProperties = $model->renamedProperties ?? [];
130 $result->removedProperties = $model->removedProperties ?? [];
131
132 foreach(Utils::stringifyKeys($model->addedProperties ?? []) as $blockName => $properties){
133 foreach(Utils::stringifyKeys($properties) as $propertyName => $propertyValue){
134 $result->addedProperties[$blockName][$propertyName] = self::jsonModelToTag($propertyValue);
135 }
136 }
137
138 $convertedRemappedValuesIndex = [];
139 foreach(Utils::stringifyKeys($model->remappedPropertyValuesIndex ?? []) as $mappingKey => $mappingValues){
140 foreach($mappingValues as $oldNew){
141 $convertedRemappedValuesIndex[$mappingKey][] = new BlockStateUpgradeSchemaValueRemap(
142 self::jsonModelToTag($oldNew->old),
143 self::jsonModelToTag($oldNew->new)
144 );
145 }
146 }
147
148 foreach(Utils::stringifyKeys($model->remappedPropertyValues ?? []) as $blockName => $properties){
149 foreach(Utils::stringifyKeys($properties) as $property => $mappedValuesKey){
150 if(!isset($convertedRemappedValuesIndex[$mappedValuesKey])){
151 throw new \UnexpectedValueException("Missing key from schema values index $mappedValuesKey");
152 }
153 $result->remappedPropertyValues[$blockName][$property] = $convertedRemappedValuesIndex[$mappedValuesKey];
154 }
155 }
156
157 foreach(Utils::stringifyKeys($model->flattenedProperties ?? []) as $blockName => $flattenRule){
158 $result->flattenedProperties[$blockName] = self::jsonModelToFlattenRule($flattenRule);
159 }
160
161 foreach(Utils::stringifyKeys($model->remappedStates ?? []) as $oldBlockName => $remaps){
162 foreach($remaps as $remap){
163 if(isset($remap->newName)){
164 $remapName = $remap->newName;
165 }elseif(isset($remap->newFlattenedName)){
166 $flattenRule = $remap->newFlattenedName;
167 $remapName = self::jsonModelToFlattenRule($flattenRule);
168 }else{
169 throw new \UnexpectedValueException("Expected exactly one of 'newName' or 'newFlattenedName' properties to be set");
170 }
171
172 $result->remappedStates[$oldBlockName][] = new BlockStateUpgradeSchemaBlockRemap(
173 array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->oldState ?? []),
174 $remapName,
175 array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->newState ?? []),
176 $remap->copiedState ?? []
177 );
178 }
179 }
180
181 return $result;
182 }
183
184 private static function buildRemappedValuesIndex(BlockStateUpgradeSchema $schema, BlockStateUpgradeSchemaModel $model) : void{
185 if(count($schema->remappedPropertyValues) === 0){
186 return;
187 }
188 $dedupMapping = [];
189 $dedupTableMap = [];
190
191 $orderedRemappedValues = $schema->remappedPropertyValues;
192 ksort($orderedRemappedValues);
193 foreach(Utils::stringifyKeys($orderedRemappedValues) as $blockName => $remaps){
194 ksort($remaps);
195 foreach(Utils::stringifyKeys($remaps) as $propertyName => $remappedValues){
196 $remappedValuesMap = [];
197 foreach($remappedValues as $oldNew){
198 $remappedValuesMap[$oldNew->old->toString()] = $oldNew;
199 }
200 ksort($remappedValuesMap);
201
202 if(isset($dedupTableMap[$propertyName])){
203 foreach($dedupTableMap[$propertyName] as $k => $dedupValuesMap){
204 if(count($remappedValuesMap) !== count($dedupValuesMap)){
205 continue;
206 }
207
208 foreach(Utils::stringifyKeys($remappedValuesMap) as $oldHash => $remappedOldNew){
209 if(
210 !isset($dedupValuesMap[$oldHash]) ||
211 !$remappedOldNew->old->equals($dedupValuesMap[$oldHash]->old) ||
212 !$remappedOldNew->new->equals($dedupValuesMap[$oldHash]->new)
213 ){
214 continue 2;
215 }
216 }
217
218 //we found a match
219 $dedupMapping[$blockName][$propertyName] = $k;
220 continue 2;
221 }
222 }
223
224 //no match, add the values to the table
225 $dedupTableMap[$propertyName][] = $remappedValuesMap;
226 $dedupMapping[$blockName][$propertyName] = array_key_last($dedupTableMap[$propertyName]);
227 }
228 }
229
230 $modelTable = [];
231 foreach(Utils::stringifyKeys($dedupTableMap) as $propertyName => $mappingSet){
232 foreach($mappingSet as $setId => $valuePairs){
233 $newDedupName = $propertyName . "_" . str_pad(strval($setId), 2, "0", STR_PAD_LEFT);
234 foreach($valuePairs as $pair){
235 $modelTable[$newDedupName][] = new BlockStateUpgradeSchemaModelValueRemap(
236 BlockStateUpgradeSchemaUtils::tagToJsonModel($pair->old),
237 BlockStateUpgradeSchemaUtils::tagToJsonModel($pair->new),
238 );
239 }
240 }
241 }
242 $modelDedupMapping = [];
243 foreach(Utils::stringifyKeys($dedupMapping) as $blockName => $properties){
244 foreach(Utils::stringifyKeys($properties) as $propertyName => $dedupTableIndex){
245 $modelDedupMapping[$blockName][$propertyName] = $propertyName . "_" . str_pad(strval($dedupTableIndex), 2, "0", STR_PAD_LEFT);
246 }
247 }
248
249 ksort($modelTable);
250 ksort($modelDedupMapping);
251 foreach(Utils::stringifyKeys($dedupMapping) as $blockName => $properties){
252 ksort($properties);
253 $dedupMapping[$blockName] = $properties;
254 }
255
256 $model->remappedPropertyValuesIndex = $modelTable;
257 $model->remappedPropertyValues = $modelDedupMapping;
258 }
259
260 private static function flattenRuleToJsonModel(BlockStateUpgradeSchemaFlattenInfo $flattenRule) : BlockStateUpgradeSchemaModelFlattenInfo{
262 $flattenRule->prefix,
263 $flattenRule->flattenedProperty,
264 $flattenRule->suffix,
265 $flattenRule->flattenedValueRemaps,
266 match($flattenRule->flattenedPropertyType){
267 StringTag::class => null, //omit for TAG_String, as this is the common case
268 ByteTag::class => "byte",
269 IntTag::class => "int",
270 default => throw new \LogicException("Unexpected tag type " . $flattenRule->flattenedPropertyType . " in flattened property type")
271 }
272 );
273 }
274
275 private static function jsonModelToFlattenRule(BlockStateUpgradeSchemaModelFlattenInfo $flattenRule) : BlockStateUpgradeSchemaFlattenInfo{
277 $flattenRule->prefix,
278 $flattenRule->flattenedProperty,
279 $flattenRule->suffix,
280 $flattenRule->flattenedValueRemaps ?? [],
281 match ($flattenRule->flattenedPropertyType) {
282 "string", null => StringTag::class,
283 "int" => IntTag::class,
284 "byte" => ByteTag::class,
285 default => throw new \UnexpectedValueException("Unexpected flattened property type $flattenRule->flattenedPropertyType, expected 'string', 'int' or 'byte'")
286 }
287 );
288 }
289
290 public static function toJsonModel(BlockStateUpgradeSchema $schema) : BlockStateUpgradeSchemaModel{
291 $result = new BlockStateUpgradeSchemaModel();
292 $result->maxVersionMajor = $schema->maxVersionMajor;
293 $result->maxVersionMinor = $schema->maxVersionMinor;
294 $result->maxVersionPatch = $schema->maxVersionPatch;
295 $result->maxVersionRevision = $schema->maxVersionRevision;
296
297 $result->renamedIds = $schema->renamedIds;
298 ksort($result->renamedIds);
299
300 $result->renamedProperties = $schema->renamedProperties;
301 ksort($result->renamedProperties);
302 foreach(Utils::stringifyKeys($result->renamedProperties) as $blockName => $properties){
303 ksort($properties);
304 $result->renamedProperties[$blockName] = $properties;
305 }
306
307 $result->removedProperties = $schema->removedProperties;
308 ksort($result->removedProperties);
309 foreach(Utils::stringifyKeys($result->removedProperties) as $blockName => $properties){
310 sort($properties); //yes, this is intended to sort(), not ksort()
311 $result->removedProperties[$blockName] = $properties;
312 }
313
314 foreach(Utils::stringifyKeys($schema->addedProperties) as $blockName => $properties){
315 $addedProperties = [];
316 foreach(Utils::stringifyKeys($properties) as $propertyName => $propertyValue){
317 $addedProperties[$propertyName] = self::tagToJsonModel($propertyValue);
318 }
319 ksort($addedProperties);
320 $result->addedProperties[$blockName] = $addedProperties;
321 }
322 if(isset($result->addedProperties)){
323 ksort($result->addedProperties);
324 }
325
326 self::buildRemappedValuesIndex($schema, $result);
327
328 foreach(Utils::stringifyKeys($schema->flattenedProperties) as $blockName => $flattenRule){
329 $result->flattenedProperties[$blockName] = self::flattenRuleToJsonModel($flattenRule);
330 }
331 if(isset($result->flattenedProperties)){
332 ksort($result->flattenedProperties);
333 }
334
335 foreach(Utils::stringifyKeys($schema->remappedStates) as $oldBlockName => $remaps){
336 $keyedRemaps = [];
337 foreach($remaps as $remap){
339 array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->oldState),
340 is_string($remap->newName) ? $remap->newName : self::flattenRuleToJsonModel($remap->newName),
341 array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->newState),
342 $remap->copiedState
343 );
344 if(count($modelRemap->copiedState) === 0){
345 unset($modelRemap->copiedState); //avoid polluting the JSON
346 }
347 $key = json_encode($modelRemap);
348 assert(!isset($keyedRemaps[$key]));
349 if(isset($keyedRemaps[$key])){
350 continue;
351 }
352 $keyedRemaps[$key] = $modelRemap;
353 }
355 //remaps with more specific criteria must come first
356 $filterSizeCompare = count($b->oldState ?? []) <=> count($a->oldState ?? []);
357 if($filterSizeCompare !== 0){
358 return $filterSizeCompare;
359 }
360 //remaps with the same number of criteria should be sorted alphabetically, but this is not strictly necessary
361 return json_encode($a->oldState ?? []) <=> json_encode($b->oldState ?? []);
362 });
363 $result->remappedStates[$oldBlockName] = $keyedRemaps; //usort strips keys, so this is already a list
364 }
365 if(isset($result->remappedStates)){
366 ksort($result->remappedStates);
367 }
368
369 return $result;
370 }
371
377 public static function loadSchemas(string $path, int $maxSchemaId) : array{
378 $iterator = new \RegexIterator(
379 new \FilesystemIterator(
380 $path,
381 \FilesystemIterator::KEY_AS_FILENAME | \FilesystemIterator::SKIP_DOTS
382 ),
383 '/^(\d{4}).*\.json$/',
384 \RegexIterator::GET_MATCH,
385 \RegexIterator::USE_KEY
386 );
387
388 $result = [];
389
391 foreach($iterator as $matches){
392 $filename = $matches[0];
393 $schemaId = (int) $matches[1];
394
395 if($schemaId > $maxSchemaId){
396 continue;
397 }
398
399 $fullPath = Path::join($path, $filename);
400
401 $raw = Filesystem::fileGetContents($fullPath);
402
403 try{
404 $schema = self::loadSchemaFromString($raw, $schemaId);
405 }catch(\RuntimeException $e){
406 throw new \RuntimeException("Loading schema file $fullPath: " . $e->getMessage(), 0, $e);
407 }
408
409 $result[$schemaId] = $schema;
410 }
411
412 ksort($result, SORT_NUMERIC);
413 return $result;
414 }
415
416 public static function loadSchemaFromString(string $raw, int $schemaId) : BlockStateUpgradeSchema{
417 try{
418 $json = json_decode($raw, false, flags: JSON_THROW_ON_ERROR);
419 }catch(\JsonException $e){
420 throw new \RuntimeException($e->getMessage(), 0, $e);
421 }
422 if(!is_object($json)){
423 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
424 }
425
426 $jsonMapper = new \JsonMapper();
427 $jsonMapper->bExceptionOnMissingData = true;
428 $jsonMapper->bExceptionOnUndefinedProperty = true;
429 $jsonMapper->bStrictObjectTypeChecking = true;
430 try{
431 $model = $jsonMapper->map($json, new BlockStateUpgradeSchemaModel());
432 }catch(\JsonMapper_Exception $e){
433 throw new \RuntimeException($e->getMessage(), 0, $e);
434 }
435
436 return self::fromJsonModel($model, $schemaId);
437 }
438}