lookupBlockId($name); if($blockName !== null){ $blockStateDictionary = RuntimeBlockMapping::getInstance()->getBlockStateDictionary(); $blockRuntimeId = $blockStateDictionary->lookupStateIdFromIdMeta($name, $meta === RecipeIngredientData::WILDCARD_META_VALUE ? 0 : $meta); if($blockRuntimeId === null){ throw new SavedDataLoadingException("$blockName with meta $meta doesn't map to any known blockstate"); } $blockStateData = $blockStateDictionary->getDataFromStateId($blockRuntimeId); if($blockStateData === null){ throw new AssumptionFailedError("We just looked up the runtime ID for this state, so it can't possibly be null"); } }else{ $blockStateData = null; } //TODO: for wildcards, we only need a way to check if the item serializer recognizes the ID; we don't need to //deserialize the whole itemstack, which might give bogus results anyway if meta 0 isn't recognized $itemTypeData = new SavedItemData( $name, $meta === RecipeIngredientData::WILDCARD_META_VALUE ? 0 : $meta, $blockStateData, null ); try{ return GlobalItemDataHandlers::getDeserializer()->deserializeType($itemTypeData); }catch(ItemTypeDeserializeException){ //probably unknown item return null; } } private static function deserializeIngredient(RecipeIngredientData $data) : ?RecipeIngredient{ if(isset($data->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"); } $itemStack = self::deserializeItemStackFromNameMeta($data->name, $data->meta); if($itemStack === null){ //probably unknown item return null; } return $data->meta === RecipeIngredientData::WILDCARD_META_VALUE ? new MetaWildcardRecipeIngredient($data->name) : new ExactRecipeIngredient($itemStack); } public static function deserializeItemStack(ItemStackData $data) : ?Item{ //count, name, block_name, block_states, meta, nbt, can_place_on, can_destroy $name = $data->name; $meta = $data->meta ?? 0; $count = $data->count ?? 1; $blockStatesRaw = $data->block_states ?? null; $nbtRaw = $data->nbt ?? null; $canPlaceOn = $data->can_place_on ?? []; $canDestroy = $data->can_destroy ?? []; $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 = new BlockStateData($blockName, $blockStatesTag, BlockStateData::CURRENT_VERSION); }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(Utils::assumeNotFalse(file_get_contents($filePath), "Missing required resource file")); 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($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(); $ingredientDeserializerFunc = \Closure::fromCallable([self::class, "deserializeIngredient"]); $itemDeserializerFunc = \Closure::fromCallable([self::class, 'deserializeItemStack']); foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shapeless_crafting.json'), ShapelessRecipeData::class) as $recipe){ $recipeType = match($recipe->block){ "crafting_table" => ShapelessRecipeType::CRAFTING(), "stonecutter" => ShapelessRecipeType::STONECUTTER(), //TODO: Cartography Table default => null }; if($recipeType === null){ continue; } $inputs = []; foreach($recipe->input as $inputData){ $input = $ingredientDeserializerFunc($inputData); if($input === null){ //unknown input item continue 2; } $inputs[] = $input; } $outputs = []; foreach($recipe->output as $outputData){ $output = $itemDeserializerFunc($outputData); if($output === null){ //unknown output item continue 2; } $outputs[] = $output; } $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 = $ingredientDeserializerFunc($inputData); if($input === null){ //unknown input item continue 2; } $inputs[$symbol] = $input; } $outputs = []; foreach($recipe->output as $outputData){ $output = $itemDeserializerFunc($outputData); if($output === null){ //unknown output item continue 2; } $outputs[] = $output; } $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(), //TODO: 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; if(self::deserializeItemStackFromNameMeta($inputId, 0) === null || self::deserializeItemStackFromNameMeta($outputId, 0) === null){ //unknown item continue; } $result->registerPotionContainerChangeRecipe(new PotionContainerChangeRecipe( $inputId, $ingredient, $outputId )); } return $result; } }