PocketMine-MP 5.15.1 git-5ef247620a7c6301a849b54e5ef1009217729fc8
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 $result->registerShapelessRecipe(new ShapelessRecipe(
240 $inputs,
241 $outputs,
242 $recipeType
243 ));
244 }
245 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shaped_crafting.json'), ShapedRecipeData::class) as $recipe){
246 if($recipe->block !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics
247 continue;
248 }
249 $inputs = [];
250 foreach(Utils::stringifyKeys($recipe->input) as $symbol => $inputData){
251 $input = self::deserializeIngredient($inputData);
252 if($input === null){ //unknown input item
253 continue 2;
254 }
255 $inputs[$symbol] = $input;
256 }
257 $outputs = [];
258 foreach($recipe->output as $outputData){
259 $output = self::deserializeItemStack($outputData);
260 if($output === null){ //unknown output item
261 continue 2;
262 }
263 $outputs[] = $output;
264 }
265 $result->registerShapedRecipe(new ShapedRecipe(
266 $recipe->shape,
267 $inputs,
268 $outputs
269 ));
270 }
271 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'smelting.json'), FurnaceRecipeData::class) as $recipe){
272 $furnaceType = match ($recipe->block){
273 "furnace" => FurnaceType::FURNACE,
274 "blast_furnace" => FurnaceType::BLAST_FURNACE,
275 "smoker" => FurnaceType::SMOKER,
276 //TODO: campfire
277 default => null
278 };
279 if($furnaceType === null){
280 continue;
281 }
282 $output = self::deserializeItemStack($recipe->output);
283 if($output === null){
284 continue;
285 }
286 $input = self::deserializeIngredient($recipe->input);
287 if($input === null){
288 continue;
289 }
290 $result->getFurnaceRecipeManager($furnaceType)->register(new FurnaceRecipe(
291 $output,
292 $input
293 ));
294 }
295
296 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'potion_type.json'), PotionTypeRecipeData::class) as $recipe){
297 $input = self::deserializeIngredient($recipe->input);
298 $ingredient = self::deserializeIngredient($recipe->ingredient);
299 $output = self::deserializeItemStack($recipe->output);
300 if($input === null || $ingredient === null || $output === null){
301 continue;
302 }
303 $result->registerPotionTypeRecipe(new PotionTypeRecipe(
304 $input,
305 $ingredient,
306 $output
307 ));
308 }
309 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'potion_container_change.json'), PotionContainerChangeRecipeData::class) as $recipe){
310 $ingredient = self::deserializeIngredient($recipe->ingredient);
311 if($ingredient === null){
312 continue;
313 }
314
315 $inputId = $recipe->input_item_name;
316 $outputId = $recipe->output_item_name;
317
318 //TODO: this is a really awful way to just check if an ID is recognized ...
319 if(
320 self::deserializeItemStackFromFields($inputId, null, null, null, null, [], []) === null ||
321 self::deserializeItemStackFromFields($outputId, null, null, null, null, [], []) === null
322 ){
323 //unknown item
324 continue;
325 }
326 $result->registerPotionContainerChangeRecipe(new PotionContainerChangeRecipe(
327 $inputId,
328 $ingredient,
329 $outputId
330 ));
331 }
332
333 //TODO: smithing
334
335 return $result;
336 }
337}
static loadJsonArrayOfObjectsFile(string $filePath, string $modelCLass)
static current(string $name, array $states)
static trapAndRemoveFalse(\Closure $closure, int $levels=E_WARNING|E_NOTICE)