PocketMine-MP 5.31.1 git-c65f740ce5006911c3bac72aa5e6c3707846bd6e
Loading...
Searching...
No Matches
BlockStateUpgrader.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
33use function count;
34use function get_class;
35use function is_string;
36use function ksort;
37use function max;
38use function sprintf;
39use const SORT_NUMERIC;
40
46 private array $upgradeSchemas = [];
47
48 private int $outputVersion = 0;
49
54 public function __construct(array $upgradeSchemas){
55 foreach($upgradeSchemas as $schema){
56 $this->addSchema($schema);
57 }
58 }
59
60 public function addSchema(BlockStateUpgradeSchema $schema) : void{
61 $schemaId = $schema->getSchemaId();
62 $versionId = $schema->getVersionId();
63 if(isset($this->upgradeSchemas[$versionId][$schemaId])){
64 throw new \InvalidArgumentException("Cannot add two schemas with the same schema ID and version ID");
65 }
66
67 //schema ID tells us the order when multiple schemas use the same version ID
68 $this->upgradeSchemas[$versionId][$schemaId] = $schema;
69
70 ksort($this->upgradeSchemas, SORT_NUMERIC);
71 ksort($this->upgradeSchemas[$versionId], SORT_NUMERIC);
72
73 $this->outputVersion = max($this->outputVersion, $schema->getVersionId());
74 }
75
76 public function upgrade(BlockStateData $blockStateData) : BlockStateData{
77 $version = $blockStateData->getVersion();
78 $name = $blockStateData->getName();
79 $states = $blockStateData->getStates();
80 foreach($this->upgradeSchemas as $resultVersion => $schemaList){
81 /*
82 * Sometimes Mojang made changes without bumping the version ID.
83 * A notable example is 0131_1.18.20.27_beta_to_1.18.30.json, which renamed a bunch of blockIDs.
84 * When this happens, all the schemas must be applied even if the version is the same, because the input
85 * version doesn't tell us which of the schemas have already been applied.
86 * If there's only one schema for a version (the norm), we can safely assume it's already been applied if
87 * the version is the same, and skip over it.
88 * TODO: this causes issues when testing isolated schemas since there will only be one schema for a version.
89 * The second check should be disabled for that case.
90 */
91 if($version > $resultVersion || (count($schemaList) === 1 && $version === $resultVersion)){
92 continue;
93 }
94
95 foreach($schemaList as $schema){
96 [$name, $states] = $this->applySchema($schema, $name, $states);
97 }
98 }
99
100 return new BlockStateData($name, $states, $this->outputVersion);
101 }
102
110 private function applySchema(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{
111 $remapped = $this->applyStateRemapped($schema, $oldName, $states);
112 if($remapped !== null){
113 return $remapped;
114 }
115
116 if(isset($schema->renamedIds[$oldName]) && isset($schema->flattenedProperties[$oldName])){
117 //TODO: this probably ought to be validated when the schema is constructed
118 throw new AssumptionFailedError("Both renamedIds and flattenedProperties are set for the same block ID \"$oldName\" - don't know what to do");
119 }
120 if(isset($schema->renamedIds[$oldName])){
121 $newName = $schema->renamedIds[$oldName];
122 }elseif(isset($schema->flattenedProperties[$oldName])){
123 [$newName, $states] = $this->applyPropertyFlattened($schema->flattenedProperties[$oldName], $oldName, $states);
124 }else{
125 $newName = $oldName;
126 }
127
128 $states = $this->applyPropertyAdded($schema, $oldName, $states);
129 $states = $this->applyPropertyRemoved($schema, $oldName, $states);
130 $states = $this->applyPropertyRenamedOrValueChanged($schema, $oldName, $states);
131 $states = $this->applyPropertyValueChanged($schema, $oldName, $states);
132
133 return [$newName, $states];
134 }
135
143 private function applyStateRemapped(BlockStateUpgradeSchema $schema, string $oldName, array $oldState) : ?array{
144 if(isset($schema->remappedStates[$oldName])){
145 foreach($schema->remappedStates[$oldName] as $remap){
146 if(count($remap->oldState) > count($oldState)){
147 //match criteria has more requirements than we have state properties
148 continue; //try next state
149 }
150 foreach(Utils::stringifyKeys($remap->oldState) as $k => $v){
151 if(!isset($oldState[$k]) || !$oldState[$k]->equals($v)){
152 continue 2; //try next state
153 }
154 }
155
156 if(is_string($remap->newName)){
157 $newName = $remap->newName;
158 }else{
159 //discard flatten modifications to state - the remap newState and copiedState will take care of it
160 [$newName, ] = $this->applyPropertyFlattened($remap->newName, $oldName, $oldState);
161 }
162
163 $newState = $remap->newState;
164 foreach($remap->copiedState as $stateName){
165 if(isset($oldState[$stateName])){
166 $newState[$stateName] = $oldState[$stateName];
167 }
168 }
169
170 return [$newName, $newState];
171 }
172 }
173
174 return null;
175 }
176
184 private function applyPropertyAdded(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{
185 if(isset($schema->addedProperties[$oldName])){
186 foreach(Utils::stringifyKeys($schema->addedProperties[$oldName]) as $propertyName => $value){
187 if(!isset($states[$propertyName])){
188 $states[$propertyName] = $value;
189 }
190 }
191 }
192
193 return $states;
194 }
195
203 private function applyPropertyRemoved(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{
204 if(isset($schema->removedProperties[$oldName])){
205 foreach($schema->removedProperties[$oldName] as $propertyName){
206 unset($states[$propertyName]);
207 }
208 }
209
210 return $states;
211 }
212
213 private function locateNewPropertyValue(BlockStateUpgradeSchema $schema, string $oldName, string $oldPropertyName, Tag $oldValue) : Tag{
214 if(isset($schema->remappedPropertyValues[$oldName][$oldPropertyName])){
215 foreach($schema->remappedPropertyValues[$oldName][$oldPropertyName] as $mappedPair){
216 if($mappedPair->old->equals($oldValue)){
217 return $mappedPair->new;
218 }
219 }
220 }
221
222 return $oldValue;
223 }
224
232 private function applyPropertyRenamedOrValueChanged(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{
233 if(isset($schema->renamedProperties[$oldName])){
234 foreach(Utils::stringifyKeys($schema->renamedProperties[$oldName]) as $oldPropertyName => $newPropertyName){
235 $oldValue = $states[$oldPropertyName] ?? null;
236 if($oldValue !== null){
237 unset($states[$oldPropertyName]);
238
239 //If a value remap is needed, we need to do it here, since we won't be able to locate the property
240 //after it's been renamed - value remaps are always indexed by old property name for the sake of
241 //being able to do changes in any order.
242 $states[$newPropertyName] = $this->locateNewPropertyValue($schema, $oldName, $oldPropertyName, $oldValue);
243 }
244 }
245 }
246
247 return $states;
248 }
249
257 private function applyPropertyValueChanged(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{
258 if(isset($schema->remappedPropertyValues[$oldName])){
259 foreach(Utils::stringifyKeys($schema->remappedPropertyValues[$oldName]) as $oldPropertyName => $remappedValues){
260 $oldValue = $states[$oldPropertyName] ?? null;
261 if($oldValue !== null){
262 $newValue = $this->locateNewPropertyValue($schema, $oldName, $oldPropertyName, $oldValue);
263 $states[$oldPropertyName] = $newValue;
264 }
265 }
266 }
267
268 return $states;
269 }
270
278 private function applyPropertyFlattened(BlockStateUpgradeSchemaFlattenInfo $flattenInfo, string $oldName, array $states) : array{
279 $flattenedValue = $states[$flattenInfo->flattenedProperty] ?? null;
280 $expectedType = $flattenInfo->flattenedPropertyType;
281 if(!$flattenedValue instanceof $expectedType){
282 //flattened property is not of the expected type, so this transformation is not applicable
283 return [$oldName, $states];
284 }
285 $embedKey = match(get_class($flattenedValue)){
286 StringTag::class => $flattenedValue->getValue(),
287 ByteTag::class => (string) $flattenedValue->getValue(),
288 IntTag::class => (string) $flattenedValue->getValue(),
289 //flattenedPropertyType is always one of these three types, but PHPStan doesn't know that
290 default => throw new AssumptionFailedError("flattenedPropertyType should be one of these three types, but have " . get_class($flattenedValue)),
291 };
292 $embedValue = $flattenInfo->flattenedValueRemaps[$embedKey] ?? $embedKey;
293 $newName = sprintf("%s%s%s", $flattenInfo->prefix, $embedValue, $flattenInfo->suffix);
294 unset($states[$flattenInfo->flattenedProperty]);
295
296 return [$newName, $states];
297 }
298}