count) && $data->count !== 1){ //every case we've seen so far where this isn't the case, it's been a bug and the count was ignored anyway //e.g. gold blocks crafted from 9 ingots, but each input item individually had a count of 9 throw new SavedDataLoadingException("Recipe inputs should have a count of exactly 1"); } if(isset($data->tag)){ return new TagWildcardRecipeIngredient($data->tag); } $meta = $data->meta ?? null; if($meta === RecipeIngredientData::WILDCARD_META_VALUE){ //this could be an unimplemented item, but it doesn't really matter, since the item shouldn't be able to //be obtained anyway - filtering unknown items is only really important for outputs, to prevent players //obtaining them return new MetaWildcardRecipeIngredient($data->name); } $itemStack = self::deserializeItemStackFromFields( $data->name, $meta, $data->count ?? null, $data->block_states ?? null, null, [], [] ); if($itemStack === null){ //probably unknown item return null; } return new ExactRecipeIngredient($itemStack); } public static function deserializeItemStack(ItemStackData $data) : ?Item{ //count, name, block_name, block_states, meta, nbt, can_place_on, can_destroy return self::deserializeItemStackFromFields( $data->name, $data->meta ?? null, $data->count ?? null, $data->block_states ?? null, $data->nbt ?? null, $data->can_place_on ?? [], $data->can_destroy ?? [] ); } /** * @param string[] $canPlaceOn * @param string[] $canDestroy */ private static function deserializeItemStackFromFields(string $name, ?int $meta, ?int $count, ?string $blockStatesRaw, ?string $nbtRaw, array $canPlaceOn, array $canDestroy) : ?Item{ $meta ??= 0; $count ??= 1; $blockName = BlockItemIdMap::getInstance()->lookupBlockId($name); if($blockName !== null){ if($meta !== 0){ throw new SavedDataLoadingException("Meta should not be specified for blockitems"); } $blockStatesTag = $blockStatesRaw === null ? [] : (new LittleEndianNbtSerializer()) ->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($blockStatesRaw, true))) ->mustGetCompoundTag() ->getValue(); $blockStateData = BlockStateData::current($blockName, $blockStatesTag); }else{ $blockStateData = null; } $nbt = $nbtRaw === null ? null : (new LittleEndianNbtSerializer()) ->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($nbtRaw, true))) ->mustGetCompoundTag(); $itemStackData = new SavedItemStackData( new SavedItemData( $name, $meta, $blockStateData, $nbt ), $count, null, null, $canPlaceOn, $canDestroy, ); try{ return GlobalItemDataHandlers::getDeserializer()->deserializeStack($itemStackData); }catch(ItemTypeDeserializeException){ //probably unknown item return null; } } /** * @return mixed[] * * @phpstan-template TData of object * @phpstan-param class-string $modelCLass * @phpstan-return list */ public static function loadJsonArrayOfObjectsFile(string $filePath, string $modelCLass) : array{ $recipes = json_decode(Filesystem::fileGetContents($filePath)); if(!is_array($recipes)){ throw new SavedDataLoadingException("$filePath root should be an array, got " . get_debug_type($recipes)); } $mapper = new \JsonMapper(); $mapper->bStrictObjectTypeChecking = true; $mapper->bExceptionOnUndefinedProperty = true; $mapper->bExceptionOnMissingData = true; return self::loadJsonObjectListIntoModel($mapper, $modelCLass, $recipes); } /** * @phpstan-template TRecipeData of object * @phpstan-param class-string $modelClass * @phpstan-return TRecipeData */ private static function loadJsonObjectIntoModel(\JsonMapper $mapper, string $modelClass, object $data) : object{ //JsonMapper does this for subtypes, but not for the base type :( try{ return $mapper->map($data, (new \ReflectionClass($modelClass))->newInstanceWithoutConstructor()); }catch(\JsonMapper_Exception $e){ throw new SavedDataLoadingException($e->getMessage(), 0, $e); } } /** * @param mixed[] $data * @return object[] * * @phpstan-template TRecipeData of object * @phpstan-param class-string $modelClass * @phpstan-return list */ private static function loadJsonObjectListIntoModel(\JsonMapper $mapper, string $modelClass, array $data) : array{ $result = []; foreach(Utils::promoteKeys($data) as $i => $item){ if(!is_object($item)){ throw new SavedDataLoadingException("Invalid entry at index $i: expected object, got " . get_debug_type($item)); } try{ $result[] = self::loadJsonObjectIntoModel($mapper, $modelClass, $item); }catch(SavedDataLoadingException $e){ throw new SavedDataLoadingException("Invalid entry at index $i: " . $e->getMessage(), 0, $e); } } return $result; } public static function make(string $directoryPath) : CraftingManager{ $result = new CraftingManager(); foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shapeless_crafting.json'), ShapelessRecipeData::class) as $recipe){ $recipeType = match($recipe->block){ "crafting_table" => ShapelessRecipeType::CRAFTING, "stonecutter" => ShapelessRecipeType::STONECUTTER, "smithing_table" => ShapelessRecipeType::SMITHING, "cartography_table" => ShapelessRecipeType::CARTOGRAPHY, default => null }; if($recipeType === null){ continue; } $inputs = []; foreach($recipe->input as $inputData){ $input = self::deserializeIngredient($inputData); if($input === null){ //unknown input item continue 2; } $inputs[] = $input; } $outputs = []; foreach($recipe->output as $outputData){ $output = self::deserializeItemStack($outputData); if($output === null){ //unknown output item continue 2; } $outputs[] = $output; } //TODO: check unlocking requirements - our current system doesn't support this $result->registerShapelessRecipe(new ShapelessRecipe( $inputs, $outputs, $recipeType )); } foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shaped_crafting.json'), ShapedRecipeData::class) as $recipe){ if($recipe->block !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics continue; } $inputs = []; foreach(Utils::stringifyKeys($recipe->input) as $symbol => $inputData){ $input = self::deserializeIngredient($inputData); if($input === null){ //unknown input item continue 2; } $inputs[$symbol] = $input; } $outputs = []; foreach($recipe->output as $outputData){ $output = self::deserializeItemStack($outputData); if($output === null){ //unknown output item continue 2; } $outputs[] = $output; } //TODO: check unlocking requirements - our current system doesn't support this $result->registerShapedRecipe(new ShapedRecipe( $recipe->shape, $inputs, $outputs )); } foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'smelting.json'), FurnaceRecipeData::class) as $recipe){ $furnaceType = match ($recipe->block){ "furnace" => FurnaceType::FURNACE, "blast_furnace" => FurnaceType::BLAST_FURNACE, "smoker" => FurnaceType::SMOKER, "campfire" => FurnaceType::CAMPFIRE, "soul_campfire" => FurnaceType::SOUL_CAMPFIRE, default => null }; if($furnaceType === null){ continue; } $output = self::deserializeItemStack($recipe->output); if($output === null){ continue; } $input = self::deserializeIngredient($recipe->input); if($input === null){ continue; } $result->getFurnaceRecipeManager($furnaceType)->register(new FurnaceRecipe( $output, $input )); } foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'potion_type.json'), PotionTypeRecipeData::class) as $recipe){ $input = self::deserializeIngredient($recipe->input); $ingredient = self::deserializeIngredient($recipe->ingredient); $output = self::deserializeItemStack($recipe->output); if($input === null || $ingredient === null || $output === null){ continue; } $result->registerPotionTypeRecipe(new PotionTypeRecipe( $input, $ingredient, $output )); } foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'potion_container_change.json'), PotionContainerChangeRecipeData::class) as $recipe){ $ingredient = self::deserializeIngredient($recipe->ingredient); if($ingredient === null){ continue; } $inputId = $recipe->input_item_name; $outputId = $recipe->output_item_name; //TODO: this is a really awful way to just check if an ID is recognized ... if( self::deserializeItemStackFromFields($inputId, null, null, null, null, [], []) === null || self::deserializeItemStackFromFields($outputId, null, null, null, null, [], []) === null ){ //unknown item continue; } $result->registerPotionContainerChangeRecipe(new PotionContainerChangeRecipe( $inputId, $ingredient, $outputId )); } //TODO: smithing return $result; } }