import fuzzywuzzy.process import itertools import graphlib import zipfile import json 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 mapping[fuzzywuzzy.process.extractOne(response, keys)[0]] 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] if not os.path.exists('from-jar'): print('please enter the path to the .minecraft folder:') 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 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 += tag_lists[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) past_recipe_choices_for_items = {} def choose_recipe_for_item(item): if item in past_recipe_choices_for_items: return past_recipe_choices_for_items[item] if item not in recipe_lists: return None recipes = recipe_lists[item] choices = {'gather': None} for recipe in recipes: choices[f'{recipe[0]} from {join_with_and([f"{i[1]} {to_display_name(i[0])}" for i in recipe[1]])}'] = recipe recipe = ask_from_list('how would you like to obtain ' + to_display_name(item) + '?', choices) past_recipe_choices_for_items[item] = recipe 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 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 while True: print('what item would you like to obtain?') item = fuzzywuzzy.process.extractOne(input('> '), id_to_name_table)[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 if ask_from_list('add more items?', {'yes': True, 'no': False}): 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 steps.append(f'{recipe[0]} {recipe[3] * count} {to_display_name(recipe[2])} from {join_with_and([f"{c * count} {to_display_name(i)}" for i, c in recipe[1]])}') for i, c in recipe[1]: 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) past_recipe_choices_for_items.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)}')