62        $lines[] = 
"Renames:";
 
   63        foreach($schema->renamedIds as $rename){
 
   64            $lines[] = 
"- $rename";
 
   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";
 
   73        $lines[] = 
"Removed properties:";
 
   74        foreach(Utils::stringifyKeys($schema->removedProperties) as $blockName => $tagNames){
 
   75            foreach($tagNames as $tagName){
 
   76                $lines[] = 
"- $blockName has $tagName removed";
 
   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";
 
   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";
 
   93        return implode(
"\n", $lines);
 
   98        if($tag instanceof 
IntTag){
 
  102        }elseif($tag instanceof 
ByteTag){
 
  105            throw new \UnexpectedValueException(
"Unexpected value type " . get_debug_type($tag));
 
  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")
 
  122            $model->maxVersionMajor,
 
  123            $model->maxVersionMinor,
 
  124            $model->maxVersionPatch,
 
  125            $model->maxVersionRevision,
 
  128        $result->renamedIds = $model->renamedIds ?? [];
 
  129        $result->renamedProperties = $model->renamedProperties ?? [];
 
  130        $result->removedProperties = $model->removedProperties ?? [];
 
  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);
 
  138        $convertedRemappedValuesIndex = [];
 
  139        foreach(Utils::stringifyKeys($model->remappedPropertyValuesIndex ?? []) as $mappingKey => $mappingValues){
 
  140            foreach($mappingValues as $oldNew){
 
  142                    self::jsonModelToTag($oldNew->old),
 
  143                    self::jsonModelToTag($oldNew->new)
 
  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");
 
  153                $result->remappedPropertyValues[$blockName][$property] = $convertedRemappedValuesIndex[$mappedValuesKey];
 
  157        foreach(Utils::stringifyKeys($model->flattenedProperties ?? []) as $blockName => $flattenRule){
 
  158            $result->flattenedProperties[$blockName] = self::jsonModelToFlattenRule($flattenRule);
 
  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);
 
  169                    throw new \UnexpectedValueException(
"Expected exactly one of 'newName' or 'newFlattenedName' properties to be set");
 
  176                    $remap->copiedState ?? []
 
  185        if(count($schema->remappedPropertyValues) === 0){
 
  191        $orderedRemappedValues = $schema->remappedPropertyValues;
 
  192        ksort($orderedRemappedValues);
 
  193        foreach(Utils::stringifyKeys($orderedRemappedValues) as $blockName => $remaps){
 
  195            foreach(Utils::stringifyKeys($remaps) as $propertyName => $remappedValues){
 
  196                $remappedValuesMap = [];
 
  197                foreach($remappedValues as $oldNew){
 
  198                    $remappedValuesMap[$oldNew->old->toString()] = $oldNew;
 
  200                ksort($remappedValuesMap);
 
  202                if(isset($dedupTableMap[$propertyName])){
 
  203                    foreach($dedupTableMap[$propertyName] as $k => $dedupValuesMap){
 
  204                        if(count($remappedValuesMap) !== count($dedupValuesMap)){
 
  208                        foreach(Utils::stringifyKeys($remappedValuesMap) as $oldHash => $remappedOldNew){
 
  210                                !isset($dedupValuesMap[$oldHash]) ||
 
  211                                !$remappedOldNew->old->equals($dedupValuesMap[$oldHash]->old) ||
 
  212                                !$remappedOldNew->new->equals($dedupValuesMap[$oldHash]->new)
 
  219                        $dedupMapping[$blockName][$propertyName] = $k;
 
  225                $dedupTableMap[$propertyName][] = $remappedValuesMap;
 
  226                $dedupMapping[$blockName][$propertyName] = array_key_last($dedupTableMap[$propertyName]);
 
  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){
 
  236                        BlockStateUpgradeSchemaUtils::tagToJsonModel($pair->old),
 
  237                        BlockStateUpgradeSchemaUtils::tagToJsonModel($pair->new),
 
  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);
 
  250        ksort($modelDedupMapping);
 
  251        foreach(Utils::stringifyKeys($dedupMapping) as $blockName => $properties){
 
  253            $dedupMapping[$blockName] = $properties;
 
  256        $model->remappedPropertyValuesIndex = $modelTable;
 
  257        $model->remappedPropertyValues = $modelDedupMapping;
 
  262            $flattenRule->prefix,
 
  263            $flattenRule->flattenedProperty,
 
  264            $flattenRule->suffix,
 
  265            $flattenRule->flattenedValueRemaps,
 
  266            match($flattenRule->flattenedPropertyType){
 
  267                StringTag::class => null, 
 
  268                ByteTag::class => 
"byte",
 
  269                IntTag::class => 
"int",
 
  270                default => throw new \LogicException(
"Unexpected tag type " . $flattenRule->flattenedPropertyType . 
" in flattened property type")
 
  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'")
 
  292        $result->maxVersionMajor = $schema->maxVersionMajor;
 
  293        $result->maxVersionMinor = $schema->maxVersionMinor;
 
  294        $result->maxVersionPatch = $schema->maxVersionPatch;
 
  295        $result->maxVersionRevision = $schema->maxVersionRevision;
 
  297        $result->renamedIds = $schema->renamedIds;
 
  298        ksort($result->renamedIds);
 
  300        $result->renamedProperties = $schema->renamedProperties;
 
  301        ksort($result->renamedProperties);
 
  302        foreach(Utils::stringifyKeys($result->renamedProperties) as $blockName => $properties){
 
  304            $result->renamedProperties[$blockName] = $properties;
 
  307        $result->removedProperties = $schema->removedProperties;
 
  308        ksort($result->removedProperties);
 
  309        foreach(Utils::stringifyKeys($result->removedProperties) as $blockName => $properties){
 
  311            $result->removedProperties[$blockName] = $properties;
 
  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);
 
  319            ksort($addedProperties);
 
  320            $result->addedProperties[$blockName] = $addedProperties;
 
  322        if(isset($result->addedProperties)){
 
  323            ksort($result->addedProperties);
 
  326        self::buildRemappedValuesIndex($schema, $result);
 
  328        foreach(Utils::stringifyKeys($schema->flattenedProperties) as $blockName => $flattenRule){
 
  329            $result->flattenedProperties[$blockName] = self::flattenRuleToJsonModel($flattenRule);
 
  331        if(isset($result->flattenedProperties)){
 
  332            ksort($result->flattenedProperties);
 
  335        foreach(Utils::stringifyKeys($schema->remappedStates) as $oldBlockName => $remaps){
 
  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),
 
  344                if(count($modelRemap->copiedState) === 0){
 
  345                    unset($modelRemap->copiedState); 
 
  347                $key = json_encode($modelRemap);
 
  348                assert(!isset($keyedRemaps[$key]));
 
  349                if(isset($keyedRemaps[$key])){
 
  352                $keyedRemaps[$key] = $modelRemap;
 
  356                $filterSizeCompare = count($b->oldState ?? []) <=> count($a->oldState ?? []);
 
  357                if($filterSizeCompare !== 0){
 
  358                    return $filterSizeCompare;
 
  361                return json_encode($a->oldState ?? []) <=> json_encode($b->oldState ?? []);
 
  363            $result->remappedStates[$oldBlockName] = $keyedRemaps; 
 
  365        if(isset($result->remappedStates)){
 
  366            ksort($result->remappedStates);
 
  377    public static function loadSchemas(
string $path, 
int $maxSchemaId) : array{
 
  378        $iterator = new \RegexIterator(
 
  379            new \FilesystemIterator(
 
  381                \FilesystemIterator::KEY_AS_FILENAME | \FilesystemIterator::SKIP_DOTS
 
  383            '/^(\d{4}).*\.json$/',
 
  384            \RegexIterator::GET_MATCH,
 
  385            \RegexIterator::USE_KEY
 
  391        foreach($iterator as $matches){
 
  392            $filename = $matches[0];
 
  393            $schemaId = (int) $matches[1];
 
  395            if($schemaId > $maxSchemaId){
 
  399            $fullPath = Path::join($path, $filename);
 
  401            $raw = Filesystem::fileGetContents($fullPath);
 
  404                $schema = self::loadSchemaFromString($raw, $schemaId);
 
  405            }
catch(\RuntimeException $e){
 
  406                throw new \RuntimeException(
"Loading schema file $fullPath: " . $e->getMessage(), 0, $e);
 
  409            $result[$schemaId] = $schema;
 
  412        ksort($result, SORT_NUMERIC);
 
 
  416    public static function loadSchemaFromString(
string $raw, 
int $schemaId) : BlockStateUpgradeSchema{
 
  418            $json = json_decode($raw, false, flags: JSON_THROW_ON_ERROR);
 
  419        }
catch(\JsonException $e){
 
  420            throw new \RuntimeException($e->getMessage(), 0, $e);
 
  422        if(!is_object($json)){
 
  423            throw new \RuntimeException(
"Unexpected root type of schema file " . gettype($json) . 
", expected object");
 
  426        $jsonMapper = new \JsonMapper();
 
  427        $jsonMapper->bExceptionOnMissingData = 
true;
 
  428        $jsonMapper->bExceptionOnUndefinedProperty = 
true;
 
  429        $jsonMapper->bStrictObjectTypeChecking = 
true;
 
  431            $model = $jsonMapper->map($json, 
new BlockStateUpgradeSchemaModel());
 
  432        }
catch(\JsonMapper_Exception $e){
 
  433            throw new \RuntimeException($e->getMessage(), 0, $e);
 
  436        return self::fromJsonModel($model, $schemaId);