PocketMine-MP 5.17.1 git-df4ada81e5d74a14046f27cf44a37dcee69d657e
CraftingManagerFromDataHelper.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\crafting;
25
45use Symfony\Component\Filesystem\Path;
46use function base64_decode;
47use function get_debug_type;
48use function is_array;
49use function is_object;
50use function json_decode;
51
53
54 private static function deserializeIngredient(RecipeIngredientData $data) : ?RecipeIngredient{
55 if(isset($data->count) && $data->count !== 1){
56 //every case we've seen so far where this isn't the case, it's been a bug and the count was ignored anyway
57 //e.g. gold blocks crafted from 9 ingots, but each input item individually had a count of 9
58 throw new SavedDataLoadingException("Recipe inputs should have a count of exactly 1");
59 }
60 if(isset($data->tag)){
61 return new TagWildcardRecipeIngredient($data->tag);
62 }
63
64 $meta = $data->meta ?? null;
65 if($meta === RecipeIngredientData::WILDCARD_META_VALUE){
66 //this could be an unimplemented item, but it doesn't really matter, since the item shouldn't be able to
67 //be obtained anyway - filtering unknown items is only really important for outputs, to prevent players
68 //obtaining them
69 return new MetaWildcardRecipeIngredient($data->name);
70 }
71
72 $itemStack = self::deserializeItemStackFromFields(
73 $data->name,
74 $meta,
75 $data->count ?? null,
76 $data->block_states ?? null,
77 null,
78 [],
79 []
80 );
81 if($itemStack === null){
82 //probably unknown item
83 return null;
84 }
85 return new ExactRecipeIngredient($itemStack);
86 }
87
88 public static function deserializeItemStack(ItemStackData $data) : ?Item{
89 //count, name, block_name, block_states, meta, nbt, can_place_on, can_destroy
90 return self::deserializeItemStackFromFields(
91 $data->name,
92 $data->meta ?? null,
93 $data->count ?? null,
94 $data->block_states ?? null,
95 $data->nbt ?? null,
96 $data->can_place_on ?? [],
97 $data->can_destroy ?? []
98 );
99 }
100
105 private static function deserializeItemStackFromFields(string $name, ?int $meta, ?int $count, ?string $blockStatesRaw, ?string $nbtRaw, array $canPlaceOn, array $canDestroy) : ?Item{
106 $meta ??= 0;
107 $count ??= 1;
108
109 $blockName = BlockItemIdMap::getInstance()->lookupBlockId($name);
110 if($blockName !== null){
111 if($meta !== 0){
112 throw new SavedDataLoadingException("Meta should not be specified for blockitems");
113 }
114 $blockStatesTag = $blockStatesRaw === null ?
115 [] :
117 ->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($blockStatesRaw, true)))
118 ->mustGetCompoundTag()
119 ->getValue();
120 $blockStateData = BlockStateData::current($blockName, $blockStatesTag);
121 }else{
122 $blockStateData = null;
123 }
124
125 $nbt = $nbtRaw === null ? null : (new LittleEndianNbtSerializer())
126 ->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($nbtRaw, true)))
127 ->mustGetCompoundTag();
128
129 $itemStackData = new SavedItemStackData(
130 new SavedItemData(
131 $name,
132 $meta,
133 $blockStateData,
134 $nbt
135 ),
136 $count,
137 null,
138 null,
139 $canPlaceOn,
140 $canDestroy,
141 );
142
143 try{
144 return GlobalItemDataHandlers::getDeserializer()->deserializeStack($itemStackData);
146 //probably unknown item
147 return null;
148 }
149 }
150
158 public static function loadJsonArrayOfObjectsFile(string $filePath, string $modelCLass) : array{
159 $recipes = json_decode(Filesystem::fileGetContents($filePath));
160 if(!is_array($recipes)){
161 throw new SavedDataLoadingException("$filePath root should be an array, got " . get_debug_type($recipes));
162 }
163
164 $mapper = new \JsonMapper();
165 $mapper->bStrictObjectTypeChecking = true;
166 $mapper->bExceptionOnUndefinedProperty = true;
167 $mapper->bExceptionOnMissingData = true;
168
169 return self::loadJsonObjectListIntoModel($mapper, $modelCLass, $recipes);
170 }
171
177 private static function loadJsonObjectIntoModel(\JsonMapper $mapper, string $modelClass, object $data) : object{
178 //JsonMapper does this for subtypes, but not for the base type :(
179 try{
180 return $mapper->map($data, (new \ReflectionClass($modelClass))->newInstanceWithoutConstructor());
181 }catch(\JsonMapper_Exception $e){
182 throw new SavedDataLoadingException($e->getMessage(), 0, $e);
183 }
184 }
185
194 private static function loadJsonObjectListIntoModel(\JsonMapper $mapper, string $modelClass, array $data) : array{
195 $result = [];
196 foreach($data as $i => $item){
197 if(!is_object($item)){
198 throw new SavedDataLoadingException("Invalid entry at index $i: expected object, got " . get_debug_type($item));
199 }
200 try{
201 $result[] = self::loadJsonObjectIntoModel($mapper, $modelClass, $item);
202 }catch(SavedDataLoadingException $e){
203 throw new SavedDataLoadingException("Invalid entry at index $i: " . $e->getMessage(), 0, $e);
204 }
205 }
206 return $result;
207 }
208
209 public static function make(string $directoryPath) : CraftingManager{
210 $result = new CraftingManager();
211
212 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shapeless_crafting.json'), ShapelessRecipeData::class) as $recipe){
213 $recipeType = match($recipe->block){
214 "crafting_table" => ShapelessRecipeType::CRAFTING,
215 "stonecutter" => ShapelessRecipeType::STONECUTTER,
216 "smithing_table" => ShapelessRecipeType::SMITHING,
217 "cartography_table" => ShapelessRecipeType::CARTOGRAPHY,
218 default => null
219 };
220 if($recipeType === null){
221 continue;
222 }
223 $inputs = [];
224 foreach($recipe->input as $inputData){
225 $input = self::deserializeIngredient($inputData);
226 if($input === null){ //unknown input item
227 continue 2;
228 }
229 $inputs[] = $input;
230 }
231 $outputs = [];
232 foreach($recipe->output as $outputData){
233 $output = self::deserializeItemStack($outputData);
234 if($output === null){ //unknown output item
235 continue 2;
236 }
237 $outputs[] = $output;
238 }
239 //TODO: check unlocking requirements - our current system doesn't support this
240 $result->registerShapelessRecipe(new ShapelessRecipe(
241 $inputs,
242 $outputs,
243 $recipeType
244 ));
245 }
246 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shaped_crafting.json'), ShapedRecipeData::class) as $recipe){
247 if($recipe->block !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics
248 continue;
249 }
250 $inputs = [];
251 foreach(Utils::stringifyKeys($recipe->input) as $symbol => $inputData){
252 $input = self::deserializeIngredient($inputData);
253 if($input === null){ //unknown input item
254 continue 2;
255 }
256 $inputs[$symbol] = $input;
257 }
258 $outputs = [];
259 foreach($recipe->output as $outputData){
260 $output = self::deserializeItemStack($outputData);
261 if($output === null){ //unknown output item
262 continue 2;
263 }
264 $outputs[] = $output;
265 }
266 //TODO: check unlocking requirements - our current system doesn't support this
267 $result->registerShapedRecipe(new ShapedRecipe(
268 $recipe->shape,
269 $inputs,
270 $outputs
271 ));
272 }
273 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'smelting.json'), FurnaceRecipeData::class) as $recipe){
274 $furnaceType = match ($recipe->block){
275 "furnace" => FurnaceType::FURNACE,
276 "blast_furnace" => FurnaceType::BLAST_FURNACE,
277 "smoker" => FurnaceType::SMOKER,
278 //TODO: campfire
279 default => null
280 };
281 if($furnaceType === null){
282 continue;
283 }
284 $output = self::deserializeItemStack($recipe->output);
285 if($output === null){
286 continue;
287 }
288 $input = self::deserializeIngredient($recipe->input);
289 if($input === null){
290 continue;
291 }
292 $result->getFurnaceRecipeManager($furnaceType)->register(new FurnaceRecipe(
293 $output,
294 $input
295 ));
296 }
297
298 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'potion_type.json'), PotionTypeRecipeData::class) as $recipe){
299 $input = self::deserializeIngredient($recipe->input);
300 $ingredient = self::deserializeIngredient($recipe->ingredient);
301 $output = self::deserializeItemStack($recipe->output);
302 if($input === null || $ingredient === null || $output === null){
303 continue;
304 }
305 $result->registerPotionTypeRecipe(new PotionTypeRecipe(
306 $input,
307 $ingredient,
308 $output
309 ));
310 }
311 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'potion_container_change.json'), PotionContainerChangeRecipeData::class) as $recipe){
312 $ingredient = self::deserializeIngredient($recipe->ingredient);
313 if($ingredient === null){
314 continue;
315 }
316
317 $inputId = $recipe->input_item_name;
318 $outputId = $recipe->output_item_name;
319
320 //TODO: this is a really awful way to just check if an ID is recognized ...
321 if(
322 self::deserializeItemStackFromFields($inputId, null, null, null, null, [], []) === null ||
323 self::deserializeItemStackFromFields($outputId, null, null, null, null, [], []) === null
324 ){
325 //unknown item
326 continue;
327 }
328 $result->registerPotionContainerChangeRecipe(new PotionContainerChangeRecipe(
329 $inputId,
330 $ingredient,
331 $outputId
332 ));
333 }
334
335 //TODO: smithing
336
337 return $result;
338 }
339}
static loadJsonArrayOfObjectsFile(string $filePath, string $modelCLass)
static current(string $name, array $states)
static trapAndRemoveFalse(\Closure $closure, int $levels=E_WARNING|E_NOTICE)