Skip to content

Commit

Permalink
more recipe parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
PssbleTrngle committed Oct 14, 2023
1 parent bb6cdf6 commit 1206b9b
Show file tree
Hide file tree
Showing 41 changed files with 727 additions and 128 deletions.
49 changes: 48 additions & 1 deletion src/loader/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
import { IResolver } from '@pssbletrngle/pack-resolver'
import { Acceptor, IResolver } from '@pssbletrngle/pack-resolver'
import { Id } from '../common/id.js'
import { fromJson } from '../textHelper.js'
import Registry from '../common/registry.js'
import { RegistryProvider } from '../emit/index.js'
import { Logger } from '../logger.js'

export type AcceptorWithLoader = (logger: Logger, ...paramenters: Parameters<Acceptor>) => ReturnType<Acceptor>

export default interface Loader {
loadFrom(resolver: IResolver): Promise<void>
}

export abstract class JsonLoader<T> implements RegistryProvider<T> {
protected readonly registry = new Registry<T>()

protected abstract parse(logger: Logger, json: unknown, id: Id): T | null

forEach(consumer: (recipe: T, id: Id) => void): void {
this.registry.forEach(consumer)
}

private tryParseJson(logger: Logger, content: string) {
try {
return fromJson(content)
} catch (error) {
if (error instanceof SyntaxError) {
logger.warn(`unable to parse json: ${error.message}`, content)
return null
}
throw error
}
}

readonly accept: AcceptorWithLoader = (logger, path, content) => {
const match = /data\/(?<namespace>[\w-]+)\/\w+\/(?<rest>[\w-/]+).json/.exec(path)
if (!match?.groups) return false

const { namespace, rest } = match.groups
const id: Id = { namespace, path: rest }

const json = this.tryParseJson(logger.group(path), content.toString())
if (!json) return false

const parsed = this.parse(logger, json, id)
if (!parsed) return false

this.registry.set(id, parsed)

return true
}
}
42 changes: 6 additions & 36 deletions src/loader/loot.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,13 @@
import { Logger } from '../logger.js'
import { Acceptor } from '@pssbletrngle/pack-resolver'
import { encodeId, Id } from '../common/id.js'
import { fromJson } from '../textHelper.js'
import Registry from '../common/registry.js'
import { tryCatching } from '../error.js'
import { RegistryProvider } from '../emit/index.js'
import { LootTable, LootTableSchema } from '../schema/loot.js'
import { JsonLoader } from './index.js'
import { Logger } from '../logger.js'

