PocketMine-MP 5.21.2 git-a6534ecbbbcf369264567d27e5ed70f7f5be9816
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 foreach($this->upgradeSchemas as $resultVersion => $schemaList){
79 /*
80 * Sometimes Mojang made changes without bumping the version ID.
81 * A notable example is 0131_1.18.20.27_beta_to_1.18.30.json, which renamed a bunch of blockIDs.
82 * When this happens, all the schemas must be applied even if the version is the same, because the input
83 * version doesn't tell us which of the schemas have already been applied.
84 * If there's only one schema for a version (the norm), we can safely assume it's already been applied if
85 * the version is the same, and skip over it.
86 * TODO: this causes issues when testing isolated schemas since there will only be one schema for a version.
87 * The second check should be disabled for that case.
88 */
89 if($version > $resultVersion || (count($schemaList) === 1 && $version === $resultVersion)){
90 continue;
91 }
92
93 foreach($schemaList as $schema){
94 $blockStateData = $this->applySchema($schema, $blockStateData);
95 }
96 }
97
98 if($this->outputVersion > $version){
99 //always update the version number of the blockstate, even if it didn't change - this is needed for
100 //external tools
101 $blockStateData = new BlockStateData($blockStateData->getName(), $blockStateData->getStates(), $this->outputVersion);
102 }
103 return $blockStateData;
104 }
105
106 private function applySchema(BlockStateUpgradeSchema $schema, BlockStateData $blockStateData) : BlockStateData{
107 $newStateData = $this->applyStateRemapped($schema, $blockStateData);
108 if($newStateData !== null){
109 return $newStateData;
110 }
111
112 $oldName = $blockStateData->getName();
113 $states = $blockStateData->getStates();
114
115 if(isset($schema->renamedIds[$oldName]) && isset($schema->flattenedProperties[$oldName])){
116 //TODO: this probably ought to be validated when the schema is constructed
117 throw new AssumptionFailedError("Both renamedIds and flattenedProperties are set for the same block ID \"$oldName\" - don't know what to do");
118 }
119 if(isset($schema->renamedIds[$oldName])){
120 $newName = $schema->renamedIds[$oldName] ?? null;
121 }elseif(isset($schema->flattenedProperties[$oldName])){
122 [$newName, $states] = $this->applyPropertyFlattened($schema->flattenedProperties[$oldName], $oldName, $states);
123 }else{
124 $newName = null;
125 }
126
127 $stateChanges = 0;
128
129 $states = $this->applyPropertyAdded($schema, $oldName, $states, $stateChanges);
130 $states = $this->applyPropertyRemoved($schema, $oldName, $states, $stateChanges);
131 $states = $this->applyPropertyRenamedOrValueChanged($schema, $oldName, $states, $stateChanges);
132 $states = $this->applyPropertyValueChanged($schema, $oldName, $states, $stateChanges);
133
134 if($newName !== null || $stateChanges > 0){
135 return new BlockStateData($newName ?? $oldName, $states, $schema->getVersionId());
136 }
137
138 return $blockStateData;
139 }
140
141 private function applyStateRemapped(BlockStateUpgradeSchema $schema, BlockStateData $blockStateData) : ?BlockStateData{
142 $oldName = $blockStateData->getName();
143 $oldState = $blockStateData->getStates();
144
145 if(isset($schema->remappedStates[$oldName])){
146 foreach($schema->remappedStates[$oldName] as $remap){
147 if(count($remap->oldState) > count($oldState)){
148 //match criteria has more requirements than we have state properties
149 continue; //try next state
150 }
151 foreach(Utils::stringifyKeys($remap->oldState) as $k => $v){
152 if(!isset($oldState[$k]) || !$oldState[$k]->equals($v)){
153 continue 2; //try next state
154 }
155 }
156
157 if(is_string($remap->newName)){
158 $newName = $remap->newName;
159 }else{
160 //discard flatten modifications to state - the remap newState and copiedState will take care of it
161 [$newName, ] = $this->applyPropertyFlattened($remap->newName, $oldName, $oldState);
162 }
163
164 $newState = $remap->newState;
165 foreach($remap->copiedState as $stateName){
166 if(isset($oldState[$stateName])){
167 $newState[$stateName] = $oldState[$stateName];
168 }
169 }
170
171 return new BlockStateData($newName, $newState, $schema->getVersionId());
172 }
173 }
174
175 return null;
176 }
177
185 private function applyPropertyAdded(BlockStateUpgradeSchema $schema, string $oldName, array $states, int &$stateChanges) : array{
186 if(isset($schema->addedProperties[$oldName])){
187 foreach(Utils::stringifyKeys($schema->addedProperties[$oldName]) as $propertyName => $value){
188 if(!isset($states[$propertyName])){
189 $stateChanges++;
190 $states[$propertyName] = $value;
191 }
192 }
193 }
194
195 return $states;
196 }
197
205 private function applyPropertyRemoved(BlockStateUpgradeSchema $schema, string $oldName, array $states, int &$stateChanges) : array{
206 if(isset($schema->removedProperties[$oldName])){
207 foreach($schema->removedProperties[$oldName] as $propertyName){
208 if(isset($states[$propertyName])){
209 $stateChanges++;
210 unset($states[$propertyName]);
211 }
212 }
213 }
214
215 return $states;
216 }
217
218 private function locateNewPropertyValue(BlockStateUpgradeSchema $schema, string $oldName, string $oldPropertyName, Tag $oldValue) : Tag{
219 if(isset($schema->remappedPropertyValues[$oldName][$oldPropertyName])){
220 foreach($schema->remappedPropertyValues[$oldName][$oldPropertyName] as $mappedPair){
221 if($mappedPair->old->equals($oldValue)){
222 return $mappedPair->new;
223 }
224 }
225 }
226
227 return $oldValue;
228 }
229
237 private function applyPropertyRenamedOrValueChanged(BlockStateUpgradeSchema $schema, string $oldName, array $states, int &$stateChanges) : array{
238 if(isset($schema->renamedProperties[$oldName])){
239 foreach(Utils::stringifyKeys($schema->renamedProperties[$oldName]) as $oldPropertyName => $newPropertyName){
240 $oldValue = $states[$oldPropertyName] ?? null;
241 if($oldValue !== null){
242 $stateChanges++;
243 unset($states[$oldPropertyName]);
244
245 //If a value remap is needed, we need to do it here, since we won't be able to locate the property
246 //after it's been renamed - value remaps are always indexed by old property name for the sake of
247 //being able to do changes in any order.
248 $states[$newPropertyName] = $this->locateNewPropertyValue($schema, $oldName, $oldPropertyName, $oldValue);
249 }
250 }
251 }
252
253 return $states;
254 }
255
263 private function applyPropertyValueChanged(BlockStateUpgradeSchema $schema, string $oldName, array $states, int &$stateChanges) : array{
264 if(isset($schema->remappedPropertyValues[$oldName])){
265 foreach(Utils::stringifyKeys($schema->remappedPropertyValues[$oldName]) as $oldPropertyName => $remappedValues){
266 $oldValue = $states[$oldPropertyName] ?? null;
267 if($oldValue !== null){
268 $newValue = $this->locateNewPropertyValue($schema, $oldName, $oldPropertyName, $oldValue);
269 if($newValue !== $oldValue){
270 $stateChanges++;
271 $states[$oldPropertyName] = $newValue;
272 }
273 }
274 }
275 }
276
277 return $states;
278 }
279
287 private function applyPropertyFlattened(BlockStateUpgradeSchemaFlattenInfo $flattenInfo, string $oldName, array $states) : array{
288 $flattenedValue = $states[$flattenInfo->flattenedProperty] ?? null;
289 $expectedType = $flattenInfo->flattenedPropertyType;
290 if(!$flattenedValue instanceof $expectedType){
291 //flattened property is not of the expected type, so this transformation is not applicable
292 return [$oldName, $states];
293 }
294 $embedKey = match(get_class($flattenedValue)){
295 StringTag::class => $flattenedValue->getValue(),
296 ByteTag::class => (string) $flattenedValue->getValue(),
297 IntTag::class => (string) $flattenedValue->getValue(),
298 //flattenedPropertyType is always one of these three types, but PHPStan doesn't know that
299 default => throw new AssumptionFailedError("flattenedPropertyType should be one of these three types, but have " . get_class($flattenedValue)),
300 };
301 $embedValue = $flattenInfo->flattenedValueRemaps[$embedKey] ?? $embedKey;
302 $newName = sprintf("%s%s%s", $flattenInfo->prefix, $embedValue, $flattenInfo->suffix);
303 unset($states[$flattenInfo->flattenedProperty]);
304
305 return [$newName, $states];
306 }
307}