diff -ruN a/retux.py b/retux.py --- a/retux.py +++ b/retux.py @@ -19,6 +19,8 @@ __version__ = "1.6.2" +import pygame +import asyncio import argparse import datetime import gettext @@ -43,25 +45,23 @@ import xsge_physics import xsge_tiled -try: - from tkinter import Tk - import tkinter.filedialog as tkinter_filedialog -except ImportError: - HAVE_TK = False -else: - HAVE_TK = True +# No file dialogs in a browser: importing levelsets is left out. +HAVE_TK = False if getattr(sys, "frozen", False): __file__ = sys.executable -DATA = tempfile.mkdtemp("retux-data") CONFIG = os.path.join( os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")), "retux") LOCAL = os.path.join( os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")), "retux") +if os.path.isdir("/saves"): + # On the web the page keeps /saves in the browser (IndexedDB). + CONFIG = "/saves/retux/config" + LOCAL = "/saves/retux/local" dirs = [os.path.join(os.path.dirname(__file__), "data"), os.path.join(LOCAL, "data")] @@ -102,7 +102,7 @@ "--no-hud", help=_("Don't show the player's heads-up display."), action="store_true") parser.add_argument("--god", help=_("Pray to your god. Choose wisely…")) -args = parser.parse_args() +args = parser.parse_args(["-p"]) PRINT_ERRORS = args.print_errors DELTA = not args.nodelta @@ -120,19 +120,9 @@ elif args.god.lower() in {"emacs", "vi", "vim", "ed"}: HELL = True -for d in dirs: - if os.path.isdir(d): - for dirpath, dirnames, filenames in os.walk(d, True, None, True): - dirtail = os.path.relpath(dirpath, d) - nd = os.path.join(DATA, dirtail) - - for dirname in dirnames: - dp = os.path.join(nd, dirname) - if not os.path.exists(dp): - os.makedirs(dp) - - for fname in filenames: - shutil.copy2(os.path.join(dirpath, fname), nd) +# The data is used where it is (the original copied it, together with the +# user's levelsets, to a temporary folder). +DATA = dirs[0] del dirs gettext.install("retux", os.path.abspath(os.path.join(DATA, "locale"))) @@ -375,7 +365,8 @@ up_key = [["up", "w"]] down_key = [["down", "s"]] jump_key = [["space"]] -action_key = [["ctrl_left", "ctrl_right"]] +# Not Ctrl: browsers take Ctrl+W, Ctrl+S, Ctrl+D... for themselves. +action_key = [["x"]] sneak_key = [["shift_left", "shift_right"]] menu_key = [["tab", "backspace"]] pause_key = [["enter", "p"]] @@ -458,11 +449,10 @@ self.show_hell() def event_close(self): - rush_save() - self.end() + pass def event_paused_close(self): - self.event_close() + pass class Level(sge.dsp.Room): @@ -6494,7 +6484,7 @@ class MainMenu(Menu): items = [_("New Game"), _("Load Game"), _("Select Levelset"), _("Options"), - _("Credits"), _("Quit")] + _("Credits")] def event_choose(self): if self.choice == 0: @@ -6513,8 +6503,6 @@ credits_room = CreditsScreen.load(os.path.join("special", "credits.json")) credits_room.start() - else: - sge.game.end() class NewGameMenu(Menu): @@ -6683,7 +6671,6 @@ def create_page(cls, default=0): smt = scale_method if scale_method else "fastest" cls.items = [ - _("Fullscreen: {}").format(_("On") if fullscreen else _("Off")), _("Scale Method: {}").format(smt), _("Scale Proportional: {}").format( _("On") if scale_proportional else _("Off")), @@ -6693,8 +6680,11 @@ _("Show FPS: {}").format(_("On") if fps_enabled else _("Off")), _("Joystick Threshold: {}%").format(int(joystick_threshold * 100)), _("Configure keyboard"), _("Configure joysticks"), - _("Detect joysticks"), _("Import levelset"), _("Back")] - return cls.create(default) + _("Detect joysticks"), _("Back")] + # The list lost its first entry (fullscreen) and "Import levelset": + # the numbers used by event_choose still count the first one, so the + # default shifts by one. + return cls.create(max(default - 1, 0)) def event_press_left(self): # Variant of event_press_enter which passes True to the custom @@ -6721,12 +6711,9 @@ global fps_enabled global joystick_threshold - if self.choice == 0: - play_sound(select_sound) - fullscreen = not fullscreen - sge.game.fullscreen = fullscreen - OptionsMenu.create_page(default=self.choice) - elif self.choice == 1: + if self.choice is not None: + self.choice += 1 # see create_page + if self.choice == 1: choices = [None, "noblur", "smooth"] + sge.SCALE_METHODS if scale_method in choices: i = choices.index(scale_method) @@ -6803,69 +6790,6 @@ sge.joystick.refresh() play_sound(heal_sound) OptionsMenu.create_page(default=self.choice) - elif self.choice == 11: - if HAVE_TK: - play_sound(confirm_sound) - fnames = tkinter_filedialog.askopenfilenames( - filetypes=[(_("ReTux levelset files"), ".rtz"), - (_("all files"), ".*")]) - for fname in fnames: - w = 400 - h = 128 - margin = 16 - x = SCREEN_SIZE[0] / 2 - w / 2 - y = SCREEN_SIZE[1] / 2 - h / 2 - c = sge.gfx.Color("black") - window = xsge_gui.Window(gui_handler, x, y, w, h, - background_color=c, border=False) - - x = margin - y = margin - text = _("Importing {}...").format(fname) - c = sge.gfx.Color("white") - xsge_gui.Label( - window, x, y, 1, text, font=font, width=(w - 2 * margin), - height=(h - 3 * margin - - xsge_gui.progressbar_container_sprite.height), - color=c) - - x = margin - y = h - margin - xsge_gui.progressbar_container_sprite.height - progressbar = xsge_gui.ProgressBar(window, x, y, 0, - width=(w - 2 * margin)) - - window.show() - gui_handler.event_step(0, 0) - _refresh_screen(0, 0) - - try: - with zipfile.ZipFile(fname, 'r') as rtz: - infolist = rtz.infolist() - for i in range(len(infolist)): - member = infolist[i] - rtz.extract(member, DATA) - rtz.extract(member, os.path.join(LOCAL, "data")) - progressbar.progress = (i + 1) / len(infolist) - progressbar.redraw() - sge.game.pump_input() - gui_handler.event_step(0, 0) - _refresh_screen(0, 0) - except Exception as e: - show_error(str(e)) - - window.destroy() - sge.game.pump_input() - gui_handler.event_step(0, 0) - _refresh_screen(0, 0) - sge.game.pump_input() - sge.game.input_events = [] - else: - play_sound(kill_sound) - e = _("This feature requires Tkinter, which was not " - "successfully imported. Please make sure Tkinter is " - "installed and try again.") - show_error(e) - OptionsMenu.create_page(default=self.choice) else: play_sound(cancel_sound) write_to_disk() @@ -6926,84 +6850,41 @@ text = _("Press the key you wish to bind to this function, or the " "Escape key to cancel.") + def bound(target, self=self, default=self.choice): + # What to do with the input picked (None: cancelled). + def done(k): + if k is not None: + bind_key(target, k) + play_sound(confirm_sound) + else: + play_sound(cancel_sound) + self.__class__.create_page(default=default, page=self.page) + return done + if self.choice == 0: play_sound(select_sound) self.__class__.create_page(default=self.choice, page=(self.page + 1)) elif self.choice == 1: - k = wait_key(text) - if k is not None: - bind_key(left_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(left_key[self.page])) elif self.choice == 2: - k = wait_key(text) - if k is not None: - bind_key(right_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(right_key[self.page])) elif self.choice == 3: - k = wait_key(text) - if k is not None: - bind_key(up_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(up_key[self.page])) elif self.choice == 4: - k = wait_key(text) - if k is not None: - bind_key(down_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(down_key[self.page])) elif self.choice == 5: - k = wait_key(text) - if k is not None: - bind_key(jump_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(jump_key[self.page])) elif self.choice == 6: - k = wait_key(text) - if k is not None: - bind_key(action_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(action_key[self.page])) elif self.choice == 7: - k = wait_key(text) - if k is not None: - bind_key(sneak_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(sneak_key[self.page])) elif self.choice == 8: - k = wait_key(text) - if k is not None: - bind_key(menu_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(menu_key[self.page])) elif self.choice == 9: - k = wait_key(text) - if k is not None: - bind_key(pause_key[self.page], k) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_key(text, bound(pause_key[self.page])) else: play_sound(cancel_sound) - OptionsMenu.create_page(default=7) + OptionsMenu.create_page(default=8) class JoystickMenu(Menu): @@ -7064,84 +6945,41 @@ text = _("Press the joystick button, axis, or hat direction you wish " "to bind to this function, or the Escape key to cancel.") + def bound(target, self=self, default=self.choice): + # What to do with the input picked (None: cancelled). + def done(js): + if js is not None: + bind_js(target, js) + play_sound(confirm_sound) + else: + play_sound(cancel_sound) + self.__class__.create_page(default=default, page=self.page) + return done + if self.choice == 0: play_sound(select_sound) self.__class__.create_page(default=self.choice, page=(self.page + 1)) elif self.choice == 1: - js = wait_js(text) - if js is not None: - bind_js(left_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(left_js[self.page])) elif self.choice == 2: - js = wait_js(text) - if js is not None: - bind_js(right_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(right_js[self.page])) elif self.choice == 3: - js = wait_js(text) - if js is not None: - bind_js(up_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(up_js[self.page])) elif self.choice == 4: - js = wait_js(text) - if js is not None: - bind_js(down_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(down_js[self.page])) elif self.choice == 5: - js = wait_js(text) - if js is not None: - bind_js(jump_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(jump_js[self.page])) elif self.choice == 6: - js = wait_js(text) - if js is not None: - bind_js(action_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(action_js[self.page])) elif self.choice == 7: - js = wait_js(text) - if js is not None: - bind_js(sneak_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(sneak_js[self.page])) elif self.choice == 8: - js = wait_js(text) - if js is not None: - bind_js(menu_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(menu_js[self.page])) elif self.choice == 9: - js = wait_js(text) - if js is not None: - bind_js(pause_js[self.page], js) - play_sound(confirm_sound) - else: - play_sound(cancel_sound) - self.__class__.create_page(default=self.choice, page=self.page) + wait_js(text, bound(pause_js[self.page])) else: play_sound(cancel_sound) - OptionsMenu.create_page(default=8) + OptionsMenu.create_page(default=9) class ModalMenu(xsge_gui.MenuDialog): @@ -7392,77 +7230,51 @@ xsge_gui.escape_joystick_events = [(0, "button", 8)] -def wait_key(text): - # Wait for a key press and return it. - sge.game.pump_input() - sge.game.input_events = [] +class WaitInputDialog(xsge_gui.Dialog): - while True: - # Input events - sge.game.pump_input() - while sge.game.input_events: - event = sge.game.input_events.pop(0) - if isinstance(event, sge.input.KeyPress): - sge.game.pump_input() - sge.game.input_events = [] - if event.key == "escape": - return None - else: - return event.key - - # Regulate speed - sge.game.regulate_speed(fps=10) - - # Project text - sge.game.project_text(font, text, sge.game.width / 2, - sge.game.height / 2, width=sge.game.width, - height=sge.game.height, - color=sge.gfx.Color("white"), - halign="center", valign="middle", - outline=sge.gfx.Color("black"), - outline_thickness=text_outline_thickness) + # Waits for a key press (or a joystick event) and keeps it in + # ``result`` (None: cancelled with Escape). It replaces the loops that + # wait_key() and wait_js() used to run. + + def __init__(self, text, joystick=False): + self.result = None + self.joystick = joystick + w = sge.game.width + h = sge.game.height + super().__init__(gui_handler, 0, 0, w, h, + background_color=sge.gfx.Color("black"), + border=False) + xsge_gui.Label(self, 0, 0, 0, text, font=font, width=w, height=h, + color=sge.gfx.Color("white"), halign="center", + valign="middle") + + def event_global_key_press(self, key, char): + if self.joystick: + if key == "escape": + self.destroy() + else: + self.result = None if key == "escape" else key + self.destroy() - # Refresh - _refresh_screen(0, 0) + def event_global_joystick(self, js_name, js_id, input_type, input_id, + value): + if (self.joystick + and input_type not in {"axis0", "hat_center_x", "hat_center_y"} + and value >= joystick_threshold): + self.result = (js_id, input_type, input_id) + self.destroy() -def wait_js(text): - # Wait for a joystick press and return it. - sge.game.pump_input() - sge.game.input_events = [] +def wait_key(text, callback): + # Wait for a key press and pass it to callback (None if cancelled). + dialog = WaitInputDialog(text) + dialog.show(on_close=lambda: callback(dialog.result)) - while True: - # Input events - sge.game.pump_input() - while sge.game.input_events: - event = sge.game.input_events.pop(0) - if isinstance(event, sge.input.KeyPress): - if event.key == "escape": - sge.game.pump_input() - sge.game.input_events = [] - return None - elif isinstance(event, sge.input.JoystickEvent): - if (event.input_type not in {"axis0", "hat_center_x", - "hat_center_y"} and - event.value >= joystick_threshold): - sge.game.pump_input() - sge.game.input_events = [] - return (event.js_id, event.input_type, event.input_id) - - # Regulate speed - sge.game.regulate_speed(fps=10) - - # Project text - sge.game.project_text(font, text, sge.game.width / 2, - sge.game.height / 2, width=sge.game.width, - height=sge.game.height, - color=sge.gfx.Color("white"), - halign="center", valign="middle", - outline=sge.gfx.Color("black"), - outline_thickness=text_outline_thickness) - # Refresh - _refresh_screen(0, 0) +def wait_js(text, callback): + # Wait for a joystick press and pass it to callback (None if cancelled). + dialog = WaitInputDialog(text, joystick=True) + dialog.show(on_close=lambda: callback(dialog.result)) def show_error(message): @@ -8630,8 +8442,6 @@ finally: cfg_version = cfg.get("version", 0) - fullscreen = cfg.get("fullscreen", fullscreen) - sge.game.fullscreen = fullscreen scale_method = cfg.get("scale_method", scale_method) sge.game.scale_method = scale_method scale_proportional = cfg.get("scale_proportional", scale_proportional) @@ -8722,15 +8532,13 @@ save_slots[i] = loaded_slots[i] -if __name__ == '__main__': +async def main(): print(_("Starting game...")) - if HAVE_TK: - tkwindow = Tk() - tkwindow.withdraw() - try: - sge.game.start() + await sge.game.start() finally: write_to_disk() - shutil.rmtree(DATA) + + +asyncio.run(main())