diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenedName.php b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenedName.php index d9cbc780e..1c95dd9c7 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenedName.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenedName.php @@ -23,17 +23,28 @@ declare(strict_types=1); namespace pocketmine\data\bedrock\block\upgrade; +use function ksort; +use const SORT_STRING; + final class BlockStateUpgradeSchemaFlattenedName{ + /** + * @param string[] $flattenedValueRemaps + * @phpstan-param array $flattenedValueRemaps + */ public function __construct( public string $prefix, public string $flattenedProperty, - public string $suffix - ){} + public string $suffix, + public array $flattenedValueRemaps + ){ + ksort($this->flattenedValueRemaps, SORT_STRING); + } public function equals(self $that) : bool{ return $this->prefix === $that->prefix && $this->flattenedProperty === $that->flattenedProperty && - $this->suffix === $that->suffix; + $this->suffix === $that->suffix && + $this->flattenedValueRemaps === $that->flattenedValueRemaps; } } diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php index 9c63d51f0..832631490 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php @@ -166,7 +166,8 @@ final class BlockStateUpgradeSchemaUtils{ $remap->newName ?? new BlockStateUpgradeSchemaFlattenedName( $remap->newFlattenedName->prefix, $remap->newFlattenedName->flattenedProperty, - $remap->newFlattenedName->suffix + $remap->newFlattenedName->suffix, + $remap->newFlattenedName->flattenedValueRemaps ?? [], ), array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->newState ?? []), $remap->copiedState ?? [] @@ -301,7 +302,8 @@ final class BlockStateUpgradeSchemaUtils{ new BlockStateUpgradeSchemaModelFlattenedName( $remap->newName->prefix, $remap->newName->flattenedProperty, - $remap->newName->suffix + $remap->newName->suffix, + $remap->newName->flattenedValueRemaps ), array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->newState), $remap->copiedState diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgrader.php b/src/data/bedrock/block/upgrade/BlockStateUpgrader.php index f4a5b6e93..4a305d8bc 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgrader.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgrader.php @@ -142,7 +142,8 @@ final class BlockStateUpgrader{ }else{ $flattenedValue = $oldState[$remap->newName->flattenedProperty] ?? null; if($flattenedValue instanceof StringTag){ - $newName = sprintf("%s%s%s", $remap->newName->prefix, $flattenedValue->getValue(), $remap->newName->suffix); + $embedValue = $remap->newName->flattenedValueRemaps[$flattenedValue->getValue()] ?? $flattenedValue->getValue(); + $newName = sprintf("%s%s%s", $remap->newName->prefix, $embedValue, $remap->newName->suffix); unset($oldState[$remap->newName->flattenedProperty]); }else{ //flattened property is not a TAG_String, so this transformation is not applicable diff --git a/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenedName.php b/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenedName.php index 4508d9a3b..001192f47 100644 --- a/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenedName.php +++ b/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenedName.php @@ -23,7 +23,9 @@ declare(strict_types=1); namespace pocketmine\data\bedrock\block\upgrade\model; -final class BlockStateUpgradeSchemaModelFlattenedName{ +use function count; + +final class BlockStateUpgradeSchemaModelFlattenedName implements \JsonSerializable{ /** @required */ public string $prefix; @@ -31,10 +33,31 @@ final class BlockStateUpgradeSchemaModelFlattenedName{ public string $flattenedProperty; /** @required */ public string $suffix; + /** + * @var string[] + * @phpstan-var array + */ + public array $flattenedValueRemaps; - public function __construct(string $prefix, string $flattenedProperty, string $suffix){ + /** + * @param string[] $flattenedValueRemaps + * @phpstan-param array $flattenedValueRemaps + */ + public function __construct(string $prefix, string $flattenedProperty, string $suffix, array $flattenedValueRemaps){ $this->prefix = $prefix; $this->flattenedProperty = $flattenedProperty; $this->suffix = $suffix; + $this->flattenedValueRemaps = $flattenedValueRemaps; + } + + /** + * @return mixed[] + */ + public function jsonSerialize() : array{ + $result = (array) $this; + if(count($this->flattenedValueRemaps) === 0){ + unset($result["flattenedValueRemaps"]); + } + return $result; } } diff --git a/tools/generate-blockstate-upgrade-schema.php b/tools/generate-blockstate-upgrade-schema.php index dfb8f6066..54984d459 100644 --- a/tools/generate-blockstate-upgrade-schema.php +++ b/tools/generate-blockstate-upgrade-schema.php @@ -38,18 +38,23 @@ use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Filesystem; use pocketmine\utils\Utils; use function array_key_first; +use function array_key_last; use function array_keys; use function array_map; use function array_shift; use function array_values; use function count; use function dirname; -use function explode; use function file_put_contents; use function fwrite; use function implode; use function json_encode; use function ksort; +use function min; +use function sort; +use function strlen; +use function strrev; +use function substr; use function usort; use const JSON_PRETTY_PRINT; use const SORT_STRING; @@ -275,6 +280,77 @@ function processStateGroup(string $oldName, array $upgradeTable, BlockStateUpgra return true; } +/** + * @param string[] $strings + */ +function findCommonPrefix(array $strings) : string{ + sort($strings, SORT_STRING); + + $first = $strings[array_key_first($strings)]; + $last = $strings[array_key_last($strings)]; + + $maxLength = min(strlen($first), strlen($last)); + for($i = 0; $i < $maxLength; ++$i){ + if($first[$i] !== $last[$i]){ + return substr($first, 0, $i); + } + } + + return substr($first, 0, $maxLength); +} + +/** + * @param string[] $strings + */ +function findCommonSuffix(array $strings) : string{ + $reversed = array_map(strrev(...), $strings); + + return strrev(findCommonPrefix($reversed)); +} + +/** + * @param string[][][] $candidateFlattenedValues + * @phpstan-param array>> $candidateFlattenedValues + * + * @return BlockStateUpgradeSchemaFlattenedName[][] + * @phpstan-return array> + */ +function buildFlattenPropertyRules(array $candidateFlattenedValues) : array{ + $flattenPropertyRules = []; + foreach(Utils::stringifyKeys($candidateFlattenedValues) as $propertyName => $filters){ + foreach(Utils::stringifyKeys($filters) as $filter => $valueToId){ + $ids = array_values($valueToId); + + //TODO: this is a bit too enthusiastic. For example, when flattening the old "stone", it will see that + //"granite", "andesite", "stone" etc all have "e" as a common suffix, which works, but looks a bit daft. + //This also causes more remaps to be generated than necessary, since some of the values are already + //contained in the new ID. + $idPrefix = findCommonPrefix($ids); + $idSuffix = findCommonSuffix($ids); + if(strlen($idSuffix) < 2){ + $idSuffix = ""; + } + + $valueMap = []; + foreach(Utils::stringifyKeys($valueToId) as $value => $newId){ + $newValue = substr($newId, strlen($idPrefix), $idSuffix !== "" ? -strlen($idSuffix) : null); + if($newValue !== $value){ + $valueMap[$value] = $newValue; + } + } + + $flattenPropertyRules[$propertyName][$filter] = new BlockStateUpgradeSchemaFlattenedName( + $idPrefix, + $propertyName, + $idSuffix, + $valueMap + ); + } + } + ksort($flattenPropertyRules, SORT_STRING); + return $flattenPropertyRules; +} + /** * Attempts to compress a list of remapped states by looking at which state properties were consistently unchanged. * This significantly reduces the output size during flattening when the flattened block has many permutations @@ -327,9 +403,9 @@ function processRemappedStates(array $upgradeTable) : array{ $unchangedStatesByNewName[$newName] = $unchangedStates; } - $flattenedProperties = []; $notFlattenedProperties = []; - $notFlattenedPropertyValues = []; + + $candidateFlattenedValues = []; foreach($upgradeTable as $pair){ foreach(Utils::stringifyKeys($pair->old->getStates()) as $propertyName => $propertyValue){ if(isset($notFlattenedProperties[$propertyName])){ @@ -344,37 +420,41 @@ function processRemappedStates(array $upgradeTable) : array{ $notFlattenedProperties[$propertyName] = true; continue; } - $parts = explode($rawValue, $pair->new->getName(), 2); - if(count($parts) !== 2){ - //the new name does not contain the property value, but it may still be able to be flattened in other cases - $notFlattenedPropertyValues[$propertyName][$rawValue] = $rawValue; - continue; - } - [$prefix, $suffix] = $parts; $filter = $pair->old->getStates(); foreach($unchangedStatesByNewName[$pair->new->getName()] as $unchangedPropertyName){ unset($filter[$unchangedPropertyName]); } unset($filter[$propertyName]); + $rawFilter = encodeOrderedProperties($filter); - $flattenRule = new BlockStateUpgradeSchemaFlattenedName( - prefix: $prefix, - flattenedProperty: $propertyName, - suffix: $suffix - ); - if(!isset($flattenedProperties[$propertyName][$rawFilter])){ - $flattenedProperties[$propertyName][$rawFilter] = $flattenRule; - }elseif(!$flattenRule->equals($flattenedProperties[$propertyName][$rawFilter])){ - $notFlattenedProperties[$propertyName] = true; + if(isset($candidateFlattenedValues[$propertyName][$rawFilter])){ + $valuesToIds = $candidateFlattenedValues[$propertyName][$rawFilter]; + $existingNewId = $valuesToIds[$rawValue] ?? null; + if($existingNewId !== null && $existingNewId !== $pair->new->getName()){ + //this old value is associated with multiple new IDs - bad candidate for flattening + $notFlattenedProperties[$propertyName] = true; + continue; + } + foreach(Utils::stringifyKeys($valuesToIds) as $otherRawValue => $otherNewId){ + if($otherRawValue === $rawValue){ + continue; + } + if($otherNewId === $pair->new->getName()){ + //this old value maps to the same new ID as another old value - bad candidate for flattening + $notFlattenedProperties[$propertyName] = true; + continue 2; + } + } } + $candidateFlattenedValues[$propertyName][$rawFilter][$rawValue] = $pair->new->getName(); } } foreach(Utils::stringifyKeys($notFlattenedProperties) as $propertyName => $_){ - unset($flattenedProperties[$propertyName]); + unset($candidateFlattenedValues[$propertyName]); } - ksort($flattenedProperties, SORT_STRING); + $flattenedProperties = buildFlattenPropertyRules($candidateFlattenedValues); $flattenProperty = array_key_first($flattenedProperties); $list = []; @@ -393,19 +473,15 @@ function processRemappedStates(array $upgradeTable) : array{ } ksort($cleanedOldState); ksort($cleanedNewState); - $flattened = false; if($flattenProperty !== null){ $flattenedValue = $cleanedOldState[$flattenProperty] ?? null; if(!$flattenedValue instanceof StringTag){ - throw new AssumptionFailedError("This should always be a TAG_String"); - } - if(!isset($notFlattenedPropertyValues[$flattenProperty][$flattenedValue->getValue()])){ - unset($cleanedOldState[$flattenProperty]); - $flattened = true; + throw new AssumptionFailedError("This should always be a TAG_String ($newName $flattenProperty)"); } + unset($cleanedOldState[$flattenProperty]); } $rawOldState = encodeOrderedProperties($cleanedOldState); - $newNameRule = $flattenProperty !== null && $flattened ? + $newNameRule = $flattenProperty !== null ? $flattenedProperties[$flattenProperty][$rawOldState] ?? throw new AssumptionFailedError("This should always be set") : $newName;