diff --git a/CLI.py b/CLI.py index 5b8d25f9..96d3be21 100644 --- a/CLI.py +++ b/CLI.py @@ -26,7 +26,7 @@ def parse_arguments(argv, no_defaults=False): # get settings settings = get_settings() - fish = BabelFish() + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") # we need to know how many players we have first parser = argparse.ArgumentParser(add_help=False) @@ -102,6 +102,7 @@ def parse_arguments(argv, no_defaults=False): def get_settings(): # set default settings settings = { + "lang": "en", "retro": False, "mode": "open", "logic": "noglitches", diff --git a/DoorShuffle.py b/DoorShuffle.py index 8bd93efc..7b084fa4 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -5,6 +5,9 @@ import operator as op import time from enum import unique, Flag +from source.classes.BabelFish import BabelFish +import CLI as cli + from functools import reduce from BaseClasses import RegionType, Door, DoorType, Direction, Sector, CrystalBarrier from Regions import key_only_locations @@ -305,6 +308,9 @@ def pair_existing_key_doors(world, player, door_a, door_b): # paired_door.pair = False def within_dungeon(world, player): + settings = cli.get_settings() + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + fix_big_key_doors_with_ugly_smalls(world, player) overworld_prep(world, player) entrances_map, potentials, connections = determine_entrance_list(world, player) @@ -326,7 +332,7 @@ def within_dungeon(world, player): start = time.process_time() for builder in world.dungeon_layouts[player].values(): shuffle_key_doors(builder, world, player) - logging.getLogger('').info('Key door shuffle time: %s', time.process_time()-start) + logging.getLogger('').info('%s: %s', fish.translate("cli","cli","keydoor.shuffle.time"), time.process_time()-start) smooth_door_pairs(world, player) @@ -347,6 +353,11 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player): + settings = cli.get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + entrances_map, potentials, connections = connections_tuple enabled_entrances = {} sector_queue = deque(dungeon_builders.values()) @@ -366,7 +377,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ sector_queue.append(builder) last_key = builder.name else: - logging.getLogger('').info('Generating dungeon: %s', builder.name) + logging.getLogger('').info('%s: %s', fish.translate("cli","cli","generating.dungeon"), builder.name) ds = generate_dungeon(builder, origin_list_sans_drops, split_dungeon, world, player) find_new_entrances(ds, connections, potentials, enabled_entrances, world, player) ds.name = name @@ -501,7 +512,7 @@ def shuffle_dungeon(world, player, start_region_names, dungeon_region_names): for door in get_doors(world, world.get_region(name, player), player): ugly_regions[door.name] = 0 available_doors.append(door) - + # Loop until all available doors are used while len(available_doors) > 0: # Pick a random available door to connect, prioritizing ones that aren't blocked. @@ -713,6 +724,9 @@ def cross_dungeon(world, player): def assign_cross_keys(dungeon_builders, world, player): + settings = cli.get_settings() + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + start = time.process_time() total_keys = remaining = 29 total_candidates = 0 @@ -792,7 +806,7 @@ def assign_cross_keys(dungeon_builders, world, player): dungeon.small_keys = [] else: dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys - logging.getLogger('').info('Cross Dungeon: Key door shuffle time: %s', time.process_time()-start) + logging.getLogger('').info('%s: %s', fish.translate("cli","cli","keydoor.shuffle.time.crossed"), time.process_time()-start) def reassign_boss(boss_region, boss_key, builder, gt, world, player): @@ -948,15 +962,18 @@ def calc_used_dungeon_items(builder): def find_valid_combination(builder, start_regions, world, player, drop_keys=True): + settings = cli.get_settings() + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + logger = logging.getLogger('') - logger.info('Shuffling Key doors for %s', builder.name) + logger.info('%s %s', fish.translate("cli","cli","shuffling.keydoors"), builder.name) # find valid combination of candidates if len(builder.candidates) < builder.key_doors_num: if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) return False builder.key_doors_num = len(builder.candidates) # reduce number of key doors - logger.info('Lowering key door count because not enough candidates: %s', builder.name) + logger.info('%s: %s', fish.translate("cli","cli","lowering.keys.candidates"), builder.name) combinations = ncr(len(builder.candidates), builder.key_doors_num) itr = 0 start = time.process_time() @@ -976,7 +993,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) return False - logger.info('Lowering key door count because no valid layouts: %s', builder.name) + logger.info('%s: %s', fish.translate("cli","cli","lowering.keys.layouts"), builder.name) builder.key_doors_num -= 1 if builder.key_doors_num < 0: raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5dd74be4..25f15e36 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -8,6 +8,9 @@ from functools import reduce import operator as op from typing import List +from source.classes.BabelFish import BabelFish +import CLI as cli + from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, Sector, PolSlot, flooded_keys from Regions import key_only_locations, dungeon_events, flooded_keys_reverse from Dungeons import dungeon_regions @@ -1094,6 +1097,11 @@ def simple_dungeon_builder(name, sector_list): def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None): + settings = cli.get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + logger = logging.getLogger('') logger.info('Shuffling Dungeon Sectors') if dungeon_entrances is None: @@ -1149,7 +1157,7 @@ def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None): # polarity: if not global_pole.is_valid(dungeon_map): raise NeutralizingException('Either free location/crystal assignment is already globally invalid - lazy dev check this earlier!') - logger.info('-Balancing Doors') + logger.info(fish.translate("cli","cli","balance.doors")) assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) # the rest assign_the_rest(dungeon_map, neutral_sectors, global_pole) @@ -1435,8 +1443,13 @@ def sum_polarity(sector_list): def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger): + settings = cli.get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + # step 1: fix polarity connection issues - logger.info('--Basic Traversal') + logger.info(fish.translate("cli","cli","basic.traversal")) unconnected_builders = identify_polarity_issues(dungeon_map) while len(unconnected_builders) > 0: for name, builder in unconnected_builders.items(): @@ -1524,10 +1537,15 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger): + settings = cli.get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + builder_order = list(dungeon_map.values()) random.shuffle(builder_order) for builder in builder_order: - logger.info('--Balancing %s', builder.name) + logger.info('%s %s', fish.translate("cli","cli","balancing"), builder.name) while not builder.polarity().is_neutral(): candidates = find_neutralizing_candidates(builder, polarized_sectors) valid, sectors = False, None @@ -1832,8 +1850,13 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole): def split_dungeon_builder(builder, split_list): + settings = cli.get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + logger = logging.getLogger('') - logger.info('Splitting Up Desert/Skull') + logger.info(fish.translate("cli","cli","splitting.up") + ' ' + 'Desert/Skull') candidate_sectors = dict.fromkeys(builder.sectors) global_pole = GlobalPolarity(candidate_sectors) @@ -1848,6 +1871,11 @@ def split_dungeon_builder(builder, split_list): def balance_split(candidate_sectors, dungeon_map, global_pole): + settings = cli.get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + logger = logging.getLogger('') # categorize sectors crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors) @@ -1860,7 +1888,7 @@ def balance_split(candidate_sectors, dungeon_map, global_pole): # blue barriers assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole) # polarity: - logger.info('-Re-balancing ' + next(iter(dungeon_map.keys())) + ' et al') + logger.info(fish.translate("cli","cli","re-balancing") + ' ' + next(iter(dungeon_map.keys())) + ' et al') assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) # the rest assign_the_rest(dungeon_map, neutral_sectors, global_pole) diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index 64a0e2d7..a66a7238 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -8,7 +8,9 @@ import textwrap import shlex import sys -from CLI import parse_arguments +from source.classes.BabelFish import BabelFish + +from CLI import parse_arguments, get_args_priority from Main import main from Rom import get_sprite_from_name from Utils import is_bundled, close_console @@ -17,6 +19,11 @@ from Fill import FillError def start(): args = parse_arguments(None) + settings = get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + if is_bundled() and len(sys.argv) == 1: # for the bundled builds, if we have no arguments, the user # probably wants the gui. Users of the bundled build who want the command line @@ -52,10 +59,10 @@ def start(): for _ in range(args.count): try: main(seed=seed, args=args) - logger.info('Finished run %s', _+1) + logger.info('%s %s', fish.translate("cli","cli","finished.run"), _+1) except (FillError, Exception, RuntimeError) as err: failures.append((err, seed)) - logger.warning('Generation failed: %s', err) + logger.warning('%s: %s', fish.translate("cli","cli","generation.failed"), err) seed = random.randint(0, 999999999) for fail in failures: logger.info('%s seed failed with: %s', fail[1], fail[0]) diff --git a/Main.py b/Main.py index 315ba5f5..33ca7548 100644 --- a/Main.py +++ b/Main.py @@ -8,6 +8,9 @@ import random import time import zlib +from source.classes.BabelFish import BabelFish +import CLI as cli + from BaseClasses import World, CollectionState, Item, Region, Location, Shop from Items import ItemFactory from KeyDoorShuffle import validate_key_placement @@ -34,6 +37,11 @@ def main(args, seed=None): start = time.perf_counter() + settings = cli.get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + # initialize the world world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) logger = logging.getLogger('') @@ -62,7 +70,14 @@ def main(args, seed=None): world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} - logger.info('ALttP Door Randomizer Version %s - Seed: %s\n', __version__, world.seed) + logger.info( + '%s %s %s - %s: %s\n', + fish.translate("cli","cli","app.title"), + fish.translate("cli","cli","version"), + __version__, + fish.translate("cli","cli","seed"), + world.seed + ) parsed_names = parse_player_names(args.names, world.players, args.teams) world.teams = len(parsed_names) @@ -95,7 +110,7 @@ def main(args, seed=None): create_rooms(world, player) create_dungeons(world, player) - logger.info('Shuffling the World about.') + logger.info(fish.translate("cli","cli","shuffling.world")) for player in range(1, world.players + 1): if world.mode[player] != 'inverted': @@ -103,7 +118,7 @@ def main(args, seed=None): else: link_inverted_entrances(world, player) - logger.info('Shuffling dungeons') + logger.info(fish.translate("cli","cli","shuffling.dungeons")) for player in range(1, world.players + 1): link_doors(world, player) @@ -111,21 +126,21 @@ def main(args, seed=None): mark_light_world_regions(world, player) else: mark_dark_world_regions(world, player) - logger.info('Generating Item Pool.') + logger.info(fish.translate("cli","cli","generating.itempool")) for player in range(1, world.players + 1): generate_itempool(world, player) - logger.info('Calculating Access Rules.') + logger.info(fish.translate("cli","cli","calc.access.rules")) for player in range(1, world.players + 1): set_rules(world, player) - logger.info('Placing Dungeon Prizes.') + logger.info(fish.translate("cli","cli","placing.dungeon.prizes")) fill_prizes(world) - logger.info('Placing Dungeon Items.') + logger.info(fish.translate("cli","cli","placing.dungeon.items")) shuffled_locations = None if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + @@ -139,9 +154,17 @@ def main(args, seed=None): for player in range(1, world.players+1): for key_layout in world.key_layout[player].values(): if not validate_key_placement(key_layout, world, player): - raise RuntimeError("Keylock detected: %s (Player %d)" % (key_layout.sector.name, player)) + raise RuntimeError( + "%s: %s (%s %d)" % + ( + fish.translate("cli","cli","keylock.detected"), + key_layout.sector.name, + fish.translate("cli","cli","player"), + player + ) + ) - logger.info('Fill the world.') + logger.info(fish.translate("cli","cli","fill.world")) if args.algorithm == 'flood': flood_items(world) # different algo, biased towards early game progress items @@ -160,14 +183,14 @@ def main(args, seed=None): distribute_items_restrictive(world, True) if world.players > 1: - logger.info('Balancing multiworld progression.') + logger.info(fish.translate("cli","cli","balance.multiworld")) balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom if not world.can_beat_game(): - raise RuntimeError('Cannot beat game. Something went terribly wrong here!') + raise RuntimeError(fish.translate("cli","cli","cannot.beat.game")) - logger.info('Patching ROM.') + logger.info(fish.translate("cli","cli","patching.rom")) outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed) @@ -191,8 +214,8 @@ def main(args, seed=None): if not args.jsonout: rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000) else: - logging.warning("EnemizerCLI not found at:" + args.enemizercli) - logging.warning("No Enemizer options will be applied until this is resolved.") + logging.warning(fish.translate("cli","cli","enemizer.not.found") + ': ' + args.enemizercli) + logging.warning(fish.translate("cli","cli","enemizer.nothing.applied")) if args.race: patch_race_rom(rom) @@ -246,7 +269,7 @@ def main(args, seed=None): world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) if not args.skip_playthrough: - logger.info('Calculating playthrough.') + logger.info(fish.translate("cli","cli","calc.playthrough")) create_playthrough(world) if args.jsonout: @@ -254,8 +277,8 @@ def main(args, seed=None): elif args.create_spoiler and not args.skip_playthrough: world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) - logger.info('Done. Enjoy.') - logger.info('Total Time: %s', time.perf_counter() - start) + logger.info(fish.translate("cli","cli","done")) + logger.info('%s: %s', fish.translate("cli","cli","total.time"), time.perf_counter() - start) # print_wiki_doors_by_room(dungeon_regions,world,1) @@ -391,6 +414,11 @@ def copy_dynamic_regions_and_locations(world, ret): def create_playthrough(world): + settings = cli.get_args_priority(None, None, None) + if "load" in settings: + settings = settings["load"] + fish = BabelFish(lang=settings["lang"] if "lang" in settings else "en") + # create a copy as we will modify it old_world = world world = copy_world(world) @@ -401,7 +429,7 @@ def create_playthrough(world): collection_spheres = [] state = CollectionState(world) sphere_candidates = list(prog_locations) - logging.getLogger('').debug('Building up collection spheres.') + logging.getLogger('').debug(fish.translate("cli","cli","building.collection.spheres")) while sphere_candidates: state.sweep_for_events(key_only=True) state.sweep_for_crystal_access() @@ -424,7 +452,7 @@ def create_playthrough(world): if not sphere: logging.getLogger('').debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]): - raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.') + raise RuntimeError(fish.translate("cli","cli","cannot.reach.progression")) else: old_world.spoiler.unreachables = sphere_candidates.copy() break @@ -478,7 +506,7 @@ def create_playthrough(world): logging.getLogger('').debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) if not sphere: - raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') + raise RuntimeError(fish.translate("cli","cli","cannot.reach.required")) # store the required locations for statistical analysis old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere] diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index aaaff7ae..fb54383d 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -1,4 +1,5 @@ { + "lang": {}, "create_spoiler": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 2b1df5c9..90b641d7 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -1,5 +1,46 @@ { + "cli": { + "app.title": "ALttP Door Randomizer", + "version": "Version", + "seed": "Seed", + "player": "Player", + "shuffling.world": "Shuffling the World about", + "shuffling.dungeons": "Shuffling dungeons", + "basic.traversal": "--Basic Traversal", + "generating.dungeon": "Generating dungeon", + "shuffling.keydoors": "Shuffling Key doors for", + "lowering.keys.candidates": "Lowering key door count because not enough candidates", + "lowering.keys.layouts": "Lowering key door count because no valid layouts", + "keydoor.shuffle.time": "Key door shuffle time", + "keydoor.shuffle.time.crossed": "Cross Dungeon: Key door shuffle time", + "generating.itempool": "Generating Item Pool", + "calc.access.rules": "Calculating Access Rules", + "placing.dungeon.prizes": "Placing Dungeon Prizes", + "placing.dungeon.items": "Placing Dungeon Items", + "keylock.detected": "Keylock detected", + "fill.world": "Fill the world", + "balance.doors": "-Balancing Doors", + "re-balancing": "-Re-balancing", + "balancing": "--Balancing", + "splitting.up": "Splitting Up", + "balance.multiworld": "Balancing multiworld progression", + "cannot.beat.game": "Cannot beat game! Something went terribly wrong here!", + "cannot.reach.progression": "Not all progression items reachable. Something went terribly wrong here.", + "cannot.reach.required": "Not all required items reachable. Something went terribly wrong here.", + "patching.rom": "Patching ROM", + "calc.playthrough": "Calculating playthrough", + "done": "Done. Enjoy.", + "total.time": "Total Time", + "finished.run": "Finished run", + "generation.failed": "Generation failed", + "generation.fail.rate": "Generation fail rate", + "generation.success.rate": "Generation success rate", + "enemizer.not.found": "Enemizer not found at", + "enemizer.nothing.applied": "No Enemizer options will be applied until this is resolved.", + "building.collection.spheres": "Building up collection spheres" + }, "help": { + "lang": [ "App Language, if available, defaults to English" ], "create_spoiler": [ "Output a Spoiler File" ], "logic": [ "Select Enforcement of Item Requirements. (default: %(default)s)", diff --git a/resources/app/cli/lang/es.json b/resources/app/cli/lang/es.json new file mode 100644 index 00000000..fe9f3173 --- /dev/null +++ b/resources/app/cli/lang/es.json @@ -0,0 +1,25 @@ +{ + "cli": { + "app.title": "ALttP Puerta Aleatorizador", + "version": "Versión", + "seed": "Número", + "player": "Player", + "shuffling.world": "Barajando el Mundo", + "shuffling.dungeons": "Barajando Mazmorras", + "balance.doors": "-Equilibriando Puertas", + "re-balancing": "-Reequilibriando", + "balancing": "--Equilibriando", + "splitting.up": "División", + "basic.traversal": "--Recorrido Básico", + "generating.dungeon": "Generando mazmorra", + "shuffling.keydoors": "Barajando Puertas Clave para", + "placing.dungeon.prizes": "Placing Dungeon Prizes", + "placing.dungeon.items": "Placing Dungeon Items", + "keylock.detected": "Bloqueo de Teclas detectado", + "fill.world": "Llenar el Mundo", + "patching.rom": "Parchear ROM", + "calc.playthrough": "Cálculo de Juego", + "generation.failed": "Generación Fallida", + "enemizer.not.found": "Enemizer no encontrado en" + } +} \ No newline at end of file diff --git a/source/classes/BabelFish.py b/source/classes/BabelFish.py index d2b52765..4a2897d0 100644 --- a/source/classes/BabelFish.py +++ b/source/classes/BabelFish.py @@ -34,7 +34,8 @@ class BabelFish(): with open(langs_filename,encoding="utf-8") as f: #open it self.lang_defns[lang][key[:key.rfind(os.sep)].replace(os.sep,'.')] = json.load(f) #save translation definitions else: - print(langs_filename + " not found for translation!") + pass +# print(langs_filename + " not found for translation!") def translate(self, domain="", key="", subkey=""): #three levels of keys # start with nothing