# Copyright 2004-2019 Tom Rothamel # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # This file contains the code for in-game Ren'Py error handling. It's a # module (as opposed to a .rpy file) because that allows us to ensure # that it is fully loaded or run before any other Ren'Py code runs. init python in gui: system_font = None init python: style._default = Style(None) init python hide: if renpy.loadable("gui.rpy") or renpy.loadable("gui.rpyc"): config.screen_width, config.screen_height = 1280, 720 def init_system_styles(): if gui.system_font is not None: style._default.font = gui.system_font config.init_system_styles = init_system_styles init label _errorhandling: python in gui: from store import config def _scale(n): return int(min(n * config.screen_width / 960, n * config.screen_height / 720)) _wide = (config.screen_width > (config.screen_height * 1.5)) style _default: # Text properties font "DejaVuSans.ttf" language "unicode" antialias True size gui._scale(16) color "#404040" black_color (0, 0, 0, 255) bold False italic False underline False strikethrough False kerning 0.0 drop_shadow None drop_shadow_color (0, 0, 0, 255) outlines [ ] outline_scaling "step" minwidth 0 text_align 0 justify False text_y_fudge 0 first_indent 0 rest_indent 0 line_spacing 0 line_leading 0 line_overlap_split 0 layout "tex" subtitle_width 0.9 slow_cps None slow_cps_multiplier 1.0 slow_abortable False hinting "auto" adjust_spacing True # Window properties background None xpadding 0 ypadding 0 xmargin 0 ymargin 0 xfill False yfill False xminimum 0 yminimum 0 # Placement properties xpos None ypos None xanchor None yanchor None xmaximum None ymaximum None xoffset 0 yoffset 0 subpixel False # Sound properties activate_sound None hover_sound None # Box properties spacing 0 first_spacing None box_layout None box_wrap False box_wrap_spacing 0 box_reverse False order_reverse False xfit False yfit False # Button properties focus_mask None focus_rect None keyboard_focus True key_events False hover_key_events True # Bar properties fore_bar Null() aft_bar Null() thumb None thumb_shadow None left_gutter 0 right_gutter 0 thumb_offset 0 unscrollable None bar_invert False bar_vertical False # Viewport properties clipping False # Grid properties xspacing None yspacing None init python: # Temporarily, until the real styles can be defined. style.default = style._default style.image = style._default style.fixed = style._default ########################################################################## style _frame is _default: background "#d0d0d0" xpadding gui._scale(150 if gui._wide else 15) ypadding gui._scale(15) xfill True yfill True style _text is _default style _fixed is _default style _input is _default style _hbox is _default: box_layout 'horizontal' style _vbox is _default: box_layout 'vertical' style _grid is _default style _side is _default style _drag is _default style _viewport is _default: xfill True yfill True style _button is _default: ypadding gui._scale(6) style _button_text is _default: size gui._scale(22) color "#468" hover_color "#24c" insensitive_color "#00000020" style _small_button is _button style _small_button_text is _button_text: size gui._scale(16) style _radio_button is _button: selected_background Solid("#468", xsize=gui._scale(5), ysize=gui._scale(22), yalign=0.5) selected_hover_background Solid("#24C", xsize=gui._scale(5), ysize=gui._scale(22), yalign=0.5) left_padding gui._scale(15) ypadding gui._scale(2) style _label is _default: top_margin gui._scale(10) bottom_margin gui._scale(15) style _label_text is _default: size gui._scale(30) kerning -1 style _bar is _default: left_bar "#468" hover_left_bar "#24C" right_bar "#b0b0b0" ysize gui._scale(20) style _vbar is _default: bottom_bar "#468" hover_bottom_bar "#24C" top_bar "#b0b0b0" xsize gui._scale(20) bar_vertical True style _slider is _bar style _vslider is _vbar style _scrollbar is _default: thumb "#808080" hover_thumb "#a0a0a0" base_bar "#b0b0b0" unscrollable "hide" ymaximum gui._scale(10) style _vscrollbar is _default: thumb "#808080" hover_thumb "#a0a0a0" base_bar "#b0b0b0" unscrollable "hide" xmaximum gui._scale(10) bar_vertical True bar_invert True style _hyperlink is _default: color "#468" hover_color Color("#24C") return # Invokes _errorhanding when this is first loaded. init: call _errorhandling # Early hyperlink support. init python: def _error_hyperlink_styler(target): return style._hyperlink def _error_hyperlink_function(target): if target.startswith("http:") or target.startswith("https:"): try: import webbrowser webbrowser.open(target) except: pass if target.startswith("edit:"): prefix, line, filename = target.split(":", 2) line = int(line) renpy.launch_editor([ filename ], line) style._default.hyperlink_functions = (_error_hyperlink_styler, _error_hyperlink_function, None) init python: # The keymap we use before the real keymap is defined. config.keymap = dict( # Bindings present almost everywhere, unless explicitly # disabled. toggle_fullscreen = [ 'f', 'alt_K_RETURN', 'alt_K_KP_ENTER', 'K_F11' ], reload_game = [ 'R' ], quit = [ 'meta_q', 'alt_K_F4', 'alt_q' ], iconify = [ 'meta_m', 'alt_m' ], choose_renderer = [ 'G' ], # Focus. focus_left = [ 'K_LEFT' ], focus_right = [ 'K_RIGHT' ], focus_up = [ 'K_UP' ], focus_down = [ 'K_DOWN' ], # Button. button_ignore = [ 'mousedown_1' ], button_select = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER', 'K_SELECT' ], # Viewport. viewport_leftarrow = [ 'K_LEFT', 'repeat_K_LEFT' ], viewport_rightarrow = [ 'K_RIGHT', 'repeat_K_RIGHT' ], viewport_uparrow = [ 'K_UP', 'repeat_K_UP' ], viewport_downarrow = [ 'K_DOWN', 'repeat_K_DOWN' ], viewport_wheelup = [ 'mousedown_4' ], viewport_wheeldown = [ 'mousedown_5' ], viewport_drag_start = [ 'mousedown_1' ], viewport_drag_end = [ 'mouseup_1' ], viewport_pageup = [ 'K_PAGEUP', 'repeat_K_PAGEUP' ], viewport_pagedown = [ 'K_PAGEDOWN', 'repeat_K_PAGEDOWN' ], # These control the bar. bar_activate = [ 'mousedown_1', 'K_RETURN', 'K_KP_ENTER', 'K_SELECT' ], bar_deactivate = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER', 'K_SELECT' ], bar_decrease = [ 'K_LEFT' ], bar_increase = [ 'K_RIGHT' ], # Null console, just in case. console = [ ], ) config.pad_bindings = { "pad_leftshoulder_press" : [ "rollback", ], "pad_lefttrigger_pos" : [ "rollback", ], "pad_back_press" : [ "rollback", ], "pad_guide_press" : [ "game_menu", ], "pad_start_press" : [ "game_menu", ], "pad_y_press" : [ "hide_windows", ], "pad_rightshoulder_press" : [ "rollforward", ], "pad_righttrigger_pos" : [ "dismiss", "button_select" ], "pad_a_press" : [ "dismiss", "button_select" ], "pad_b_press" : [ "button_alternate" ], "pad_dpleft_press" : [ "focus_left", "bar_left" ], "pad_leftx_neg" : [ "focus_left", "bar_left" ], "pad_rightx_neg" : [ "focus_left", "bar_left" ], "pad_dpright_press" : [ "focus_right", "bar_right" ], "pad_leftx_pos" : [ "focus_right", "bar_right" ], "pad_rightx_pos" : [ "focus_right", "bar_right" ], "pad_dpup_press" : [ "focus_up", "bar_up" ], "pad_lefty_neg" : [ "focus_up", "bar_up" ], "pad_righty_neg" : [ "focus_up", "bar_up" ], "pad_dpdown_press" : [ "focus_down", "bar_down" ], "pad_lefty_pos" : [ "focus_down", "bar_down" ], "pad_righty_pos" : [ "focus_down", "bar_down" ], } # Null translation function. This gets redefined once things start # successfully. def _(s): return s # This function is responsible for taking a traceback, and converting # it to a string that can be shown with text. def __format_traceback(s): import re lines = [ i.replace("{", "{{") for i in s.split("\n") ] rv = [ ("{size=%d}" % gui._scale(22)) + lines[0] + "{/size}" ] for i in lines[1:]: i = re.sub(r'(File "(.*)", line (\d+))', r'{a=edit:\3:\2}\1{/a}', i) rv.append(" " + i) rv[1] = "{vspace=5}" + rv[1] return "\n".join(rv) def __format_parse_errors(s): import re rv = "" lines = s.split("\n") len_lines = len(lines) ln = 0 while ln < len_lines: line = lines[ln] ln += 1 if ln < len_lines and lines[ln].endswith("^"): highlight = len(lines[ln]) - 1 ln += 1 else: highlight = -1 pos = 0 for c in line: if pos == highlight: rv += u"{color=#c00}\u2192{/color}" highlight = -1 pos += 1 if c == "{": rv += "{{" else: rv += c if highlight > 0: rv += u"{color=#c00}\u2190{/color}" rv += "\n" rv = re.sub(r'(File "(.*)", line (\d+))', r'{a=edit:\3:\2}\1{/a}', rv) return rv class _EditFile(Action): def __init__(self, filename, line=1): self.filename = filename self.line = line def __call__(self): try: renpy.launch_editor([ self.filename ], self.line, transient=1) except: pass class _CopyFile(Action): def __init__(self, filename, template): self.filename = filename self.template = template def __call__(self): import pygame.scrap with open(self.filename, "rb") as f: f.read(3) # skip the BOM. s = self.template.format(f.read().decode("utf-8")) s = s.replace("\n", "\r\n") s = s.replace("\r\r", "\r") pygame.scrap.put(pygame.SCRAP_TEXT, s.encode("utf-8")) def __can_open_traceback(): return True class __TooltipAction(object): def __init__(self, tooltip, value): self.tooltip = tooltip self.value = value def __call__(self): if self.tooltip.value != self.value: self.tooltip.value = self.value renpy.restart_interaction() def unhovered(self): if self.tooltip.value != self.tooltip.default: self.tooltip.value = self.tooltip.default renpy.restart_interaction() class __Tooltip(object): def __init__(self, default): self.value = default self.default = default def action(self, value): return __TooltipAction(self, value) class __XScrollValue(BarValue): def __init__(self, viewport): self.viewport = viewport def get_adjustment(self): w = renpy.get_widget(None, self.viewport) if not isinstance(w, Viewport): raise Exception("The displayable with id %r is not declared, or not a viewport." % self.viewport) return w.xadjustment def get_style(self): return "scrollbar", "vscrollbar" class __YScrollValue(BarValue): def __init__(self, viewport): self.viewport = viewport def get_adjustment(self): w = renpy.get_widget(None, self.viewport) if not isinstance(w, Viewport): raise Exception("The displayable with id %r is not declared, or not a viewport." % self.viewport) return w.yadjustment def get_style(self): return "scrollbar", "vscrollbar" class __ErrorQuit(Action): """ An action that quits with an error status. """ def __call__(self): renpy.quit(status=1) class __EnterConsole(Action): """ An action that enters the console if we can. """ def __call__(self): try: f = _console.enter except: return None return f() # This screen can be customized to add additional actions to the exception # screen. It currently takes two positional parameters. # # * traceback_fn - a filename containing the exception text. # * tt - a tooltip used for help text. # # For forward-compatibility, custom implmentations should use *args to ignore # added arguments. screen _exception_actions(traceback_fn, tt, *args): textbutton _("Open"): action _EditFile(traceback_fn) hovered tt.action(_("Opens the traceback.txt file in a text editor.")) textbutton _("Copy BBCode"): action _CopyFile(traceback_fn, u"[code]\n{}[/code]\n") hovered tt.action(_("Copies the traceback.txt file to the clipboard as BBcode for forums like https://lemmasoft.renai.us/.")) textbutton __("Copy Markdown"): action _CopyFile(traceback_fn, u"```\n{}```\n") hovered tt.action(_("Copies the traceback.txt file to the clipboard as Markdown for Discord.")) python early in _errorhandling: # These enable various error handling modes. rollback = True ignore = True reload = True console = True # The screen that is used for error handling. screen _exception: modal True zorder 1090 default tt = __Tooltip("") default fmt_short = __format_traceback(short) default fmt_full = __format_traceback(full) frame: style_group "" has side "t c b": spacing gui._scale(10) side "c r": xfill True label _("An exception has occurred.") text_size gui._scale(40) text "{size=-3}[config.version!q]\n[renpy.version_only!q]\n[renpy.platform!q]{/size}" text_align 1.0 yalign 0.5 viewport: id "viewport" child_size (4000, None) mousewheel True draggable True scrollbars "both" has vbox text fmt_short substitute False text fmt_full substitute False vbox: hbox: spacing gui._scale(25) if rollback_action and _errorhandling.rollback: textbutton _("Rollback"): action rollback_action hovered tt.action(_("Attempts a roll back to a prior time, allowing you to save or choose a different choice.")) if ignore_action and _errorhandling.ignore: textbutton _("Ignore"): action ignore_action if _ignore_action: hovered tt.action(_("Ignores the exception, allowing you to continue.")) else: hovered tt.action(_("Ignores the exception, allowing you to continue. This often leads to additional errors.")) if config.developer and not renpy.mobile: if _errorhandling.reload: textbutton _("Reload"): action reload_action hovered tt.action(_("Reloads the game from disk, saving and restoring game state if possible.")) if _errorhandling.console: textbutton _("Console") : action __EnterConsole() hovered tt.action(_("Opens a console to allow debugging the problem.")) use _exception_actions(traceback_fn, tt) vbox: xfill True textbutton _("Quit"): xalign 1.0 action __ErrorQuit() hovered tt.action(_("Quits the game.")) # Tooltip. text tt.value if config.developer and reload_action: key "R" action reload_action key "console" action __EnterConsole() # The screen that is used for error handling. screen _parse_errors: modal True zorder 1090 default tt = __Tooltip("") default fmt_errors = __format_parse_errors(errors) frame: style_group "" has side "t c b": spacing gui._scale(10) label _("Parsing the script failed.") text_size gui._scale(40) viewport: id "viewport" child_size (4000, None) mousewheel True draggable True scrollbars "both" xfill True yfill True has vbox text fmt_errors substitute False vbox: hbox: spacing gui._scale(25) textbutton _("Reload"): action reload_action hovered tt.action(_("Reloads the game from disk, saving and restoring game state if possible.")) textbutton _("Open"): action _EditFile(error_fn) hovered tt.action(_("Opens the errors.txt file in a text editor.")) textbutton _("Copy BBCode"): action _CopyFile(error_fn, u"[code]\n{}[/code]\n") hovered tt.action(_("Copies the errors.txt file to the clipboard as BBcode for forums like https://lemmasoft.renai.us/.")) textbutton __("Copy Markdown"): action _CopyFile(error_fn, u"```\n{}```\n") hovered tt.action(_("Copies the errors.txt file to the clipboard as Markdown for Discord.")) vbox: xfill True textbutton _("Quit"): action __ErrorQuit() hovered tt.action(_("Quits the game.")) xalign 1.0 # Tooltip. text tt.value if config.developer and reload_action: key "R" action reload_action