PocketMine-MP 5.15.1 git-5ef247620a7c6301a849b54e5ef1009217729fc8
BlockStateReader.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\convert;
25
26use pocketmine\block\utils\BellAttachmentType;
27use pocketmine\block\utils\CoralType;
28use pocketmine\block\utils\SlabType;
29use pocketmine\block\utils\WallConnectionType;
41use function array_keys;
42use function count;
43use function get_class;
44use function implode;
45
46final class BlockStateReader{
47
52 private array $unusedStates;
53
54 public function __construct(
55 private BlockStateData $data
56 ){
57 $this->unusedStates = $this->data->getStates();
58 }
59
60 public function missingOrWrongTypeException(string $name, ?Tag $tag) : BlockStateDeserializeException{
61 return new BlockStateDeserializeException("Property \"$name\" " . ($tag !== null ? "has unexpected type " . get_class($tag) : "is missing"));
62 }
63
64 public function badValueException(string $name, string $stringifiedValue, ?string $reason = null) : BlockStateDeserializeException{
66 "Property \"$name\" has unexpected value \"$stringifiedValue\"" . (
67 $reason !== null ? " ($reason)" : ""
68 ));
69 }
70
72 public function readBool(string $name) : bool{
73 unset($this->unusedStates[$name]);
74 $tag = $this->data->getState($name);
75 if($tag instanceof ByteTag){
76 switch($tag->getValue()){
77 case 0: return false;
78 case 1: return true;
79 default: throw $this->badValueException($name, (string) $tag->getValue());
80 }
81 }
82 throw $this->missingOrWrongTypeException($name, $tag);
83 }
84
86 public function readInt(string $name) : int{
87 unset($this->unusedStates[$name]);
88 $tag = $this->data->getState($name);
89 if($tag instanceof IntTag){
90 return $tag->getValue();
91 }
92 throw $this->missingOrWrongTypeException($name, $tag);
93 }
94
96 public function readBoundedInt(string $name, int $min, int $max) : int{
97 $result = $this->readInt($name);
98 if($result < $min || $result > $max){
99 throw $this->badValueException($name, (string) $result, "Must be inside the range $min ... $max");
100 }
101 return $result;
102 }
103
105 public function readString(string $name) : string{
106 unset($this->unusedStates[$name]);
107 //TODO: only allow a specific set of values (strings are primarily used for enums)
108 $tag = $this->data->getState($name);
109 if($tag instanceof StringTag){
110 return $tag->getValue();
111 }
112 throw $this->missingOrWrongTypeException($name, $tag);
113 }
114
121 private function parseFacingValue(int $value, array $mapping) : int{
122 $result = $mapping[$value] ?? null;
123 if($result === null){
124 throw new BlockStateDeserializeException("Unmapped facing value " . $value);
125 }
126 return $result;
127 }
128
130 public function readFacingDirection() : int{
131 return $this->parseFacingValue($this->readInt(BlockStateNames::FACING_DIRECTION), [
132 0 => Facing::DOWN,
133 1 => Facing::UP,
134 2 => Facing::NORTH,
135 3 => Facing::SOUTH,
136 4 => Facing::WEST,
137 5 => Facing::EAST
138 ]);
139 }
140
142 public function readBlockFace() : int{
143 return match($raw = $this->readString(BlockStateNames::MC_BLOCK_FACE)){
144 StringValues::MC_BLOCK_FACE_DOWN => Facing::DOWN,
145 StringValues::MC_BLOCK_FACE_UP => Facing::UP,
146 StringValues::MC_BLOCK_FACE_NORTH => Facing::NORTH,
147 StringValues::MC_BLOCK_FACE_SOUTH => Facing::SOUTH,
148 StringValues::MC_BLOCK_FACE_WEST => Facing::WEST,
149 StringValues::MC_BLOCK_FACE_EAST => Facing::EAST,
150 default => throw $this->badValueException(BlockStateNames::MC_BLOCK_FACE, $raw)
151 };
152 }
153
158 public function readFacingFlags() : array{
159 $result = [];
160 $flags = $this->readBoundedInt(BlockStateNames::MULTI_FACE_DIRECTION_BITS, 0, 63);
161 foreach([
162 BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_DOWN => Facing::DOWN,
163 BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_UP => Facing::UP,
164 BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_NORTH => Facing::NORTH,
165 BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_SOUTH => Facing::SOUTH,
166 BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_WEST => Facing::WEST,
167 BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_EAST => Facing::EAST
168 ] as $flag => $facing){
169 if(($flags & $flag) !== 0){
170 $result[$facing] = $facing;
171 }
172 }
173
174 return $result;
175 }
176
178 public function readEndRodFacingDirection() : int{
179 $result = $this->readFacingDirection();
180 return Facing::axis($result) !== Axis::Y ? Facing::opposite($result) : $result;
181 }
182
184 public function readHorizontalFacing() : int{
185 return $this->parseFacingValue($this->readInt(BlockStateNames::FACING_DIRECTION), [
186 0 => Facing::NORTH, //should be illegal, but 1.13 allows it
187 1 => Facing::NORTH, //also should be illegal
188 2 => Facing::NORTH,
189 3 => Facing::SOUTH,
190 4 => Facing::WEST,
191 5 => Facing::EAST
192 ]);
193 }
194
196 public function readWeirdoHorizontalFacing() : int{
197 return $this->parseFacingValue($this->readInt(BlockStateNames::WEIRDO_DIRECTION), [
198 0 => Facing::EAST,
199 1 => Facing::WEST,
200 2 => Facing::SOUTH,
201 3 => Facing::NORTH
202 ]);
203 }
204
206 public function readLegacyHorizontalFacing() : int{
207 return $this->parseFacingValue($this->readInt(BlockStateNames::DIRECTION), [
208 0 => Facing::SOUTH,
209 1 => Facing::WEST,
210 2 => Facing::NORTH,
211 3 => Facing::EAST
212 ]);
213 }
214
219 public function read5MinusHorizontalFacing() : int{
220 return $this->parseFacingValue($this->readInt(BlockStateNames::DIRECTION), [
221 0 => Facing::EAST,
222 1 => Facing::WEST,
223 2 => Facing::SOUTH,
224 3 => Facing::NORTH
225 ]);
226 }
227
232 public function readCardinalHorizontalFacing() : int{
233 return match($raw = $this->readString(BlockStateNames::MC_CARDINAL_DIRECTION)){
234 StringValues::MC_CARDINAL_DIRECTION_NORTH => Facing::NORTH,
235 StringValues::MC_CARDINAL_DIRECTION_SOUTH => Facing::SOUTH,
236 StringValues::MC_CARDINAL_DIRECTION_WEST => Facing::WEST,
237 StringValues::MC_CARDINAL_DIRECTION_EAST => Facing::EAST,
238 default => throw $this->badValueException(BlockStateNames::MC_CARDINAL_DIRECTION, $raw)
239 };
240 }
241
243 public function readCoralFacing() : int{
244 return $this->parseFacingValue($this->readInt(BlockStateNames::CORAL_DIRECTION), [
245 0 => Facing::WEST,
246 1 => Facing::EAST,
247 2 => Facing::NORTH,
248 3 => Facing::SOUTH
249 ]);
250 }
251
253 public function readFacingWithoutDown() : int{
254 $result = $this->readFacingDirection();
255 if($result === Facing::DOWN){ //shouldn't be legal, but 1.13 allows it
256 $result = Facing::UP;
257 }
258 return $result;
259 }
260
261 public function readFacingWithoutUp() : int{
262 $result = $this->readFacingDirection();
263 if($result === Facing::UP){
264 $result = Facing::DOWN; //shouldn't be legal, but 1.13 allows it
265 }
266 return $result;
267 }
268
273 public function readPillarAxis() : int{
274 $rawValue = $this->readString(BlockStateNames::PILLAR_AXIS);
275 $value = [
276 StringValues::PILLAR_AXIS_X => Axis::X,
277 StringValues::PILLAR_AXIS_Y => Axis::Y,
278 StringValues::PILLAR_AXIS_Z => Axis::Z
279 ][$rawValue] ?? null;
280 if($value === null){
281 throw $this->badValueException(BlockStateNames::PILLAR_AXIS, $rawValue, "Invalid axis value");
282 }
283 return $value;
284 }
285
287 public function readSlabPosition() : SlabType{
288 return match($rawValue = $this->readString(BlockStateNames::MC_VERTICAL_HALF)){
289 StringValues::MC_VERTICAL_HALF_BOTTOM => SlabType::BOTTOM,
290 StringValues::MC_VERTICAL_HALF_TOP => SlabType::TOP,
291 default => throw $this->badValueException(BlockStateNames::MC_VERTICAL_HALF, $rawValue, "Invalid slab position"),
292 };
293 }
294
299 public function readTorchFacing() : int{
300 //TODO: horizontal directions are flipped (MCPE bug: https://bugs.mojang.com/browse/MCPE-152036)
301 return match($rawValue = $this->readString(BlockStateNames::TORCH_FACING_DIRECTION)){
302 StringValues::TORCH_FACING_DIRECTION_EAST => Facing::WEST,
303 StringValues::TORCH_FACING_DIRECTION_NORTH => Facing::SOUTH,
304 StringValues::TORCH_FACING_DIRECTION_SOUTH => Facing::NORTH,
305 StringValues::TORCH_FACING_DIRECTION_TOP => Facing::UP,
306 StringValues::TORCH_FACING_DIRECTION_UNKNOWN => Facing::UP, //should be illegal, but 1.13 allows it
307 StringValues::TORCH_FACING_DIRECTION_WEST => Facing::EAST,
308 default => throw $this->badValueException(BlockStateNames::TORCH_FACING_DIRECTION, $rawValue, "Invalid torch facing"),
309 };
310 }
311
313 public function readCoralType() : CoralType{
314 return match($type = $this->readString(BlockStateNames::CORAL_COLOR)){
315 StringValues::CORAL_COLOR_BLUE => CoralType::TUBE,
316 StringValues::CORAL_COLOR_PINK => CoralType::BRAIN,
317 StringValues::CORAL_COLOR_PURPLE => CoralType::BUBBLE,
318 StringValues::CORAL_COLOR_RED => CoralType::FIRE,
319 StringValues::CORAL_COLOR_YELLOW => CoralType::HORN,
320 default => throw $this->badValueException(BlockStateNames::CORAL_COLOR, $type),
321 };
322 }
323
325 public function readBellAttachmentType() : BellAttachmentType{
326 return match($type = $this->readString(BlockStateNames::ATTACHMENT)){
327 StringValues::ATTACHMENT_HANGING => BellAttachmentType::CEILING,
328 StringValues::ATTACHMENT_STANDING => BellAttachmentType::FLOOR,
329 StringValues::ATTACHMENT_SIDE => BellAttachmentType::ONE_WALL,
330 StringValues::ATTACHMENT_MULTIPLE => BellAttachmentType::TWO_WALLS,
331 default => throw $this->badValueException(BlockStateNames::ATTACHMENT, $type),
332 };
333 }
334
336 public function readWallConnectionType(string $name) : ?WallConnectionType{
337 return match($type = $this->readString($name)){
338 //TODO: this looks a bit confusing due to use of EAST, but the values are the same for all connections
339 //we need to find a better way to auto-generate the constant names when they are reused
340 //for now, using these constants is better than nothing since it still gives static analysability
341 StringValues::WALL_CONNECTION_TYPE_EAST_NONE => null,
342 StringValues::WALL_CONNECTION_TYPE_EAST_SHORT => WallConnectionType::SHORT,
343 StringValues::WALL_CONNECTION_TYPE_EAST_TALL => WallConnectionType::TALL,
344 default => throw $this->badValueException($name, $type),
345 };
346 }
347
351 public function ignored(string $name) : void{
352 if($this->data->getState($name) !== null){
353 unset($this->unusedStates[$name]);
354 }else{
355 throw $this->missingOrWrongTypeException($name, null);
356 }
357 }
358
362 public function todo(string $name) : void{
363 $this->ignored($name);
364 }
365
369 public function checkUnreadProperties() : void{
370 if(count($this->unusedStates) > 0){
371 throw new BlockStateDeserializeException("Unread properties: " . implode(", ", array_keys($this->unusedStates)));
372 }
373 }
374}