import thefuzz.process import thefuzz.fuzz import itertools import graphlib import zipfile import json import sys import os def ask_from_list(prompt, mapping): keys = list(mapping.keys()) keys.sort() max_len = len(str(len(keys))) print(prompt) for i, x in enumerate(keys): print(" " * (max_len - len(str(i + 1))) + f' ({i + 1}) {x}') response = input('> ') try: i = int(response) if i >= 1 and i <= len(mapping): return mapping[keys[i - 1]] except ValueError: pass return ask_from_list(prompt, mapping) def join_with_and(list): if len(list) == 1: return list[0] if len(list) == 2: return list[0] + ' and ' + list[1] return ', '.join(list[:-1]) + ', and ' + list[-1] def guess_minecraft_directory(): if sys.platform.startswith('linux'): return os.path.join(os.path.expanduser('~'), '.minecraft') if sys.platform in ['win32', 'cygwin']: return os.path.join(os.getenv('APPDATA'), '.minecraft') return '.' if not os.path.exists('from-jar'): versions_dir = os.path.join(guess_minecraft_directory(), 'versions') while not os.path.exists(versions_dir): print('please enter the path to the .minecraft directory:') versions_dir = os.path.join(input('> '), 'versions') versions = {} for dir in os.walk(versions_dir): for file in dir[2]: if file.endswith('.jar'): versions[file.removesuffix('.jar')] = os.path.join(dir[0], file) jar_path = ask_from_list('which version should we extract?', versions) with zipfile.ZipFile(jar_path, 'r') as jar: print('extracting files from jar...') interesting_dirs = [ os.path.join('data', 'minecraft', 'recipes'), os.path.join('data', 'minecraft', 'tags', 'items'), os.path.join('assets', 'minecraft', 'lang') ] for file in jar.namelist(): for dir in interesting_dirs: if file.startswith(dir): jar.extract(file, 'from-jar') print('loading data...') id_to_name_table = {} name_to_id_table = {} tag_lists = {} # recipe is (method, [(input, count)], output, count) recipe_lists = {} with open(os.path.join('from-jar', 'assets', 'minecraft', 'lang', 'en_us.json')) as file: data = json.load(file) for key in data: id = None if key.startswith('block.minecraft.'): id = 'minecraft:' + key.removeprefix('block.minecraft.') if '.' in id: continue elif key.startswith('item.minecraft.'): id = 'minecraft:' + key.removeprefix('item.minecraft.') if '.' in id: continue else: continue name = data[key].lower() name_to_id_table[name] = id id_to_name_table[id] = name def to_display_name(id): return id_to_name_table[id] if id in id_to_name_table else '<' + id + '>' tags_dir = os.path.join('from-jar', 'data', 'minecraft', 'tags', 'items') for fname in os.listdir(tags_dir): with open(os.path.join(tags_dir, fname)) as file: data = json.load(file) tag_lists['minecraft:' + fname.removesuffix('.json')] = data['values'] def add_recipe(recipe): output = recipe[2] if output in recipe_lists: if recipe not in recipe_lists[output]: recipe_lists[output].append(recipe) else: recipe_lists[output] = [recipe] def recursive_tag_lookup(tag): list = [] for id in tag_lists[tag]: if id[0] == '#': list += recursive_tag_lookup(id[1:]) else: list.append(id) return list def ilist_product(ilist): ilist_new = [] for il in ilist: il_new = [] if not isinstance(il, list): il = [il] for i in il: if 'tag' in i: il_new.append('#' + i['tag']) else: il_new.append(i['item']) ilist_new.append(il_new) return itertools.product(*ilist_new) def add_shaped_recipe(recipe): output = recipe['result']['item'] count = recipe['result']['count'] if 'count' in recipe['result'] else 1 the_keys = list(recipe['key'].keys()) for choice in ilist_product([recipe['key'][key] for key in the_keys]): map = {} for i in range(len(the_keys)): map[the_keys[i]] = choice[i] counts = {} for p in ''.join(recipe['pattern']): if p != ' ': item = map[p] if item in counts: counts[item] += 1 else: counts[item] = 1 items = list(counts.keys()) items.sort() add_recipe(('craft', [(item, counts[item]) for item in items], output, count)) def add_shapeless_recipe(recipe): output = recipe['result']['item'] count = recipe['result']['count'] if 'count' in recipe['result'] else 1 for choice in ilist_product(recipe['ingredients']): counts = {} for item in choice: if item in counts: counts[item] += 1 else: counts[item] = 1 items = list(counts.keys()) items.sort() add_recipe(('craft', [(item, counts[item]) for item in items], output, count)) def add_stonecut_recipe(recipe): output = recipe['result'] count = recipe['count'] for choice in ilist_product([recipe['ingredient']]): add_recipe(('stonecut', [(choice[0], 1)], output, count)) def add_smelt_recipe(recipe): output = recipe['result'] count = 1 for choice in ilist_product([recipe['ingredient']]): add_recipe(('smelt', [(choice[0], 1)], output, count)) smelting_methods = ['minecraft:smelting', 'minecraft:campfire_cooking', 'minecraft:smoking', 'minecraft:blasting'] recipes_dir = os.path.join('from-jar', 'data', 'minecraft', 'recipes') for fname in os.listdir(recipes_dir): with open(os.path.join(recipes_dir, fname)) as file: data = json.load(file) if data['type'] == 'minecraft:crafting_shaped': add_shaped_recipe(data) elif data['type'] == 'minecraft:crafting_shapeless': add_shapeless_recipe(data) elif data['type'] == 'minecraft:stonecutting': add_stonecut_recipe(data) elif data['type'] in smelting_methods: add_smelt_recipe(data) elif data['type'].startswith('minecraft:crafting_special_') or data['type'] in ['minecraft:smithing_trim', 'minecraft:crafting_decorated_pot', 'minecraft:smithing_transform']: pass else: print('unrecognized type ' + data['type']) exit(1) chosen_tags = {} chosen_recipes = {} def choose_recipe_for_item(dest_item): if dest_item in chosen_recipes: return chosen_recipes[dest_item] if dest_item not in recipe_lists: return None recipes = recipe_lists[dest_item] choices = {'gather': (None, {})} for recipe in recipes: tags = [] for item, _ in recipe[1]: if item[0] == '#': if item[1:] not in chosen_tags: tags.append(item[1:]) for tag_choice in itertools.product(*[recursive_tag_lookup(tag) for tag in tags]): tag_map = {} for i in range(len(tags)): tag_map[tags[i]] = tag_choice[i] ingredients = [] for item, count in recipe[1]: if item[0] != '#': ingredients.append((item, count)) else: tag = item[1:] if tag in chosen_tags: ingredients.append((chosen_tags[tag], count)) else: ingredients.append((tag_map[tag], count)) ingredients.sort() source = join_with_and([f'{count} {to_display_name(item)}' for item, count in ingredients]) desc = f'{recipe[0]} {source} into {recipe[3]} {to_display_name(recipe[2])}' choices[desc] = (recipe, tag_map) recipe, tag_map = ask_from_list('how would you like to obtain ' + to_display_name(dest_item) + '?', choices) chosen_recipes[dest_item] = recipe for tag in tag_map: chosen_tags[tag] = tag_map[tag] return recipe # dependees item_deps_added = [] # dependency -> dependee item_deps = {} items_needed = {} def add_item_deps(item): if item not in item_deps_added: item_deps_added.append(item) if item not in item_deps: item_deps[item] = [] recipe = choose_recipe_for_item(item) if recipe is not None: for dep, _ in recipe[1]: if dep[0] == '#': dep = chosen_tags[dep[1:]] if dep in item_deps: item_deps[dep].append(item) else: item_deps[dep] = [item] add_item_deps(dep) def add_needed_item(item, count): if item in items_needed: items_needed[item] += count else: items_needed[item] = count def item_ratio(x, y): ratio = thefuzz.fuzz.ratio(x, y) partial_ratio = thefuzz.fuzz.partial_ratio(x, y) return (ratio + partial_ratio) // 2 while True: print('what item would you like to obtain? (press enter to stop)') the_input = input('> ') if the_input == '': break good = thefuzz.process.extract(the_input, id_to_name_table, scorer=item_ratio, limit=8) item = None if good[0][1] != 100: print('there is no item with that name') mapping = {'(none of these)': None} cutoff = good[0][1] - 10 for name, ratio, id in good: if ratio >= cutoff: mapping[name] = id if len(mapping) == 1: continue item = ask_from_list('do you mean one of these?', mapping) if item is None: continue else: item = good[0][2] while True: print('how many ' + to_display_name(item) + '?') s = input('> ') try: i = int(s) if i > 0: add_needed_item(item, i) except ValueError: continue break while True: for item in items_needed: add_item_deps(item) original_needed = items_needed.copy() steps = [] gather_steps = [] ts = graphlib.TopologicalSorter(item_deps) try: for item in ts.static_order(): if item in items_needed: needed = items_needed[item] if needed == 0: continue recipe = choose_recipe_for_item(item) if recipe is None: gather_steps.append(f'gather {needed} {to_display_name(item)}') add_needed_item(item, -needed) else: count = (needed - 1) // recipe[3] + 1 ingredients = [] for ingredient, c in recipe[1]: if ingredient[0] == '#': ingredients.append((chosen_tags[ingredient[1:]], c)) else: ingredients.append((ingredient, c)) source = join_with_and([f'{c * count} {to_display_name(i)}' for i, c in ingredients]) steps.append(f'{recipe[0]} {source} into {recipe[3] * count} {to_display_name(recipe[2])}') for i, c in ingredients: add_needed_item(i, c * count) add_needed_item(item, -recipe[3] * count) except graphlib.CycleError as ex: print('cycle detected: ' + ' from '.join(to_display_name(id) for id in ex.args[1])) mapping = {} for id in ex.args[1][1:]: mapping[to_display_name(id)] = id id = ask_from_list('change recipe for which item?', mapping) chosen_recipes.pop(id) choose_recipe_for_item(id) items_needed = original_needed item_deps_added = [] item_deps = {} continue break steps.reverse() print('steps:') for step in gather_steps: print(' ' + step) for step in steps: print(' ' + step) requested = list(original_needed.keys()) requested.sort() print('you should now have the requested items:') for item in requested: print(f' {original_needed[item]} {to_display_name(item)}') extra = {} for item in items_needed: if items_needed[item] != 0: extra[item] = -items_needed[item] if len(extra) != 0: items = list(extra.keys()) items.sort() print('plus these extras:') for i in items: print(f' {extra[i]} {to_display_name(i)}')