PocketMine-MP 5.43.2 git-10285977bca624a6424b6c684dfc75cb8fb37c1a
Loading...
Searching...
No Matches
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
44use Symfony\Component\Filesystem\Path;
45use function base64_decode;
46use function count;
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 = false; //to allow hydrating ItemStackData - since this is only used for offline data it's safe
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(Utils::promoteKeys($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 "furnace" => FurnaceType::FURNACE,
219 "blast_furnace" => FurnaceType::BLAST_FURNACE,
220 "smoker" => FurnaceType::SMOKER,
221 "campfire" => FurnaceType::CAMPFIRE,
222 "soul_campfire" => FurnaceType::SOUL_CAMPFIRE,
223 default => null
224 };
225 if($recipeType === null){
226 continue;
227 }
228 $inputs = [];
229 foreach($recipe->input as $inputData){
230 $input = self::deserializeIngredient($inputData);
231 if($input === null){ //unknown input item
232 continue 2;
233 }
234 $inputs[] = $input;
235 }
236 $outputs = [];
237 foreach($recipe->output as $outputData){
238 $output = self::deserializeItemStack($outputData);
239 if($output === null){ //unknown output item
240 continue 2;
241 }
242 $outputs[] = $output;
243 }
244 //TODO: check unlocking requirements - our current system doesn't support this
245
246 if($recipeType instanceof FurnaceType){
247 if(count($inputs) !== 1 || count($outputs) !== 1){
248 throw new SavedDataLoadingException("Furnace recipes must have exactly 1 input and 1 output");
249 }
250
251 $result->getFurnaceRecipeManager($recipeType)->register(new FurnaceRecipe(
252 $outputs[0],
253 $inputs[0]
254 ));
255 }else{
256 $result->registerShapelessRecipe(new ShapelessRecipe(
257 $inputs,
258 $outputs,
259 $recipeType
260 ));
261 }
262 }
263 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shaped_crafting.json'), ShapedRecipeData::class) as $recipe){
264 if($recipe->block !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics
265 continue;
266 }
267 $inputs = [];
268 foreach(Utils::stringifyKeys($recipe->input) as $symbol => $inputData){
269 $input = self::deserializeIngredient($inputData);
270 if($input === null){ //unknown input item
271 continue 2;
272 }
273 $inputs[$symbol] = $input;
274 }
275 $outputs = [];
276 foreach($recipe->output as $outputData){
277 $output = self::deserializeItemStack($outputData);
278 if($output === null){ //unknown output item
279 continue 2;
280 }
281 $outputs[] = $output;
282 }
283 //TODO: check unlocking requirements - our current system doesn't support this
284 $result->registerShapedRecipe(new ShapedRecipe(
285 $recipe->shape,
286 $inputs,
287 $outputs
288 ));
289 }
290
291 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'potion_type.json'), PotionTypeRecipeData::class) as $recipe){
292 $input = self::deserializeIngredient($recipe->input);
293 $ingredient = self::deserializeIngredient($recipe->ingredient);
294 $output = self::deserializeItemStack($recipe->output);
295 if($input === null || $ingredient === null || $output === null){
296 continue;
297 }
298 $result->registerPotionTypeRecipe(new PotionTypeRecipe(
299 $input,
300 $ingredient,
301 $output
302 ));
303 }
304 foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'potion_container_change.json'), PotionContainerChangeRecipeData::class) as $recipe){
305 $ingredient = self::deserializeIngredient($recipe->ingredient);
306 if($ingredient === null){
307 continue;
308 }
309
310 $inputId = $recipe->input_item_name;
311 $outputId = $recipe->output_item_name;
312
313 //TODO: this is a really awful way to just check if an ID is recognized ...
314 if(
315 self::deserializeItemStackFromFields($inputId, null, null, null, null, [], []) === null ||
316 self::deserializeItemStackFromFields($outputId, null, null, null, null, [], []) === null
317 ){
318 //unknown item
319 continue;
320 }
321 $result->registerPotionContainerChangeRecipe(new PotionContainerChangeRecipe(
322 $inputId,
323 $ingredient,
324 $outputId
325 ));
326 }
327
328 //TODO: smithing
329
330 return $result;
331 }
332}
static loadJsonArrayOfObjectsFile(string $filePath, string $modelClass)
static current(string $name, array $states)
static trapAndRemoveFalse(\Closure $closure, int $levels=E_WARNING|E_NOTICE)