55        if(isset($data->count) && $data->count !== 1){
 
   60        if(isset($data->tag)){
 
   64        $meta = $data->meta ?? 
null;
 
   65        if($meta === RecipeIngredientData::WILDCARD_META_VALUE){
 
   72        $itemStack = self::deserializeItemStackFromFields(
 
   76            $data->block_states ?? 
null,
 
   81        if($itemStack === 
null){
 
   90        return self::deserializeItemStackFromFields(
 
   94            $data->block_states ?? 
null,
 
   96            $data->can_place_on ?? [],
 
   97            $data->can_destroy ?? []
 
  105    private static function deserializeItemStackFromFields(
string $name, ?
int $meta, ?
int $count, ?
string $blockStatesRaw, ?
string $nbtRaw, array $canPlaceOn, array $canDestroy) : ?
Item{
 
  109        $blockName = BlockItemIdMap::getInstance()->lookupBlockId($name);
 
  110        if($blockName !== 
null){
 
  114            $blockStatesTag = $blockStatesRaw === 
null ?
 
  117                    ->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($blockStatesRaw, 
true)))
 
  118                    ->mustGetCompoundTag()
 
  122            $blockStateData = 
null;
 
  126            ->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($nbtRaw, 
true)))
 
  127            ->mustGetCompoundTag();
 
  144            return GlobalItemDataHandlers::getDeserializer()->deserializeStack($itemStackData);
 
  159        $recipes = json_decode(
Filesystem::fileGetContents($filePath));
 
  160        if(!is_array($recipes)){
 
  164        $mapper = new \JsonMapper();
 
  165        $mapper->bStrictObjectTypeChecking = 
false; 
 
  166        $mapper->bExceptionOnUndefinedProperty = 
true;
 
  167        $mapper->bExceptionOnMissingData = 
true;
 
  169        return self::loadJsonObjectListIntoModel($mapper, $modelCLass, $recipes);
 
 
  177    private static function loadJsonObjectIntoModel(\JsonMapper $mapper, 
string $modelClass, 
object $data) : object{
 
  180            return $mapper->map($data, (new \ReflectionClass($modelClass))->newInstanceWithoutConstructor());
 
  181        }
catch(\JsonMapper_Exception $e){
 
  182            throw new SavedDataLoadingException($e->getMessage(), 0, $e);
 
  194    private static function loadJsonObjectListIntoModel(\JsonMapper $mapper, 
string $modelClass, array $data) : array{
 
  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));
 
  201                $result[] = self::loadJsonObjectIntoModel($mapper, $modelClass, $item);
 
  202            }
catch(SavedDataLoadingException $e){
 
  203                throw new SavedDataLoadingException(
"Invalid entry at index $i: " . $e->getMessage(), 0, $e);
 
  209    public static function make(
string $directoryPath) : CraftingManager{
 
  210        $result = new CraftingManager();
 
  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,
 
  220            if($recipeType === 
null){
 
  224            foreach($recipe->input as $inputData){
 
  225                $input = self::deserializeIngredient($inputData);
 
  232            foreach($recipe->output as $outputData){
 
  233                $output = self::deserializeItemStack($outputData);
 
  234                if($output === null){ 
 
  237                $outputs[] = $output;
 
  240            $result->registerShapelessRecipe(
new ShapelessRecipe(
 
  246        foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 
'shaped_crafting.json'), ShapedRecipeData::class) as $recipe){
 
  247            if($recipe->block !== 
"crafting_table"){ 
 
  251            foreach(Utils::stringifyKeys($recipe->input) as $symbol => $inputData){
 
  252                $input = self::deserializeIngredient($inputData);
 
  256                $inputs[$symbol] = $input;
 
  259            foreach($recipe->output as $outputData){
 
  260                $output = self::deserializeItemStack($outputData);
 
  261                if($output === 
null){ 
 
  264                $outputs[] = $output;
 
  267            $result->registerShapedRecipe(
new ShapedRecipe(
 
  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                "campfire" => FurnaceType::CAMPFIRE,
 
  279                "soul_campfire" => FurnaceType::SOUL_CAMPFIRE,
 
  282            if($furnaceType === 
null){
 
  285            $output = self::deserializeItemStack($recipe->output);
 
  286            if($output === 
null){
 
  289            $input = self::deserializeIngredient($recipe->input);
 
  293            $result->getFurnaceRecipeManager($furnaceType)->register(
new FurnaceRecipe(
 
  299        foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 
'potion_type.json'), PotionTypeRecipeData::class) as $recipe){
 
  300            $input = self::deserializeIngredient($recipe->input);
 
  301            $ingredient = self::deserializeIngredient($recipe->ingredient);
 
  302            $output = self::deserializeItemStack($recipe->output);
 
  303            if($input === 
null || $ingredient === 
null || $output === 
null){
 
  306            $result->registerPotionTypeRecipe(
new PotionTypeRecipe(
 
  312        foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 
'potion_container_change.json'), PotionContainerChangeRecipeData::class) as $recipe){
 
  313            $ingredient = self::deserializeIngredient($recipe->ingredient);
 
  314            if($ingredient === 
null){
 
  318            $inputId = $recipe->input_item_name;
 
  319            $outputId = $recipe->output_item_name;
 
  323                self::deserializeItemStackFromFields($inputId, 
null, 
null, 
null, 
null, [], []) === 
null ||
 
  324                self::deserializeItemStackFromFields($outputId, 
null, 
null, 
null, 
null, [], []) === 
null 
  329            $result->registerPotionContainerChangeRecipe(
new PotionContainerChangeRecipe(