export default class LootTableLoader implements RegistryProvider<LootTable> {
private readonly registry = new Registry<LootTable>()

constructor(private readonly logger: Logger) {}

private parse(raw: unknown, id: Id): LootTable | null {
const logger = this.logger.group(`error parsing loot table ${encodeId(id)}`)
return tryCatching(logger, () => {
return LootTableSchema.parse(raw)
export default class LootTableLoader extends JsonLoader<LootTable> {
protected parse(logger: Logger, json: unknown, id: Id): LootTable | null {
return tryCatching(logger.group(`error parsing loot table ${encodeId(id)}`), () => {
return LootTableSchema.parse(json)
})
}

readonly accept: Acceptor = (path, content) => {
// TODO extract common logic
const match = /data\/(?<namespace>[\w-]+)\/loot_tables\/(?<rest>[\w-/]+).json/.exec(path)
if (!match?.groups) return false

const { namespace, rest } = match.groups
const id: Id = { namespace, path: rest }

const json = fromJson(content.toString())

const parsed = this.parse(json, id)
if (!parsed) return false

this.registry.set(id, parsed)

return true
}

forEach(consumer: (recipe: LootTable, id: Id) => void): void {
this.registry.forEach(consumer)
}
}
39 changes: 31 additions & 8 deletions src/loader/pack.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Acceptor, IResolver } from '@pssbletrngle/pack-resolver'
import { Acceptor, IResolver, ResolverInfo } from '@pssbletrngle/pack-resolver'
import match from 'minimatch'
import { Logger } from '../logger.js'
import Loader from './index.js'
import RecipeLoader from './recipe.js'
import Loader, { AcceptorWithLoader } from './index.js'
import RecipeLoader, { RecipeLoaderAccessor } from './recipe.js'
import TagsLoader from './tags.js'
import RecipeEmitter, { RecipeRules } from '../emit/recipe.js'
import TagEmitter, { TagRules } from '../emit/tags.js'
Expand All @@ -13,9 +13,9 @@ import LootTableEmitter, { LootRules } from '../emit/loot.js'
export default class PackLoader implements Loader {
constructor(private readonly logger: Logger) {}

private readonly tagLoader = new TagsLoader(this.logger)
private readonly recipesLoader = new RecipeLoader(this.logger)
private readonly lootLoader = new LootTableLoader(this.logger)
private readonly tagLoader = new TagsLoader()
private readonly recipesLoader = new RecipeLoader()
private readonly lootLoader = new LootTableLoader()

private readonly tagEmitter = new TagEmitter(this.logger, this.tagLoader)
private readonly recipeEmitter = new RecipeEmitter(this.logger, this.recipesLoader, this.tagLoader)
Expand All @@ -41,23 +41,46 @@ export default class PackLoader implements Loader {
return this.lootEmitter
}

get recipeLoader(): RecipeLoaderAccessor {
return this.recipesLoader
}

resolveIngredientTest(test: IngredientTest) {
return this.recipeEmitter.resolveIngredientTest(test)
}

private acceptors: Record<string, Acceptor> = {
private acceptors: Record<string, AcceptorWithLoader> = {
'data/*/tags/**/*.json': this.tagLoader.accept,
'data/*/recipes/**/*.json': this.recipesLoader.accept,
'data/*/loot_tables/**/*.json': this.lootLoader.accept,
}

async loadFromMultiple(resolvers: ResolverInfo[]) {
await Promise.all(
resolvers.map(({ resolver, name }) => {
const logger = this.logger.group(name)
return resolver.extract((path, content) => {
const acceptor = Object.entries(this.acceptors).find(([pattern]) => match(path, pattern))?.[1]
if (!acceptor) return false
return acceptor(logger, path, content)
})
})
)

this.freeze()
}

async loadFrom(resolver: IResolver) {
await resolver.extract((path, content) => {
const acceptor = Object.entries(this.acceptors).find(([pattern]) => match(path, pattern))?.[1]
if (!acceptor) return false
return acceptor(path, content)
return acceptor(this.logger, path, content)
})

this.freeze()
}

private freeze() {
this.tagLoader.freeze()
}

Expand Down
153 changes: 111 additions & 42 deletions src/loader/recipe.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { Acceptor } from '@pssbletrngle/pack-resolver'
import { Logger } from '../logger.js'
import RecipeParser, { Recipe } from '../parser/recipe/index.js'
import ShapedParser from '../parser/recipe/vanilla/shaped.js'
import { RecipeDefinition } from '../schema/recipe.js'
import { Id } from '../common/id.js'
import { encodeId } from '../common/id.js'
import ShapelessParser from '../parser/recipe/vanilla/shapeless.js'
import CreateProcessingRecipeParser from '../parser/recipe/create/processing.js'
import { fromJson } from '../textHelper.js'
import SmeltingParser from '../parser/recipe/vanilla/smelting.js'
import SmithingParser from '../parser/recipe/vanilla/smithing.js'
import AssemblyRecipeParser from '../parser/recipe/create/assembly.js'
Expand All @@ -23,24 +20,37 @@ import BrewRecipeParser from '../parser/recipe/botania/brew.js'
import ManaInfusionRecipeParser from '../parser/recipe/botania/manaInfusion.js'
import GogWrapperRecipeParser from '../parser/recipe/botania/gogWrapper.js'
import ApothecaryRecipeParser from '../parser/recipe/botania/apothecary.js'
import Registry from '../common/registry.js'
import TerraPlateRecipeParser from '../parser/recipe/botania/terraPlate.js'
import PureDaisyRecipeParser from '../parser/recipe/botania/pureDaisy.js'
import { RegistryProvider } from '../emit/index.js'
import { JsonLoader } from './index.js'
import ForgeConditionalRecipeParser from '../parser/recipe/forge/conditional.js'
import HammeringRecipeParser from '../parser/recipe/adAstra/hammering.js'
import InputOutputRecipeParser from '../parser/recipe/adAstra/inputOutput.js'
import NasaWorkbenchRecipeParser from '../parser/recipe/adAstra/nasaWorkbench.js'
import SpaceStationRecipeParser from '../parser/recipe/adAstra/spaceStation.js'
import QuarkExclusionRecipeParser from '../parser/recipe/quark/exclusion.js'
import TreeExtractionRecipeParser from '../parser/recipe/thermal/treeExtraction.js'
import ThermalFuelRecipeParser from '../parser/recipe/thermal/fuel.js'
import RootComponentRecipeParser from '../parser/recipe/roots/component.js'
import RootRitualRecipeParser from '../parser/recipe/roots/ritual.js'
import { ShapelessRecipeParser } from '../parser/index.js'
import { Logger } from '../logger.js'

export default class RecipeLoader implements RegistryProvider<Recipe> {
export interface RecipeLoaderAccessor {
unknownRecipeTypes(): RecipeDefinition[]
registerParser(recipeType: string, parser: RecipeParser<RecipeDefinition, Recipe>): void
ignoreType(recipeType: string): void
}

export default class RecipeLoader extends JsonLoader<Recipe> implements RecipeLoaderAccessor {
private readonly recipeParsers = new Map<string, RecipeParser<RecipeDefinition, Recipe>>()

private readonly ignoredRecipeTypes = new Set<string>()
private readonly unknownRecipeTypes = new Set<string>()
private readonly _unknownRecipeTypes = new Map<string, RecipeDefinition>()

private readonly recipes = new Registry<Recipe>()
constructor() {
super()

forEach(consumer: (recipe: Recipe, id: Id) => void) {
this.recipes.forEach(consumer)
}

constructor(private readonly logger: Logger) {
this.registerParser('minecraft:crafting_shaped', new ShapedParser())
this.registerParser('minecraft:crafting_shapeless', new ShapelessParser())
this.registerParser('minecraft:smelting', new SmeltingParser())
Expand Down Expand Up @@ -68,6 +78,7 @@ export default class RecipeLoader implements RegistryProvider<Recipe> {

this.registerParser('farmersdelight:cooking', new CookingRecipeParser())
this.registerParser('farmersdelight:cutting', new CuttingRecipeParser())
this.registerParser('farmersrespite:brewing', new CookingRecipeParser())

this.registerParser('thermal:bottler', new ThermalRecipeParser())
this.registerParser('thermal:centrifuge', new ThermalRecipeParser())
Expand All @@ -87,6 +98,11 @@ export default class RecipeLoader implements RegistryProvider<Recipe> {
this.registerParser('thermal:smelter', new ThermalRecipeParser())
this.registerParser('thermal:smelter_recycle', new ThermalRecipeParser())
this.registerParser('thermal:smelter_catalyst', new ThermalCatalystRecipeParser())
this.registerParser('thermal:tree_extractor', new TreeExtractionRecipeParser())
this.registerParser('thermal:compression_fuel', new ThermalFuelRecipeParser())
this.registerParser('thermal:magmatic_fuel', new ThermalFuelRecipeParser())
this.registerParser('thermal:gourmand_fuel', new ThermalFuelRecipeParser())
this.registerParser('thermal:numismatic_fuel', new ThermalFuelRecipeParser())

this.registerParser('botania:nbt_output_wrapper', new NbtWrapperRecipeParser(this))
this.registerParser('botania:orechid', new OrechidRecipeParser())
Expand All @@ -108,32 +124,102 @@ export default class RecipeLoader implements RegistryProvider<Recipe> {
this.registerParser('botania:gog_alternation', new GogWrapperRecipeParser(this))
this.registerParser('botania:petal_apothecary', new ApothecaryRecipeParser())

this.ignoredRecipeTypes.add('immersiveengineering:cloche')
this.ignoredRecipeTypes.add('immersiveengineering:crusher')
this.ignoredRecipeTypes.add('immersiveengineering:fermenter')
this.ignoredRecipeTypes.add('immersiveengineering:metal_press')
this.ignoredRecipeTypes.add('immersiveengineering:squeezer')
this.registerParser('theoneprobe:probe_helmet', new ShapedParser())

this.registerParser('forge:conditional', new ForgeConditionalRecipeParser(this))

this.registerParser('sullysmod:grindstone_polishing', new SmeltingParser())

this.registerParser('ad_astra:hammering', new HammeringRecipeParser())
this.registerParser('ad_astra:cryo_fuel_conversion', new InputOutputRecipeParser())
this.registerParser('ad_astra:fuel_conversion', new InputOutputRecipeParser())
this.registerParser('ad_astra:oxygen_conversion', new InputOutputRecipeParser())
this.registerParser('ad_astra:compressing', new InputOutputRecipeParser())
this.registerParser('ad_astra:crafting_shaped_space_suit', new ShapedParser())
this.registerParser('ad_astra:nasa_workbench', new NasaWorkbenchRecipeParser())
this.registerParser('ad_astra:space_station', new SpaceStationRecipeParser())

this.registerParser('quark:exclusion', new QuarkExclusionRecipeParser(this))
this.registerParser('patchouli:shapeless_book_recipe', new ShapelessRecipeParser())

this.registerParser('cofh_core:crafting_shaped_potion', new ShapedParser())

this.registerParser('rootsclassic:component', new RootComponentRecipeParser())
this.registerParser('rootsclassic:ritual', new RootRitualRecipeParser())

this.ignoreType('jeed:effect_provider')
this.ignoreType('jeed:potion_provider')

this.ignoreType('pipez:copy_nbt')
this.ignoreType('pipez:clear_nbt')

this.ignoreType('refinedstorage:upgrade_with_enchanted_book')

this.ignoreType('forge:ore_shaped')

// TODO could to in the future
this.ignoreType('ad_astra:lunarian_trade_simple')
this.ignoreType('ad_astra:lunarian_trade_enchanted_item')
this.ignoreType('ad_astra:lunarian_trade_suspicious_stew')
this.ignoreType('ad_astra:lunarian_trade_enchanted_book')
this.ignoreType('ad_astra:lunarian_trade_dyed_item')
this.ignoreType('ad_astra:lunarian_trade_potioned_item')

this.ignoreType('supplementaries:trapped_present')
this.ignoreType('supplementaries:weathered_map')
this.ignoreType('supplementaries:soap_clearing')
this.ignoreType('supplementaries:rope_arrow_create')
this.ignoreType('supplementaries:present_dye')
this.ignoreType('supplementaries:rope_arrow_add')
this.ignoreType('supplementaries:item_lore')
this.ignoreType('supplementaries:flag_from_banner')
this.ignoreType('supplementaries:bubble_blower_charge')
this.ignoreType('supplementaries:bamboo_spikes_tipped')
this.ignoreType('supplementaries:antique_book')
this.ignoreType('supplementaries:cauldron_flag_dye')
this.ignoreType('supplementaries:cauldron_flag_clear')
this.ignoreType('supplementaries:cauldron_blackboard')

this.ignoreType('quark:mixed_exclusion')
this.ignoreType('quark:elytra_duplication')
this.ignoreType('quark:slab_to_block')

this.ignoreType('immersiveengineering:cloche')
this.ignoreType('immersiveengineering:crusher')
this.ignoreType('immersiveengineering:fermenter')
this.ignoreType('immersiveengineering:metal_press')
this.ignoreType('immersiveengineering:squeezer')
this.ignoreType('immersiveengineering:mineral_mix')
}

ignoreType(recipeType: string) {
this.ignoredRecipeTypes.add(recipeType)
}

unknownRecipeTypes() {
return [...this._unknownRecipeTypes.values()]
}

parse<TDefinition extends RecipeDefinition, TRecipe extends Recipe<TDefinition>>(
logger: Logger,
definition: TDefinition
): TRecipe | null {
const parser = this.recipeParsers.get(definition.type)
const parser = this.recipeParsers.get(encodeId(definition.type))

if (!('type' in definition)) return null
if (Object.keys(definition).length <= 1) return null
if (this.ignoredRecipeTypes.has(definition.type)) return null

if (!parser) {
if (!this.unknownRecipeTypes.has(definition.type)) {
this.logger.warn(`unknown recipe type: '${definition.type}'`, definition)
this.unknownRecipeTypes.add(definition.type)
if (!this._unknownRecipeTypes.has(definition.type)) {
logger.warn(`unknown recipe type: '${definition.type}'`, definition)
this._unknownRecipeTypes.set(definition.type, definition)
}
return null
}

try {
return parser.create(definition) as TRecipe
return parser.create(definition, logger) as TRecipe
} catch (e) {
throw new Error(`Failed to parse recipe with type '${definition.type}'`, { cause: e })
}
Expand All @@ -143,24 +229,7 @@ export default class RecipeLoader implements RegistryProvider<Recipe> {
this.recipeParsers.set(recipeType, parser)
}

readonly accept: Acceptor = (path, content) => {
const match = /data\/(?<namespace>[\w-]+)\/recipes\/(?<rest>[\w-/]+).json/.exec(path)
if (!match?.groups) return false

const { namespace, rest } = match.groups
const id: Id = { namespace, path: rest }

const parsed: RecipeDefinition = fromJson(content.toString())

const recipe = this.parse(parsed)
if (!recipe) return false

this.recipes.set(id, recipe)

return true
}

clear() {
this.unknownRecipeTypes.clear()
this._unknownRecipeTypes.clear()
}
}
Loading

0 comments on commit 1206b9b

Please sign in to comment.