3605 lines
105 KiB
Python
3605 lines
105 KiB
Python
# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
|
|
#
|
|
# 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 code for initializing and managing the display
|
|
# window.
|
|
|
|
from __future__ import print_function
|
|
import renpy.display
|
|
import renpy.audio
|
|
import renpy.text
|
|
import renpy.test
|
|
|
|
import pygame_sdl2 as pygame
|
|
|
|
import sys
|
|
import os
|
|
import time
|
|
import cStringIO
|
|
import threading
|
|
import copy
|
|
import gc
|
|
import inspect
|
|
|
|
import_time = time.time()
|
|
|
|
try:
|
|
import android # @UnresolvedImport
|
|
except:
|
|
android = None
|
|
|
|
TIMEEVENT = pygame.event.register("TIMEEVENT")
|
|
PERIODIC = pygame.event.register("PERIODIC")
|
|
REDRAW = pygame.event.register("REDRAW")
|
|
EVENTNAME = pygame.event.register("EVENTNAME")
|
|
|
|
# All events except for TIMEEVENT and REDRAW
|
|
ALL_EVENTS = set(pygame.event.get_standard_events()) # @UndefinedVariable
|
|
ALL_EVENTS.add(PERIODIC)
|
|
ALL_EVENTS.add(EVENTNAME)
|
|
|
|
enabled_events = {
|
|
pygame.QUIT,
|
|
|
|
pygame.APP_TERMINATING,
|
|
pygame.APP_LOWMEMORY,
|
|
pygame.APP_WILLENTERBACKGROUND,
|
|
pygame.APP_DIDENTERBACKGROUND,
|
|
pygame.APP_WILLENTERFOREGROUND,
|
|
pygame.APP_DIDENTERFOREGROUND,
|
|
|
|
pygame.WINDOWEVENT,
|
|
pygame.SYSWMEVENT,
|
|
|
|
pygame.KEYDOWN,
|
|
pygame.KEYUP,
|
|
|
|
pygame.TEXTEDITING,
|
|
pygame.TEXTINPUT,
|
|
|
|
pygame.MOUSEMOTION,
|
|
pygame.MOUSEBUTTONDOWN,
|
|
pygame.MOUSEBUTTONUP,
|
|
pygame.MOUSEWHEEL,
|
|
|
|
pygame.JOYAXISMOTION,
|
|
pygame.JOYHATMOTION,
|
|
pygame.JOYBALLMOTION,
|
|
pygame.JOYBUTTONDOWN,
|
|
pygame.JOYBUTTONUP,
|
|
pygame.JOYDEVICEADDED,
|
|
pygame.JOYDEVICEREMOVED,
|
|
|
|
pygame.CONTROLLERAXISMOTION,
|
|
pygame.CONTROLLERBUTTONDOWN,
|
|
pygame.CONTROLLERBUTTONUP,
|
|
pygame.CONTROLLERDEVICEADDED,
|
|
pygame.CONTROLLERDEVICEREMOVED,
|
|
|
|
pygame.RENDER_TARGETS_RESET,
|
|
|
|
TIMEEVENT,
|
|
PERIODIC,
|
|
REDRAW,
|
|
EVENTNAME,
|
|
}
|
|
|
|
# The number of msec between periodic events.
|
|
PERIODIC_INTERVAL = 50
|
|
|
|
# Time management.
|
|
time_base = 0.0
|
|
time_mult = 1.0
|
|
|
|
|
|
def init_time():
|
|
warp = os.environ.get("RENPY_TIMEWARP", "1.0")
|
|
|
|
global time_base
|
|
global time_mult
|
|
|
|
time_base = time.time()
|
|
time_mult = float(warp)
|
|
|
|
|
|
def get_time():
|
|
t = time.time()
|
|
return time_base + (t - time_base) * time_mult
|
|
|
|
|
|
def displayable_by_tag(layer, tag):
|
|
"""
|
|
Get the displayable on the given layer with the given tag.
|
|
"""
|
|
|
|
return renpy.game.context().scene_lists.get_displayable_by_tag(layer, tag)
|
|
|
|
|
|
class IgnoreEvent(Exception):
|
|
"""
|
|
Exception that is raised when we want to ignore an event, but
|
|
also don't want to return anything.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class EndInteraction(Exception):
|
|
"""
|
|
Exception that can be raised (for example, during the render method of
|
|
a displayable) to end the current interaction immediately.
|
|
"""
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
|
|
class absolute(float):
|
|
"""
|
|
This represents an absolute float coordinate.
|
|
"""
|
|
__slots__ = [ ]
|
|
|
|
|
|
def place(width, height, sw, sh, placement):
|
|
"""
|
|
Performs the Ren'Py placement algorithm.
|
|
|
|
`width`, `height`
|
|
The width and height of the area the image will be
|
|
placed in.
|
|
|
|
`size`
|
|
The size of the image to be placed.
|
|
|
|
`placement`
|
|
The tuple returned by Displayable.get_placement().
|
|
"""
|
|
|
|
xpos, ypos, xanchor, yanchor, xoffset, yoffset, _subpixel = placement
|
|
|
|
if xpos is None:
|
|
xpos = 0
|
|
if ypos is None:
|
|
ypos = 0
|
|
if xanchor is None:
|
|
xanchor = 0
|
|
if yanchor is None:
|
|
yanchor = 0
|
|
if xoffset is None:
|
|
xoffset = 0
|
|
if yoffset is None:
|
|
yoffset = 0
|
|
|
|
# We need to use type, since isinstance(absolute(0), float).
|
|
if xpos.__class__ is float:
|
|
xpos *= width
|
|
|
|
if xanchor.__class__ is float:
|
|
xanchor *= sw
|
|
|
|
x = xpos + xoffset - xanchor
|
|
|
|
if ypos.__class__ is float:
|
|
ypos *= height
|
|
|
|
if yanchor.__class__ is float:
|
|
yanchor *= sh
|
|
|
|
y = ypos + yoffset - yanchor
|
|
|
|
return x, y
|
|
|
|
|
|
class DisplayableArguments(renpy.object.Object):
|
|
"""
|
|
Represents a set of arguments that can be passed to a duplicated
|
|
displayable.
|
|
"""
|
|
|
|
# The name of the displayable without any arguments.
|
|
name = ()
|
|
|
|
# Arguments supplied.
|
|
args = ()
|
|
|
|
# The style prefix in play. This is used by DynamicImage to figure
|
|
# out the prefix list to apply.
|
|
prefix = None
|
|
|
|
# True if lint is in use.
|
|
lint = False
|
|
|
|
def copy(self, **kwargs):
|
|
"""
|
|
Returns a copy of this object with the various fields set to the
|
|
values they were given in kwargs.
|
|
"""
|
|
|
|
rv = DisplayableArguments()
|
|
rv.__dict__.update(self.__dict__)
|
|
rv.__dict__.update(kwargs)
|
|
|
|
return rv
|
|
|
|
def extraneous(self):
|
|
if renpy.config.developer and renpy.config.report_extraneous_attributes:
|
|
raise Exception("Image '{}' does not accept attributes '{}'.".format(
|
|
" ".join(self.name),
|
|
" ".join(self.args),
|
|
))
|
|
|
|
|
|
default_style = renpy.style.Style("default")
|
|
|
|
|
|
class Displayable(renpy.object.Object):
|
|
"""
|
|
The base class for every object in Ren'Py that can be
|
|
displayed to the screen.
|
|
|
|
Drawables will be serialized to a savegame file. Therefore, they
|
|
shouldn't store non-serializable things (like pygame surfaces) in
|
|
their fields.
|
|
"""
|
|
|
|
# Some invariants about method call order:
|
|
#
|
|
# per_interact is called before render.
|
|
# render is called before event.
|
|
#
|
|
# get_placement can be called at any time, so can't
|
|
# assume anything.
|
|
|
|
# If True this displayable can accept focus.
|
|
# If False, it can't, but it keeps its place in the focus order.
|
|
# If None, it does not have a place in the focus order.
|
|
focusable = None
|
|
|
|
# This is the focus named assigned by the focus code.
|
|
full_focus_name = None
|
|
|
|
# A role ('selected_' or '' that prefixes the style).
|
|
role = ''
|
|
|
|
# The event we'll pass on to our parent transform.
|
|
transform_event = None
|
|
|
|
# Can we change our look in response to transform_events?
|
|
transform_event_responder = False
|
|
|
|
# The main displayable, if this displayable is the root of a composite
|
|
# displayable. (This is used by SL to figure out where to add children
|
|
# to.) If None, it is itself.
|
|
_main = None
|
|
|
|
# A list of the children that make up this composite displayable.
|
|
_composite_parts = [ ]
|
|
|
|
# The location the displayable was created at, if known.
|
|
_location = None
|
|
|
|
# Does this displayable use the scope?
|
|
_uses_scope = False
|
|
|
|
# Arguments supplied to this displayable.
|
|
_args = DisplayableArguments()
|
|
|
|
# Set to true of the displayable is duplicatable (has a non-trivial
|
|
# duplicate method), or one of its children is.
|
|
_duplicatable = False
|
|
|
|
# Does this displayable require clipping?
|
|
_clipping = False
|
|
|
|
# Does this displayable have a tooltip?
|
|
_tooltip = None
|
|
|
|
def __ne__(self, o):
|
|
return not (self == o)
|
|
|
|
def __init__(self, focus=None, default=False, style='default', _args=None, tooltip=None, default_focus=False, **properties):
|
|
|
|
global default_style
|
|
|
|
if (style == "default") and (not properties):
|
|
self.style = default_style
|
|
else:
|
|
self.style = renpy.style.Style(style, properties) # @UndefinedVariable
|
|
|
|
self.focus_name = focus
|
|
self.default = default or default_focus
|
|
self._tooltip = tooltip
|
|
|
|
if _args is not None:
|
|
self._args = _args
|
|
|
|
def _copy(self, args=None):
|
|
"""
|
|
Makes a shallow copy of the displayable. If `args` is provided,
|
|
replaces the arguments with the stored copy.
|
|
"""
|
|
|
|
rv = copy.copy(self)
|
|
|
|
if args is not None:
|
|
rv._args = args
|
|
|
|
return rv
|
|
|
|
def _duplicate(self, args):
|
|
"""
|
|
Makes a duplicate copy of the following kids of displayables:
|
|
|
|
* Displayables that can accept arguments.
|
|
* Displayables that maintain state that should be reset before being
|
|
shown to the user.
|
|
* Containers that contain (including transitively) one of the other
|
|
kinds of displayables.
|
|
|
|
Displayables that contain state that can be manipulated by the user
|
|
are never copied.
|
|
|
|
This should call _unique on children that have been copied before
|
|
setting its own _duplicatable flag.
|
|
"""
|
|
|
|
if args and args.args:
|
|
args.extraneous()
|
|
|
|
return self
|
|
|
|
def _get_tooltip(self):
|
|
"""
|
|
Returns the tooltip of this displayable.
|
|
"""
|
|
|
|
return self._tooltip
|
|
|
|
def _in_current_store(self):
|
|
"""
|
|
Returns a version of this displayable that will not change as it is
|
|
rendered.
|
|
"""
|
|
|
|
return self
|
|
|
|
def _unique(self):
|
|
"""
|
|
This is called when a displayable is "born" unique, which occurs
|
|
when there is only a single reference to it. What it does is to
|
|
manage the _duplicatable flag - setting it false unless one of
|
|
the displayable's children happens to be duplicatable.
|
|
"""
|
|
|
|
return
|
|
|
|
def parameterize(self, name, parameters):
|
|
"""
|
|
Obsolete alias for _duplicate.
|
|
"""
|
|
|
|
a = self._args.copy(name=name, args=parameters)
|
|
return self._duplicate(a)
|
|
|
|
def _equals(self, o):
|
|
"""
|
|
This is a utility method that can be called by a Displayable's
|
|
__eq__ method, to compare displayables for type and displayable
|
|
component equality.
|
|
"""
|
|
|
|
if type(self) is not type(o):
|
|
return False
|
|
|
|
if self.focus_name != o.focus_name:
|
|
return False
|
|
|
|
if self.style != o.style:
|
|
return False
|
|
|
|
if self.default != o.default:
|
|
return False
|
|
|
|
return True
|
|
|
|
def __unicode__(self):
|
|
return self.__class__.__name__
|
|
|
|
def __repr__(self):
|
|
return "<{} at {:x}>".format(unicode(self).encode("utf-8"), id(self))
|
|
|
|
def find_focusable(self, callback, focus_name):
|
|
|
|
focus_name = self.focus_name or focus_name
|
|
|
|
if self.focusable:
|
|
callback(self, focus_name)
|
|
elif self.focusable is not None:
|
|
callback(None, focus_name)
|
|
|
|
for i in self.visit():
|
|
if i is None:
|
|
continue
|
|
|
|
i.find_focusable(callback, focus_name)
|
|
|
|
def focus(self, default=False):
|
|
"""
|
|
Called to indicate that this widget has the focus.
|
|
"""
|
|
|
|
self.set_style_prefix(self.role + "hover_", True)
|
|
|
|
if not default:
|
|
renpy.exports.play(self.style.hover_sound)
|
|
|
|
def unfocus(self, default=False):
|
|
"""
|
|
Called to indicate that this widget has become unfocused.
|
|
"""
|
|
|
|
self.set_style_prefix(self.role + "idle_", True)
|
|
|
|
def is_focused(self):
|
|
|
|
if renpy.display.focus.grab and renpy.display.focus.grab is not self:
|
|
return
|
|
|
|
return renpy.game.context().scene_lists.focused is self
|
|
|
|
def set_style_prefix(self, prefix, root):
|
|
"""
|
|
Called to set the style prefix of this widget and its child
|
|
widgets, if any.
|
|
|
|
`root` - True if this is the root of a style tree, False if this
|
|
has been passed on to a child.
|
|
"""
|
|
|
|
if prefix == self.style.prefix:
|
|
return
|
|
|
|
self.style.set_prefix(prefix)
|
|
renpy.display.render.redraw(self, 0)
|
|
|
|
def render(self, width, height, st, at):
|
|
"""
|
|
Called to display this displayable. This is called with width
|
|
and height parameters, which give the largest width and height
|
|
that this drawable can be drawn to without overflowing some
|
|
bounding box. It's also given two times. It returns a Surface
|
|
that is the current image of this drawable.
|
|
|
|
@param st: The time since this widget was first shown, in seconds.
|
|
@param at: The time since a similarly named widget was first shown,
|
|
in seconds.
|
|
"""
|
|
|
|
raise Exception("Render not implemented.")
|
|
|
|
def event(self, ev, x, y, st):
|
|
"""
|
|
Called to report than an event has occured. Ev is the raw
|
|
pygame event object representing that event. If the event
|
|
involves the mouse, x and y are the translation of the event
|
|
into the coordinates of this displayable. st is the time this
|
|
widget has been shown for.
|
|
|
|
@returns A value that should be returned from Interact, or None if
|
|
no value is appropriate.
|
|
"""
|
|
|
|
return None
|
|
|
|
def get_placement(self):
|
|
"""
|
|
Returns a style object containing placement information for
|
|
this Displayable. Children are expected to overload this
|
|
to return something more sensible.
|
|
"""
|
|
|
|
return self.style.get_placement()
|
|
|
|
def visit_all(self, callback, seen=None):
|
|
"""
|
|
Calls the callback on this displayable, and then on all children
|
|
of this displayable.
|
|
"""
|
|
|
|
if seen is None:
|
|
seen = set()
|
|
|
|
for d in self.visit():
|
|
|
|
if d is None:
|
|
continue
|
|
|
|
id_d = id(d)
|
|
if id_d in seen:
|
|
continue
|
|
|
|
seen.add(id_d)
|
|
d.visit_all(callback, seen)
|
|
|
|
callback(self)
|
|
|
|
def visit(self):
|
|
"""
|
|
Called to ask the displayable to return a list of its children
|
|
(including children taken from styles). For convenience, this
|
|
list may also include None values.
|
|
"""
|
|
|
|
return [ ]
|
|
|
|
def per_interact(self):
|
|
"""
|
|
Called once per widget per interaction.
|
|
"""
|
|
|
|
return None
|
|
|
|
def predict_one(self):
|
|
"""
|
|
Called to ask this displayable to call the callback with all
|
|
the images it may want to load.
|
|
"""
|
|
|
|
return
|
|
|
|
def predict_one_action(self):
|
|
"""
|
|
Called to ask this displayable to cause image prediction
|
|
to occur for images that may be loaded by its actions.
|
|
"""
|
|
|
|
return
|
|
|
|
def place(self, dest, x, y, width, height, surf, main=True):
|
|
"""
|
|
This places a render (which must be of this displayable)
|
|
within a bounding area. Returns an (x, y) tuple giving the location
|
|
the displayable was placed at.
|
|
|
|
`dest`
|
|
If not None, the `surf` will be blitted to `dest` at the
|
|
computed coordinates.
|
|
|
|
`x`, `y`, `width`, `height`
|
|
The bounding area.
|
|
|
|
`surf`
|
|
The render to place.
|
|
|
|
`main`
|
|
This is passed to Render.blit().
|
|
"""
|
|
|
|
placement = self.get_placement()
|
|
subpixel = placement[6]
|
|
|
|
xpos, ypos = place(width, height, surf.width, surf.height, placement)
|
|
|
|
xpos += x
|
|
ypos += y
|
|
|
|
pos = (xpos, ypos)
|
|
|
|
if dest is not None:
|
|
if subpixel:
|
|
dest.subpixel_blit(surf, pos, main, main, None)
|
|
else:
|
|
dest.blit(surf, pos, main, main, None)
|
|
|
|
return pos
|
|
|
|
def set_transform_event(self, event):
|
|
"""
|
|
Sets the transform event of this displayable to event.
|
|
"""
|
|
|
|
if event == self.transform_event:
|
|
return
|
|
|
|
self.transform_event = event
|
|
|
|
if self.transform_event_responder:
|
|
renpy.display.render.redraw(self, 0)
|
|
|
|
def _handles_event(self, event):
|
|
"""
|
|
Returns True if the displayable handles event, False otherwise.
|
|
"""
|
|
|
|
return False
|
|
|
|
def _hide(self, st, at, kind):
|
|
"""
|
|
Returns None if this displayable is ready to be hidden, or
|
|
a replacement displayable if it doesn't want to be hidden
|
|
quite yet. Kind is either "hide" or "replaced".
|
|
"""
|
|
|
|
return None
|
|
|
|
def _show(self):
|
|
"""
|
|
Called when the displayable is added to a scene list.
|
|
"""
|
|
|
|
def _target(self):
|
|
"""
|
|
If this displayable is part of a chain of one or more references,
|
|
returns the ultimate target of those references. Otherwise, returns
|
|
the displayable.
|
|
"""
|
|
|
|
return self
|
|
|
|
def _change_transform_child(self, child):
|
|
"""
|
|
If this is a transform, makes a copy of the transform and sets
|
|
the child of the innermost transform to this. Otherwise,
|
|
simply returns child.
|
|
"""
|
|
|
|
return child
|
|
|
|
def _clear(self):
|
|
"""
|
|
Clears out the children of this displayable, if any.
|
|
"""
|
|
|
|
return
|
|
|
|
def _tts_common(self, default_alt=None):
|
|
|
|
rv = [ ]
|
|
|
|
for i in self.visit():
|
|
if i is not None:
|
|
speech = i._tts()
|
|
|
|
if speech.strip():
|
|
rv.append(speech)
|
|
|
|
rv = ": ".join(rv)
|
|
rv = rv.replace("::", ":")
|
|
rv = rv.replace(": :", ":")
|
|
|
|
alt = self.style.alt
|
|
|
|
if alt is None:
|
|
alt = default_alt
|
|
|
|
if alt is not None:
|
|
rv = renpy.substitutions.substitute(alt, scope={ "text" : rv })[0]
|
|
|
|
return rv
|
|
|
|
def _tts(self):
|
|
"""
|
|
Returns the self-voicing text of this displayable and all of its
|
|
children that cannot take focus. If the displayable can take focus,
|
|
returns the empty string.
|
|
"""
|
|
|
|
return self._tts_common()
|
|
|
|
def _tts_all(self):
|
|
"""
|
|
Returns the self-voicing text of this displayable and all of its
|
|
children that cannot take focus.
|
|
"""
|
|
return self._tts_common()
|
|
|
|
|
|
class SceneListEntry(renpy.object.Object):
|
|
"""
|
|
Represents a scene list entry. Since this was replacing a tuple,
|
|
it should be treated as immutable after its initial creation.
|
|
"""
|
|
|
|
def __init__(self, tag, zorder, show_time, animation_time, displayable, name):
|
|
self.tag = tag
|
|
self.zorder = zorder
|
|
self.show_time = show_time
|
|
self.animation_time = animation_time
|
|
self.displayable = displayable
|
|
self.name = name
|
|
|
|
def __iter__(self):
|
|
return iter((self.tag, self.zorder, self.show_time, self.animation_time, self.displayable))
|
|
|
|
def __getitem__(self, index):
|
|
return (self.tag, self.zorder, self.show_time, self.animation_time, self.displayable)[index]
|
|
|
|
def __repr__(self):
|
|
return "<SLE: %r %r %r>" % (self.tag, self.name, self.displayable)
|
|
|
|
def copy(self):
|
|
return SceneListEntry(
|
|
self.tag,
|
|
self.zorder,
|
|
self.show_time,
|
|
self.animation_time,
|
|
self.displayable,
|
|
self.name)
|
|
|
|
def update_time(self, time):
|
|
|
|
rv = self
|
|
|
|
if self.show_time is None or self.animation_time is None:
|
|
rv = self.copy()
|
|
rv.show_time = rv.show_time or time
|
|
rv.animation_time = rv.animation_time or time
|
|
|
|
return rv
|
|
|
|
|
|
class SceneLists(renpy.object.Object):
|
|
"""
|
|
This stores the current scene lists that are being used to display
|
|
things to the user.
|
|
"""
|
|
|
|
__version__ = 7
|
|
|
|
def after_setstate(self):
|
|
for i in renpy.config.layers + renpy.config.top_layers:
|
|
if i not in self.layers:
|
|
self.layers[i] = [ ]
|
|
self.at_list[i] = { }
|
|
self.layer_at_list[i] = (None, [ ])
|
|
|
|
def after_upgrade(self, version):
|
|
|
|
if version < 1:
|
|
|
|
self.at_list = { }
|
|
self.layer_at_list = { }
|
|
|
|
for i in renpy.config.layers + renpy.config.top_layers:
|
|
self.at_list[i] = { }
|
|
self.layer_at_list[i] = (None, [ ])
|
|
|
|
if version < 3:
|
|
self.shown_window = False
|
|
|
|
if version < 4:
|
|
for k in self.layers:
|
|
self.layers[k] = [ SceneListEntry(*(i + (None,)) ) for i in self.layers[k] ]
|
|
|
|
self.additional_transient = [ ]
|
|
|
|
if version < 5:
|
|
self.drag_group = None
|
|
|
|
if version < 6:
|
|
self.shown = self.image_predict_info
|
|
|
|
if version < 7:
|
|
self.layer_transform = { }
|
|
|
|
def __init__(self, oldsl, shown):
|
|
|
|
super(SceneLists, self).__init__()
|
|
|
|
# Has a window been shown as part of these scene lists?
|
|
self.shown_window = False
|
|
|
|
# A map from layer name -> list(SceneListEntry)
|
|
self.layers = { }
|
|
|
|
# A map from layer name -> tag -> at_list associated with that tag.
|
|
self.at_list = { }
|
|
|
|
# A map from layer to (star time, at_list), where the at list has
|
|
# been applied to the layer as a whole.
|
|
self.layer_at_list = { }
|
|
|
|
# The current shown images,
|
|
self.shown = shown
|
|
|
|
# A list of (layer, tag) pairs that are considered to be
|
|
# transient.
|
|
self.additional_transient = [ ]
|
|
|
|
# Either None, or a DragGroup that's used as the default for
|
|
# drags with names.
|
|
self.drag_group = None
|
|
|
|
# A map from a layer to the transform that applies to that
|
|
# layer.
|
|
self.layer_transform = { }
|
|
|
|
if oldsl:
|
|
|
|
for i in renpy.config.layers + renpy.config.top_layers:
|
|
|
|
try:
|
|
self.layers[i] = oldsl.layers[i][:]
|
|
except KeyError:
|
|
self.layers[i] = [ ]
|
|
|
|
if i in oldsl.at_list:
|
|
self.at_list[i] = oldsl.at_list[i].copy()
|
|
self.layer_at_list[i] = oldsl.layer_at_list[i]
|
|
else:
|
|
self.at_list[i] = { }
|
|
self.layer_at_list[i] = (None, [ ])
|
|
|
|
for i in renpy.config.overlay_layers:
|
|
self.clear(i)
|
|
|
|
self.replace_transient(prefix=None)
|
|
|
|
self.focused = None
|
|
|
|
self.drag_group = oldsl.drag_group
|
|
|
|
self.layer_transform.update(oldsl.layer_transform)
|
|
|
|
else:
|
|
for i in renpy.config.layers + renpy.config.top_layers:
|
|
self.layers[i] = [ ]
|
|
self.at_list[i] = { }
|
|
self.layer_at_list[i] = (None, [ ])
|
|
|
|
self.music = None
|
|
self.focused = None
|
|
|
|
def replace_transient(self, prefix="hide"):
|
|
"""
|
|
Replaces the contents of the transient display list with
|
|
a copy of the master display list. This is used after a
|
|
scene is displayed to get rid of transitions and interface
|
|
elements.
|
|
|
|
`prefix`
|
|
The prefix/event to use. Set this to None to prevent the hide
|
|
from happening.
|
|
"""
|
|
|
|
for i in renpy.config.transient_layers:
|
|
self.clear(i, True)
|
|
|
|
for layer, tag in self.additional_transient:
|
|
self.remove(layer, tag, prefix=prefix)
|
|
|
|
self.additional_transient = [ ]
|
|
|
|
def transient_is_empty(self):
|
|
"""
|
|
This returns True if all transient layers are empty. This is
|
|
used by the rollback code, as we can't start a new rollback
|
|
if there is something in a transient layer (as things in the
|
|
transient layer may contain objects that cannot be pickled,
|
|
like lambdas.)
|
|
"""
|
|
|
|
for i in renpy.config.transient_layers:
|
|
if self.layers[i]:
|
|
return False
|
|
|
|
return True
|
|
|
|
def transform_state(self, old_thing, new_thing, execution=False):
|
|
"""
|
|
If the old thing is a transform, then move the state of that transform
|
|
to the new thing.
|
|
"""
|
|
|
|
if old_thing is None:
|
|
return new_thing
|
|
|
|
# Don't bother wrapping screens, as they can't be transformed.
|
|
if isinstance(new_thing, renpy.display.screen.ScreenDisplayable):
|
|
return new_thing
|
|
|
|
if renpy.config.take_state_from_target:
|
|
old_transform = old_thing._target()
|
|
else:
|
|
old_transform = old_thing
|
|
|
|
if not isinstance(old_transform, renpy.display.motion.Transform):
|
|
return new_thing
|
|
|
|
if renpy.config.take_state_from_target:
|
|
new_transform = new_thing._target()
|
|
else:
|
|
new_transform = new_thing
|
|
|
|
if not isinstance(new_transform, renpy.display.motion.Transform):
|
|
new_thing = new_transform = renpy.display.motion.Transform(child=new_thing)
|
|
|
|
new_transform.take_state(old_transform)
|
|
|
|
if execution:
|
|
new_transform.take_execution_state(old_transform)
|
|
|
|
return new_thing
|
|
|
|
def find_index(self, layer, tag, zorder, behind):
|
|
"""
|
|
This finds the spot in the named layer where we should insert the
|
|
displayable. It returns two things: an index at which the new thing
|
|
should be added, and an index at which the old thing should be hidden.
|
|
(Note that the indexes are relative to the current state of the list,
|
|
which may change on an add or remove.)
|
|
"""
|
|
|
|
add_index = None
|
|
remove_index = None
|
|
|
|
for i, sle in enumerate(self.layers[layer]):
|
|
|
|
if remove_index is None:
|
|
if (sle.tag and sle.tag == tag) or sle.displayable == tag:
|
|
remove_index = i
|
|
|
|
if zorder is None:
|
|
zorder = sle.zorder
|
|
|
|
if zorder is None:
|
|
zorder = renpy.config.tag_zorder.get(tag, 0)
|
|
|
|
for i, sle in enumerate(self.layers[layer]):
|
|
|
|
if add_index is None:
|
|
|
|
if sle.zorder == zorder:
|
|
if sle.tag and (sle.tag == tag or sle.tag in behind):
|
|
add_index = i
|
|
|
|
elif sle.zorder > zorder:
|
|
add_index = i
|
|
|
|
if add_index is None:
|
|
add_index = len(self.layers[layer])
|
|
|
|
return add_index, remove_index, zorder
|
|
|
|
def add(self,
|
|
layer,
|
|
thing,
|
|
key=None,
|
|
zorder=0,
|
|
behind=[ ],
|
|
at_list=[ ],
|
|
name=None,
|
|
atl=None,
|
|
default_transform=None,
|
|
transient=False,
|
|
keep_st=False):
|
|
"""
|
|
Adds something to this scene list. Some of these names are quite a bit
|
|
out of date.
|
|
|
|
`thing` - The displayable to add.
|
|
|
|
`key` - A string giving the tag associated with this thing.
|
|
|
|
`zorder` - Where to place this thing in the zorder, an integer
|
|
A greater value means closer to the user.
|
|
|
|
`behind` - A list of tags to place the thing behind.
|
|
|
|
`at_list` - The at_list associated with this
|
|
displayable. Counterintunitively, this is not actually
|
|
applied, but merely stored for future use.
|
|
|
|
`name` - The full name of the image being displayed. This is used for
|
|
image lookup.
|
|
|
|
`atl` - If not None, an atl block applied to the thing. (This actually is
|
|
applied here.)
|
|
|
|
`default_transform` - The default transform that is used to initialized
|
|
the values in the other transforms.
|
|
|
|
`keep_st`
|
|
If true, we preserve the shown time of a replaced displayable.
|
|
"""
|
|
|
|
if not isinstance(thing, Displayable):
|
|
raise Exception("Attempting to show something that isn't a displayable:" + repr(thing))
|
|
|
|
if layer not in self.layers:
|
|
raise Exception("Trying to add something to non-existent layer '%s'." % layer)
|
|
|
|
if key:
|
|
self.remove_hide_replaced(layer, key)
|
|
self.at_list[layer][key] = at_list
|
|
|
|
if key and name:
|
|
self.shown.predict_show(layer, name)
|
|
|
|
if transient:
|
|
self.additional_transient.append((layer, key))
|
|
|
|
l = self.layers[layer]
|
|
|
|
if atl:
|
|
thing = renpy.display.motion.ATLTransform(atl, child=thing)
|
|
|
|
add_index, remove_index, zorder = self.find_index(layer, key, zorder, behind)
|
|
|
|
at = None
|
|
st = None
|
|
|
|
if remove_index is not None:
|
|
sle = l[remove_index]
|
|
old = sle.displayable
|
|
|
|
at = sle.animation_time
|
|
|
|
if keep_st:
|
|
st = sle.show_time
|
|
|
|
if (not atl and
|
|
not at_list and
|
|
renpy.config.keep_running_transform and
|
|
isinstance(old, renpy.display.motion.Transform)):
|
|
|
|
thing = sle.displayable._change_transform_child(thing)
|
|
else:
|
|
thing = self.transform_state(l[remove_index].displayable, thing)
|
|
|
|
thing.set_transform_event("replace")
|
|
thing._show()
|
|
|
|
else:
|
|
|
|
if not isinstance(thing, renpy.display.motion.Transform):
|
|
thing = self.transform_state(default_transform, thing)
|
|
|
|
thing.set_transform_event("show")
|
|
thing._show()
|
|
|
|
sle = SceneListEntry(key, zorder, st, at, thing, name)
|
|
l.insert(add_index, sle)
|
|
|
|
if remove_index is not None:
|
|
if add_index <= remove_index:
|
|
remove_index += 1
|
|
|
|
self.hide_or_replace(layer, remove_index, "replaced")
|
|
|
|
def hide_or_replace(self, layer, index, prefix):
|
|
"""
|
|
Hides or replaces the scene list entry at the given
|
|
index. `prefix` is a prefix that is used if the entry
|
|
decides it doesn't want to be hidden quite yet.
|
|
"""
|
|
|
|
if index is None:
|
|
return
|
|
|
|
l = self.layers[layer]
|
|
oldsle = l[index]
|
|
|
|
now = get_time()
|
|
|
|
st = oldsle.show_time or now
|
|
at = oldsle.animation_time or now
|
|
|
|
if renpy.config.fast_unhandled_event:
|
|
if not oldsle.displayable._handles_event(prefix):
|
|
prefix = None
|
|
|
|
if (prefix is not None) and oldsle.tag:
|
|
|
|
d = oldsle.displayable._hide(now - st, now - at, prefix)
|
|
|
|
# _hide can mutate the layers, so we need to recompute
|
|
# index.
|
|
index = l.index(oldsle)
|
|
|
|
if d is not None:
|
|
|
|
sle = SceneListEntry(
|
|
prefix + "$" + oldsle.tag,
|
|
oldsle.zorder,
|
|
st,
|
|
at,
|
|
d,
|
|
None)
|
|
|
|
l[index] = sle
|
|
|
|
return
|
|
|
|
l.pop(index)
|
|
|
|
def get_all_displayables(self):
|
|
"""
|
|
Gets all displayables reachable from this scene list.
|
|
"""
|
|
|
|
rv = [ ]
|
|
for l in self.layers.itervalues():
|
|
for sle in l:
|
|
rv.append(sle.displayable)
|
|
|
|
return rv
|
|
|
|
def remove_above(self, layer, thing):
|
|
"""
|
|
Removes everything on the layer that is closer to the user
|
|
than thing, which may be either a tag or a displayable. Thing must
|
|
be displayed, or everything will be removed.
|
|
"""
|
|
|
|
for i in reversed(xrange(len(self.layers[layer]))):
|
|
|
|
sle = self.layers[layer][i]
|
|
|
|
if thing:
|
|
if sle.tag == thing or sle.displayable == thing:
|
|
break
|
|
|
|
if sle.tag and "$" in sle.tag:
|
|
continue
|
|
|
|
self.hide_or_replace(layer, i, "hide")
|
|
|
|
def remove(self, layer, thing, prefix="hide"):
|
|
"""
|
|
Thing is either a key or a displayable. This iterates through the
|
|
named layer, searching for entries matching the thing.
|
|
When they are found, they are removed from the displaylist.
|
|
|
|
It's not an error to remove something that isn't in the layer in
|
|
the first place.
|
|
"""
|
|
|
|
if layer not in self.layers:
|
|
raise Exception("Trying to remove something from non-existent layer '%s'." % layer)
|
|
|
|
_add_index, remove_index, _zorder = self.find_index(layer, thing, 0, [ ])
|
|
|
|
if remove_index is not None:
|
|
tag = self.layers[layer][remove_index].tag
|
|
|
|
if tag:
|
|
self.shown.predict_hide(layer, (tag,))
|
|
self.at_list[layer].pop(tag, None)
|
|
|
|
self.hide_or_replace(layer, remove_index, prefix)
|
|
|
|
def clear(self, layer, hide=False):
|
|
"""
|
|
Clears the named layer, making it empty.
|
|
|
|
If hide is True, then objects are hidden. Otherwise, they are
|
|
totally wiped out.
|
|
"""
|
|
|
|
if layer not in self.layers:
|
|
return
|
|
|
|
if not hide:
|
|
self.layers[layer] = [ ]
|
|
|
|
else:
|
|
|
|
# Have to iterate in reverse order, since otherwise
|
|
# the indexes might change.
|
|
for i in reversed(xrange(len(self.layers[layer]))):
|
|
self.hide_or_replace(layer, i, hide)
|
|
|
|
self.at_list[layer].clear()
|
|
self.shown.predict_scene(layer)
|
|
self.layer_at_list[layer] = (None, [ ])
|
|
|
|
def set_layer_at_list(self, layer, at_list, reset=True):
|
|
self.layer_at_list[layer] = (None, list(at_list))
|
|
|
|
if reset:
|
|
self.layer_transform[layer] = None
|
|
|
|
def set_times(self, time):
|
|
"""
|
|
This finds entries with a time of None, and replaces that
|
|
time with the given time.
|
|
"""
|
|
|
|
for l, (t, list) in self.layer_at_list.items(): # @ReservedAssignment
|
|
self.layer_at_list[l] = (t or time, list)
|
|
|
|
for l, ll in self.layers.iteritems():
|
|
self.layers[l] = [ i.update_time(time) for i in ll ]
|
|
|
|
def showing(self, layer, name):
|
|
"""
|
|
Returns true if something with the prefix of the given name
|
|
is found in the scene list.
|
|
"""
|
|
|
|
return self.shown.showing(layer, name)
|
|
|
|
def get_showing_tags(self, layer):
|
|
return self.shown.get_showing_tags(layer)
|
|
|
|
def get_sorted_tags(self, layer):
|
|
rv = [ ]
|
|
|
|
for sle in self.layers[layer]:
|
|
if not sle.tag:
|
|
continue
|
|
|
|
if "$" in sle.tag:
|
|
continue
|
|
|
|
rv.append(sle.tag)
|
|
|
|
return rv
|
|
|
|
def make_layer(self, layer, properties):
|
|
"""
|
|
Creates a Fixed with the given layer name and scene_list.
|
|
"""
|
|
|
|
rv = renpy.display.layout.MultiBox(layout='fixed', focus=layer, **properties)
|
|
rv.append_scene_list(self.layers[layer])
|
|
rv.layer_name = layer
|
|
rv._duplicatable = False
|
|
|
|
time, at_list = self.layer_at_list[layer]
|
|
|
|
old_transform = self.layer_transform.get(layer, None)
|
|
new_transform = None
|
|
|
|
if at_list:
|
|
|
|
for a in at_list:
|
|
|
|
if isinstance(a, renpy.display.motion.Transform):
|
|
rv = a(child=rv)
|
|
new_transform = rv
|
|
else:
|
|
rv = a(rv)
|
|
|
|
if (new_transform is not None) and (renpy.config.keep_show_layer_state):
|
|
self.transform_state(old_transform, new_transform, execution=True)
|
|
|
|
f = renpy.display.layout.MultiBox(layout='fixed')
|
|
f.add(rv, time, time)
|
|
f.layer_name = layer
|
|
|
|
rv = f
|
|
|
|
self.layer_transform[layer] = new_transform
|
|
|
|
return rv
|
|
|
|
def remove_hide_replaced(self, layer, tag):
|
|
"""
|
|
Removes things that are hiding or replaced, that have the given
|
|
tag.
|
|
"""
|
|
|
|
hide_tag = "hide$" + tag
|
|
replaced_tag = "replaced$" + tag
|
|
|
|
l = self.layers[layer]
|
|
self.layers[layer] = [ i for i in l if i.tag != hide_tag and i.tag != replaced_tag ]
|
|
|
|
def remove_hidden(self):
|
|
"""
|
|
Goes through all of the layers, and removes things that are
|
|
hidden and are no longer being kept alive by their hide
|
|
methods.
|
|
"""
|
|
|
|
now = get_time()
|
|
|
|
for l in self.layers:
|
|
newl = [ ]
|
|
|
|
for sle in self.layers[l]:
|
|
|
|
if sle.tag:
|
|
|
|
if sle.tag.startswith("hide$"):
|
|
d = sle.displayable._hide(now - sle.show_time, now - sle.animation_time, "hide")
|
|
if not d:
|
|
continue
|
|
|
|
elif sle.tag.startswith("replaced$"):
|
|
d = sle.displayable._hide(now - sle.show_time, now - sle.animation_time, "replaced")
|
|
if not d:
|
|
continue
|
|
|
|
newl.append(sle)
|
|
|
|
self.layers[l] = newl
|
|
|
|
def remove_all_hidden(self):
|
|
"""
|
|
Removes everything hidden, even if it's not time yet. (Used when making a rollback copy).
|
|
"""
|
|
|
|
for l in self.layers:
|
|
newl = [ ]
|
|
|
|
for sle in self.layers[l]:
|
|
|
|
if sle.tag:
|
|
|
|
if "$" in sle.tag:
|
|
continue
|
|
|
|
newl.append(sle)
|
|
|
|
self.layers[l] = newl
|
|
|
|
def get_displayable_by_tag(self, layer, tag):
|
|
"""
|
|
Returns the displayable on the layer with the given tag, or None
|
|
if no such displayable exists. Note that this will usually return
|
|
a Transform.
|
|
"""
|
|
|
|
if layer not in self.layers:
|
|
raise Exception("Unknown layer %r." % layer)
|
|
|
|
for sle in self.layers[layer]:
|
|
if sle.tag == tag:
|
|
return sle.displayable
|
|
|
|
return None
|
|
|
|
def get_displayable_by_name(self, layer, name):
|
|
"""
|
|
Returns the displayable on the layer with the given name, or None
|
|
if no such displayable exists. Note that this will usually return
|
|
a Transform.
|
|
"""
|
|
|
|
if layer not in self.layers:
|
|
raise Exception("Unknown layer %r." % layer)
|
|
|
|
for sle in self.layers[layer]:
|
|
if sle.name == name:
|
|
return sle.displayable
|
|
|
|
return None
|
|
|
|
def get_image_bounds(self, layer, tag, width, height):
|
|
"""
|
|
Implements renpy.get_image_bounds().
|
|
"""
|
|
|
|
if layer not in self.layers:
|
|
raise Exception("Unknown layer %r." % layer)
|
|
|
|
for sle in self.layers[layer]:
|
|
if sle.tag == tag:
|
|
break
|
|
else:
|
|
return None
|
|
|
|
now = get_time()
|
|
|
|
if sle.show_time is not None:
|
|
st = now - sle.show_time
|
|
else:
|
|
st = 0
|
|
|
|
if sle.animation_time is not None:
|
|
at = now - sle.animation_time
|
|
else:
|
|
at = 0
|
|
|
|
surf = renpy.display.render.render_for_size(sle.displayable, width, height, st, at)
|
|
|
|
sw = surf.width
|
|
sh = surf.height
|
|
|
|
x, y = place(width, height, sw, sh, sle.displayable.get_placement())
|
|
|
|
return (x, y, sw, sh)
|
|
|
|
|
|
def scene_lists(index=-1):
|
|
"""
|
|
Returns either the current scenelists object, or the one for the
|
|
context at the given index.
|
|
"""
|
|
|
|
return renpy.game.context(index).scene_lists
|
|
|
|
|
|
class MouseMove(object):
|
|
"""
|
|
This contains information about the current mouse move.
|
|
"""
|
|
|
|
def __init__(self, x, y, duration):
|
|
self.start = get_time()
|
|
|
|
if duration is not None:
|
|
self.duration = duration
|
|
else:
|
|
self.duration = 0
|
|
|
|
self.start_x, self.start_y = renpy.display.draw.get_mouse_pos()
|
|
|
|
self.end_x = x
|
|
self.end_y = y
|
|
|
|
def perform(self):
|
|
"""
|
|
Performs the mouse move. Returns True if this should be called
|
|
again, or False if the move has finished.
|
|
"""
|
|
|
|
elapsed = get_time() - self.start
|
|
|
|
if elapsed >= self.duration:
|
|
renpy.display.draw.set_mouse_pos(self.end_x, self.end_y)
|
|
return False
|
|
|
|
done = 1.0 * elapsed / self.duration
|
|
|
|
x = int(self.start_x + done * (self.end_x - self.start_x))
|
|
y = int(self.start_y + done * (self.end_y - self.start_y))
|
|
|
|
renpy.display.draw.set_mouse_pos(x, y)
|
|
return True
|
|
|
|
|
|
def get_safe_mode():
|
|
"""
|
|
Returns true if we should go into safe mode.
|
|
"""
|
|
|
|
if renpy.safe_mode_checked:
|
|
return False
|
|
|
|
try:
|
|
if renpy.windows:
|
|
import ctypes
|
|
|
|
VK_SHIFT = 0x10
|
|
|
|
ctypes.windll.user32.GetKeyState.restype = ctypes.c_ushort
|
|
if ctypes.windll.user32.GetKeyState(VK_SHIFT) & 0x8000:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
# Safe mode doesn't work on other platforms.
|
|
return False
|
|
|
|
except:
|
|
return False
|
|
|
|
|
|
# How long should we be in maximum framerate mode at the start of the game?
|
|
initial_maximum_framerate = 0.0
|
|
|
|
|
|
class Interface(object):
|
|
"""
|
|
This represents the user interface that interacts with the user.
|
|
It manages the Display objects that display things to the user, and
|
|
also handles accepting and responding to user input.
|
|
|
|
@ivar display: The display that we used to display the screen.
|
|
|
|
@ivar profile_time: The time of the last profiling.
|
|
|
|
@ivar screenshot: A screenshot, or None if no screenshot has been
|
|
taken.
|
|
|
|
@ivar old_scene: The last thing that was displayed to the screen.
|
|
|
|
@ivar transition: A map from layer name to the transition that will
|
|
be applied the next time interact restarts.
|
|
|
|
@ivar transition_time: A map from layer name to the time the transition
|
|
involving that layer started.
|
|
|
|
@ivar transition_from: A map from layer name to the scene that we're
|
|
transitioning from on that layer.
|
|
|
|
@ivar suppress_transition: If True, then the next transition will not
|
|
happen.
|
|
|
|
@ivar force_redraw: If True, a redraw is forced.
|
|
|
|
@ivar restart_interaction: If True, the current interaction will
|
|
be restarted.
|
|
|
|
@ivar pushed_event: If not None, an event that was pushed back
|
|
onto the stack.
|
|
|
|
@ivar mouse: The name of the mouse cursor to use during the current
|
|
interaction.
|
|
|
|
@ivar ticks: The number of 20hz ticks.
|
|
|
|
@ivar frame_time: The time at which we began drawing this frame.
|
|
|
|
@ivar interact_time: The time of the start of the first frame of the current interact_core.
|
|
|
|
@ivar time_event: A singleton ignored event.
|
|
|
|
@ivar event_time: The time of the current event.
|
|
|
|
@ivar timeout_time: The time at which the timeout will occur.
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
# PNG data and the surface for the current file screenshot.
|
|
self.screenshot = None
|
|
self.screenshot_surface = None
|
|
|
|
self.old_scene = { }
|
|
self.transition = { }
|
|
self.ongoing_transition = { }
|
|
self.transition_time = { }
|
|
self.transition_from = { }
|
|
self.suppress_transition = False
|
|
self.quick_quit = False
|
|
self.force_redraw = False
|
|
self.restart_interaction = False
|
|
self.pushed_event = None
|
|
self.ticks = 0
|
|
self.mouse = 'default'
|
|
self.timeout_time = None
|
|
self.last_event = None
|
|
self.current_context = None
|
|
self.roll_forward = None
|
|
|
|
# Things to be preloaded.
|
|
self.preloads = [ ]
|
|
|
|
# The time at which this draw occurs.
|
|
self.frame_time = 0
|
|
|
|
# The time when this interaction occured.
|
|
self.interact_time = None
|
|
|
|
# The time we last tried to quit.
|
|
self.quit_time = 0
|
|
|
|
# Are we currently processing the quit event?
|
|
self.in_quit_event = False
|
|
|
|
self.time_event = pygame.event.Event(TIMEEVENT)
|
|
self.redraw_event = pygame.event.Event(REDRAW)
|
|
|
|
# Are we focused?
|
|
self.mouse_focused = True
|
|
self.keyboard_focused = True
|
|
|
|
# Properties for each layer.
|
|
self.layer_properties = { }
|
|
|
|
# Have we shown the window this interaction?
|
|
self.shown_window = False
|
|
|
|
# Are we in fullscren mode?
|
|
self.fullscreen = False
|
|
|
|
# Should we ignore the rest of the current touch? Used to ignore the
|
|
# rest of a mousepress after a longpress occurs.
|
|
self.ignore_touch = False
|
|
|
|
# Should we clear the screenshot at the start of the next interaction?
|
|
self.clear_screenshot = False
|
|
|
|
for layer in renpy.config.layers + renpy.config.top_layers:
|
|
if layer in renpy.config.layer_clipping:
|
|
x, y, w, h = renpy.config.layer_clipping[layer]
|
|
self.layer_properties[layer] = dict(
|
|
xpos=x,
|
|
xanchor=0,
|
|
ypos=y,
|
|
yanchor=0,
|
|
xmaximum=w,
|
|
ymaximum=h,
|
|
xminimum=w,
|
|
yminimum=h,
|
|
clipping=True,
|
|
)
|
|
|
|
else:
|
|
self.layer_properties[layer] = dict()
|
|
|
|
# A stack giving the values of self.transition and self.transition_time
|
|
# for contexts outside the current one. This is used to restore those
|
|
# in the case where nothing has changed in the new context.
|
|
self.transition_info_stack = [ ]
|
|
|
|
# The time when the event was dispatched.
|
|
self.event_time = 0
|
|
|
|
# The time we saw the last mouse event.
|
|
self.mouse_event_time = None
|
|
|
|
# Should we show the mouse?
|
|
self.show_mouse = True
|
|
|
|
# Should we reset the display?
|
|
self.display_reset = False
|
|
|
|
# The last size we were resized to.
|
|
self.last_resize = None
|
|
|
|
# The thread that can do display operations.
|
|
self.thread = threading.current_thread()
|
|
|
|
# Initialize audio.
|
|
renpy.audio.audio.init()
|
|
|
|
# Initialize pygame.
|
|
try:
|
|
pygame.display.init()
|
|
except:
|
|
pass
|
|
|
|
# Init timing.
|
|
init_time()
|
|
self.mouse_event_time = get_time()
|
|
|
|
# The current window caption.
|
|
self.window_caption = None
|
|
|
|
renpy.game.interface = self
|
|
renpy.display.interface = self
|
|
|
|
# Are we in safe mode, from holding down shift at start?
|
|
self.safe_mode = False
|
|
|
|
# Do we need a background screenshot?
|
|
self.bgscreenshot_needed = False
|
|
|
|
# Event used to signal background screenshot taken.
|
|
self.bgscreenshot_event = threading.Event()
|
|
|
|
# The background screenshot surface.
|
|
self.bgscreenshot_surface = None
|
|
|
|
# Mouse move. If not None, information about the current mouse
|
|
# move.
|
|
self.mouse_move = None
|
|
|
|
# If in text editing mode, the current text editing event.
|
|
self.text_editing = None
|
|
|
|
# The text rectangle after the current draw.
|
|
self.text_rect = None
|
|
|
|
# The text rectangle after the previous draw.
|
|
self.old_text_rect = None
|
|
|
|
# Are we a touchscreen?
|
|
self.touch = renpy.exports.variant("touch")
|
|
|
|
# Should we restart the interaction?
|
|
self.restart_interaction = True
|
|
|
|
# For compatibility with older code.
|
|
if renpy.config.periodic_callback:
|
|
renpy.config.periodic_callbacks.append(renpy.config.periodic_callback)
|
|
|
|
renpy.display.emulator.init_emulator()
|
|
|
|
# Has start been called?
|
|
self.started = False
|
|
|
|
# Are we in fullscreen video mode?
|
|
self.fullscreen_video = False
|
|
|
|
self.safe_mode = get_safe_mode()
|
|
renpy.safe_mode_checked = True
|
|
|
|
# A scale factor used to compensate for the system DPI.
|
|
self.dpi_scale = self.setup_dpi_scaling()
|
|
|
|
renpy.display.log.write("DPI scale factor: %f", self.dpi_scale)
|
|
|
|
# A time until which we should draw at maximum framerate.
|
|
self.maximum_framerate_time = 0.0
|
|
self.maximum_framerate(initial_maximum_framerate)
|
|
|
|
# True if this is the first interact.
|
|
self.start_interact = True
|
|
|
|
# The time of each frame.
|
|
self.frame_times = [ ]
|
|
|
|
# The duration of each frame, in seconds.
|
|
self.frame_duration = 1.0 / 60.0
|
|
|
|
def setup_dpi_scaling(self):
|
|
|
|
if "RENPY_HIGHDPI" in os.environ:
|
|
return float(os.environ["RENPY_HIGHDPI"])
|
|
|
|
if not renpy.windows:
|
|
return 1.0
|
|
|
|
try:
|
|
import ctypes
|
|
from ctypes import c_void_p, c_int
|
|
|
|
ctypes.windll.user32.SetProcessDPIAware()
|
|
|
|
GetDC = ctypes.windll.user32.GetDC
|
|
GetDC.restype = c_void_p
|
|
GetDC.argtypes = [ c_void_p ]
|
|
|
|
ReleaseDC = ctypes.windll.user32.ReleaseDC
|
|
ReleaseDC.argtypes = [ c_void_p, c_void_p ]
|
|
|
|
GetDeviceCaps = ctypes.windll.gdi32.GetDeviceCaps
|
|
GetDeviceCaps.restype = c_int
|
|
GetDeviceCaps.argtypes = [ c_void_p, c_int ]
|
|
|
|
LOGPIXELSX = 88
|
|
|
|
dc = GetDC(None)
|
|
rv = GetDeviceCaps(dc, LOGPIXELSX) / 96.0
|
|
ReleaseDC(None, dc)
|
|
|
|
if rv < renpy.config.de_minimus_dpi_scale:
|
|
renpy.display.log.write("De minimus DPI scale, was %r", rv)
|
|
rv = 1.0
|
|
|
|
return rv
|
|
|
|
except:
|
|
renpy.display.log.write("Could not determine DPI scale factor:")
|
|
renpy.display.log.exception()
|
|
return 1.0
|
|
|
|
def start(self):
|
|
"""
|
|
Starts the interface, by opening a window and setting the mode.
|
|
"""
|
|
|
|
if self.started:
|
|
return
|
|
|
|
gc.collect()
|
|
|
|
if gc.garbage:
|
|
gc.garbage[:] = [ ]
|
|
|
|
renpy.display.render.render_ready()
|
|
|
|
# Kill off the presplash.
|
|
renpy.display.presplash.end()
|
|
|
|
renpy.main.log_clock("Interface start")
|
|
|
|
self.started = True
|
|
|
|
self.set_mode()
|
|
|
|
# Load the image fonts.
|
|
renpy.text.font.load_fonts()
|
|
|
|
# Setup periodic event.
|
|
pygame.time.set_timer(PERIODIC, PERIODIC_INTERVAL)
|
|
|
|
# Don't grab the screen.
|
|
pygame.event.set_grab(False)
|
|
|
|
if not self.safe_mode:
|
|
renpy.display.controller.init()
|
|
|
|
s = "Total time until interface ready: {}s".format(time.time() - import_time)
|
|
|
|
renpy.display.log.write(s)
|
|
|
|
if renpy.android and not renpy.config.log_to_stdout:
|
|
print(s)
|
|
|
|
def post_init(self):
|
|
"""
|
|
This is called after display init, but before the window is created.
|
|
"""
|
|
|
|
pygame.display.hint("SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS", "0")
|
|
|
|
# Needed for Unity.
|
|
wmclass = renpy.config.save_directory or os.path.basename(sys.argv[0])
|
|
os.environ[b'SDL_VIDEO_X11_WMCLASS'] = wmclass.encode("utf-8")
|
|
|
|
self.set_window_caption(force=True)
|
|
self.set_icon()
|
|
|
|
if renpy.config.key_repeat is not None:
|
|
delay, repeat_delay = renpy.config.key_repeat
|
|
pygame.key.set_repeat(int(1000 * delay), int(1000 * repeat_delay))
|
|
|
|
if android:
|
|
android.wakelock(True)
|
|
|
|
# Block events we don't use.
|
|
for i in pygame.event.get_standard_events():
|
|
|
|
if i in enabled_events:
|
|
continue
|
|
|
|
if i in renpy.config.pygame_events:
|
|
continue
|
|
|
|
pygame.event.set_blocked(i)
|
|
|
|
def set_icon(self):
|
|
"""
|
|
This is called to set up the window icon.
|
|
"""
|
|
|
|
# Window icon.
|
|
icon = renpy.config.window_icon
|
|
|
|
if icon:
|
|
|
|
im = renpy.display.scale.image_load_unscaled(
|
|
renpy.loader.load(icon),
|
|
icon,
|
|
)
|
|
|
|
# Convert the aspect ratio to be square.
|
|
iw, ih = im.get_size()
|
|
imax = max(iw, ih)
|
|
square_im = renpy.display.pgrender.surface_unscaled((imax, imax), True)
|
|
square_im.blit(im, ( (imax-iw)/2, (imax-ih)/2 ))
|
|
im = square_im
|
|
|
|
pygame.display.set_icon(im)
|
|
|
|
def set_window_caption(self, force=False):
|
|
|
|
window_title = renpy.config.window_title
|
|
|
|
if window_title is None:
|
|
window_title = "A Ren'Py Game"
|
|
|
|
caption = renpy.translation.translate_string(window_title) + renpy.store._window_subtitle
|
|
|
|
if renpy.exports.get_autoreload():
|
|
caption += " - autoreload"
|
|
|
|
if not force and caption == self.window_caption:
|
|
return
|
|
|
|
self.window_caption = caption
|
|
pygame.display.set_caption(caption.encode("utf-8"))
|
|
|
|
def iconify(self):
|
|
pygame.display.iconify()
|
|
|
|
def get_draw_constructors(self):
|
|
"""
|
|
Figures out the list of draw constructors to try.
|
|
"""
|
|
|
|
renderer = renpy.game.preferences.renderer
|
|
renderer = os.environ.get("RENPY_RENDERER", renderer)
|
|
|
|
if self.safe_mode:
|
|
renderer = "sw"
|
|
|
|
if (renderer == "angle") and (not renpy.windows):
|
|
renderer = "auto"
|
|
|
|
renpy.config.renderer = renderer
|
|
|
|
if renderer == "auto":
|
|
if renpy.windows:
|
|
renderers = [ "gl", "angle", "sw" ]
|
|
else:
|
|
renderers = [ "gl", "sw" ]
|
|
|
|
if renpy.config.gl2:
|
|
renderers = [ "gl2", "egl2" ] + renderers
|
|
|
|
else:
|
|
renderers = [ renderer, "sw" ]
|
|
|
|
draw_objects = { }
|
|
|
|
def make_draw(name, mod, cls, *args):
|
|
|
|
if name not in renderers:
|
|
return False
|
|
|
|
try:
|
|
__import__(mod)
|
|
module = sys.modules[mod]
|
|
draw_class = getattr(module, cls)
|
|
draw_objects[name] = draw_class(*args)
|
|
return True
|
|
|
|
except:
|
|
renpy.display.log.write("Couldn't import {0} renderer:".format(name))
|
|
renpy.display.log.exception()
|
|
|
|
return False
|
|
|
|
if renpy.windows:
|
|
has_angle = make_draw("angle", "renpy.angle.gldraw", "GLDraw")
|
|
else:
|
|
has_angle = False
|
|
|
|
make_draw("gl", "renpy.gl.gldraw", "GLDraw", not has_angle)
|
|
make_draw("gl2", "renpy.gl2.gl2draw", "GL2Draw", "gl2", False)
|
|
make_draw("gles2", "renpy.gl2.gl2draw", "GL2Draw", "gles2", True)
|
|
make_draw("sw", "renpy.display.swdraw", "SWDraw")
|
|
|
|
rv = [ ]
|
|
|
|
def append_draw(name):
|
|
if name in draw_objects:
|
|
rv.append(draw_objects[name])
|
|
else:
|
|
renpy.display.log.write("Unknown renderer: {0}".format(name))
|
|
|
|
for i in renderers:
|
|
append_draw(i)
|
|
|
|
return rv
|
|
|
|
def kill_textures(self):
|
|
if renpy.display.draw is not None:
|
|
renpy.display.draw.kill_textures()
|
|
|
|
renpy.display.render.free_memory()
|
|
renpy.text.text.layout_cache_clear()
|
|
|
|
def kill_textures_and_surfaces(self):
|
|
"""
|
|
Kill all textures and surfaces that are loaded.
|
|
"""
|
|
|
|
self.kill_textures()
|
|
renpy.display.im.cache.clear()
|
|
renpy.display.module.bo_cache = None
|
|
|
|
def set_mode(self, physical_size=None):
|
|
"""
|
|
This sets the video mode. It also picks the draw object.
|
|
"""
|
|
|
|
# Ensure that we kill off the movie when changing screen res.
|
|
if renpy.display.draw and renpy.display.draw.info["renderer"] == "sw":
|
|
renpy.display.video.movie_stop(clear=False)
|
|
|
|
renpy.display.render.free_memory()
|
|
renpy.text.text.layout_cache_clear()
|
|
renpy.display.module.bo_cache = None
|
|
|
|
if self.display_reset:
|
|
|
|
pygame.key.stop_text_input() # @UndefinedVariable
|
|
pygame.key.set_text_input_rect(None) # @UndefinedVariable
|
|
self.text_rect = None
|
|
|
|
if renpy.display.draw.info["renderer"] == "angle":
|
|
renpy.display.draw.quit()
|
|
|
|
# This is necessary to fix a bug with restoring a window from
|
|
# minimized state on windows.
|
|
pygame.display.quit()
|
|
|
|
self.kill_textures_and_surfaces()
|
|
|
|
self.old_text_rect = None
|
|
self.display_reset = False
|
|
|
|
virtual_size = (renpy.config.screen_width, renpy.config.screen_height)
|
|
|
|
if physical_size is None:
|
|
if renpy.mobile or renpy.game.preferences.physical_size is None: # @UndefinedVariable
|
|
physical_size = (None, None)
|
|
else:
|
|
physical_size = renpy.game.preferences.physical_size
|
|
|
|
# Setup screen.
|
|
fullscreen = renpy.game.preferences.fullscreen
|
|
|
|
old_fullscreen = self.fullscreen
|
|
self.fullscreen = fullscreen
|
|
|
|
if os.environ.get('RENPY_DISABLE_FULLSCREEN', False):
|
|
fullscreen = False
|
|
self.fullscreen = renpy.game.preferences.fullscreen
|
|
|
|
if renpy.display.draw:
|
|
draws = [ renpy.display.draw ]
|
|
else:
|
|
draws = self.get_draw_constructors()
|
|
|
|
for draw in draws:
|
|
if draw.set_mode(virtual_size, physical_size, fullscreen):
|
|
renpy.display.draw = draw
|
|
renpy.display.render.models = draw.info.get("models", False)
|
|
break
|
|
else:
|
|
# pygame.display.quit()
|
|
pass
|
|
else:
|
|
# Ensure we don't get stuck in fullscreen.
|
|
renpy.game.preferences.fullscreen = False
|
|
raise Exception("Could not set video mode.")
|
|
|
|
# Save the video size.
|
|
if renpy.config.save_physical_size and not fullscreen and not old_fullscreen:
|
|
renpy.game.preferences.physical_size = renpy.display.draw.get_physical_size()
|
|
|
|
if android:
|
|
android.init()
|
|
|
|
# We need to redraw the (now blank) screen.
|
|
self.force_redraw = True
|
|
|
|
# Assume we have focus until told otherwise.
|
|
self.mouse_focused = True
|
|
self.keyboard_focused = True
|
|
|
|
# Assume we're not minimized.
|
|
self.minimized = False
|
|
|
|
# Force an interaction restart.
|
|
self.restart_interaction = True
|
|
|
|
# True if we're doing a one-time profile.
|
|
self.profile_once = False
|
|
|
|
# Clear the frame times.
|
|
self.frame_times = [ ]
|
|
|
|
def draw_screen(self, root_widget, fullscreen_video, draw):
|
|
|
|
try:
|
|
renpy.display.render.per_frame = True
|
|
renpy.display.screen.per_frame()
|
|
finally:
|
|
renpy.display.render.per_frame = False
|
|
|
|
surftree = renpy.display.render.render_screen(
|
|
root_widget,
|
|
renpy.config.screen_width,
|
|
renpy.config.screen_height,
|
|
)
|
|
|
|
if draw:
|
|
renpy.display.draw.draw_screen(surftree, fullscreen_video)
|
|
|
|
now = time.time()
|
|
|
|
self.frame_times.append(now)
|
|
|
|
while (now - self.frame_times[0]) > renpy.config.performance_window:
|
|
self.frame_times.pop(0)
|
|
|
|
renpy.display.render.mark_sweep()
|
|
renpy.display.focus.take_focuses()
|
|
|
|
self.surftree = surftree
|
|
self.fullscreen_video = fullscreen_video
|
|
|
|
def take_screenshot(self, scale, background=False):
|
|
"""
|
|
This takes a screenshot of the current screen, and stores it so
|
|
that it can gotten using get_screenshot()
|
|
|
|
`background`
|
|
If true, we're in a background thread. So queue the request
|
|
until it can be handled by the main thread.
|
|
"""
|
|
|
|
self.clear_screenshot = False
|
|
|
|
# Do nothing before the first interaction.
|
|
if not self.started:
|
|
return
|
|
|
|
if background and not renpy.emscripten:
|
|
self.bgscreenshot_event.clear()
|
|
self.bgscreenshot_needed = True
|
|
|
|
if not self.bgscreenshot_event.wait(1.0):
|
|
raise Exception("Screenshot timed out.")
|
|
|
|
surf = self.bgscreenshot_surface
|
|
self.bgscreenshot_surface = None
|
|
|
|
else:
|
|
|
|
surf = renpy.display.draw.screenshot(self.surftree, self.fullscreen_video)
|
|
|
|
surf = renpy.display.scale.smoothscale(surf, scale)
|
|
|
|
renpy.display.render.mutated_surface(surf)
|
|
|
|
self.screenshot_surface = surf
|
|
|
|
sio = cStringIO.StringIO()
|
|
renpy.display.module.save_png(surf, sio, 0)
|
|
self.screenshot = sio.getvalue()
|
|
sio.close()
|
|
|
|
def check_background_screenshot(self):
|
|
"""
|
|
Handles requests for a background screenshot.
|
|
"""
|
|
|
|
if self.bgscreenshot_needed:
|
|
self.bgscreenshot_needed = False
|
|
self.bgscreenshot_surface = renpy.display.draw.screenshot(self.surftree, self.fullscreen_video)
|
|
self.bgscreenshot_event.set()
|
|
|
|
def get_screenshot(self):
|
|
"""
|
|
Gets the current screenshot, as a string. Returns None if there isn't
|
|
a current screenshot.
|
|
"""
|
|
|
|
if not self.started:
|
|
self.start()
|
|
|
|
rv = self.screenshot
|
|
|
|
if not rv:
|
|
self.take_screenshot(
|
|
(renpy.config.thumbnail_width, renpy.config.thumbnail_height),
|
|
background=(threading.current_thread() is not self.thread),
|
|
)
|
|
rv = self.screenshot
|
|
self.lose_screenshot()
|
|
|
|
return rv
|
|
|
|
def lose_screenshot(self):
|
|
"""
|
|
This deallocates the saved screenshot.
|
|
"""
|
|
|
|
self.screenshot = None
|
|
self.screenshot_surface = None
|
|
|
|
def save_screenshot(self, filename):
|
|
"""
|
|
Saves a full-size screenshot in the given filename.
|
|
"""
|
|
|
|
window = renpy.display.draw.screenshot(self.surftree, self.fullscreen_video)
|
|
|
|
if renpy.config.screenshot_crop:
|
|
window = window.subsurface(renpy.config.screenshot_crop)
|
|
|
|
try:
|
|
renpy.display.scale.image_save_unscaled(window, filename)
|
|
if renpy.emscripten:
|
|
import emscripten
|
|
emscripten.run_script(r'''FSDownload('%s')''' % filename)
|
|
return True
|
|
except:
|
|
if renpy.config.debug:
|
|
raise
|
|
|
|
return False
|
|
|
|
def show_window(self):
|
|
|
|
if not renpy.store._window:
|
|
return
|
|
|
|
if not renpy.game.preferences.show_empty_window:
|
|
return
|
|
|
|
if renpy.game.context().scene_lists.shown_window:
|
|
return
|
|
|
|
if renpy.config.empty_window:
|
|
|
|
old_history = renpy.store._history # @UndefinedVariable
|
|
renpy.store._history = False
|
|
|
|
PPP("empty window")
|
|
|
|
try:
|
|
renpy.config.empty_window()
|
|
finally:
|
|
renpy.store._history = old_history
|
|
|
|
def do_with(self, trans, paired, clear=False):
|
|
|
|
if renpy.config.with_callback:
|
|
trans = renpy.config.with_callback(trans, paired)
|
|
|
|
if (not trans) or self.suppress_transition:
|
|
self.with_none()
|
|
return False
|
|
else:
|
|
self.set_transition(trans)
|
|
return self.interact(trans_pause=True,
|
|
suppress_overlay=not renpy.config.overlay_during_with,
|
|
mouse='with',
|
|
clear=clear)
|
|
|
|
def with_none(self, overlay=True):
|
|
"""
|
|
Implements the with None command, which sets the scene we will
|
|
be transitioning from.
|
|
"""
|
|
|
|
PPP("start of with none")
|
|
|
|
renpy.game.context().say_attributes = None
|
|
|
|
# Show the window, if that's necessary.
|
|
self.show_window()
|
|
|
|
# Compute the overlay.
|
|
if overlay:
|
|
self.compute_overlay()
|
|
|
|
scene_lists = renpy.game.context().scene_lists
|
|
|
|
# Compute the scene.
|
|
for layer, d in self.compute_scene(scene_lists).iteritems():
|
|
if layer not in self.transition:
|
|
self.old_scene[layer] = d
|
|
|
|
# Get rid of transient things.
|
|
for i in renpy.config.overlay_layers:
|
|
scene_lists.clear(i)
|
|
|
|
scene_lists.replace_transient()
|
|
scene_lists.shown_window = False
|
|
|
|
if renpy.store._side_image_attributes_reset:
|
|
renpy.store._side_image_attributes = None
|
|
renpy.store._side_image_attributes_reset = False
|
|
|
|
def set_transition(self, transition, layer=None, force=False):
|
|
"""
|
|
Sets the transition that will be performed as part of the next
|
|
interaction.
|
|
"""
|
|
|
|
if self.suppress_transition and not force:
|
|
return
|
|
|
|
if transition is None:
|
|
self.transition.pop(layer, None)
|
|
else:
|
|
self.transition[layer] = transition
|
|
|
|
def event_peek(self):
|
|
"""
|
|
This peeks the next event. It returns None if no event exists.
|
|
"""
|
|
|
|
if self.pushed_event:
|
|
return self.pushed_event
|
|
|
|
ev = pygame.event.poll()
|
|
|
|
if ev.type == pygame.NOEVENT:
|
|
self.check_background_screenshot()
|
|
# Seems to prevent the CPU from speeding up.
|
|
renpy.display.draw.event_peek_sleep()
|
|
return None
|
|
|
|
self.pushed_event = ev
|
|
|
|
return ev
|
|
|
|
def event_poll(self):
|
|
"""
|
|
Called to busy-wait for an event while we're waiting to
|
|
redraw a frame.
|
|
"""
|
|
|
|
if self.pushed_event:
|
|
rv = self.pushed_event
|
|
self.pushed_event = None
|
|
else:
|
|
rv = pygame.event.poll()
|
|
|
|
self.last_event = rv
|
|
|
|
return rv
|
|
|
|
def event_wait(self):
|
|
"""
|
|
This is in its own function so that we can track in the
|
|
profiler how much time is spent in interact.
|
|
"""
|
|
|
|
if self.pushed_event:
|
|
rv = self.pushed_event
|
|
self.pushed_event = None
|
|
self.last_event = rv
|
|
return rv
|
|
|
|
self.check_background_screenshot()
|
|
|
|
ev = pygame.event.wait()
|
|
self.last_event = ev
|
|
|
|
return ev
|
|
|
|
def compute_overlay(self):
|
|
|
|
if renpy.store.suppress_overlay:
|
|
return
|
|
|
|
# Figure out what the overlay layer should look like.
|
|
renpy.ui.layer("overlay")
|
|
|
|
for i in renpy.config.overlay_functions:
|
|
i()
|
|
|
|
if renpy.game.context().scene_lists.shown_window:
|
|
for i in renpy.config.window_overlay_functions:
|
|
i()
|
|
|
|
renpy.ui.close()
|
|
|
|
def compute_scene(self, scene_lists):
|
|
"""
|
|
This converts scene lists into a dictionary mapping layer
|
|
name to a Fixed containing that layer.
|
|
"""
|
|
|
|
rv = { }
|
|
|
|
for layer in renpy.config.layers + renpy.config.top_layers:
|
|
rv[layer] = scene_lists.make_layer(layer, self.layer_properties[layer])
|
|
|
|
root = renpy.display.layout.MultiBox(layout='fixed')
|
|
root.layers = { }
|
|
|
|
for layer in renpy.config.layers:
|
|
root.layers[layer] = rv[layer]
|
|
root.add(rv[layer])
|
|
rv[None] = root
|
|
|
|
return rv
|
|
|
|
def quit_event(self):
|
|
"""
|
|
This is called to handle the user invoking a quit.
|
|
"""
|
|
|
|
if self.screenshot is None:
|
|
renpy.exports.take_screenshot()
|
|
|
|
if self.quit_time > (time.time() - .75):
|
|
renpy.exports.quit(save=True)
|
|
|
|
if self.in_quit_event:
|
|
renpy.exports.quit(save=True)
|
|
|
|
if renpy.config.quit_action is not None:
|
|
self.quit_time = time.time()
|
|
|
|
# Make the screen more suitable for interactions.
|
|
renpy.exports.movie_stop(only_fullscreen=True)
|
|
renpy.store.mouse_visible = True
|
|
|
|
try:
|
|
self.in_quit_event = True
|
|
renpy.display.behavior.run(renpy.config.quit_action)
|
|
finally:
|
|
self.in_quit_event = False
|
|
|
|
else:
|
|
renpy.exports.quit(save=True)
|
|
|
|
def get_mouse_info(self):
|
|
# Figure out if the mouse visibility algorithm is hiding the mouse.
|
|
if (renpy.config.mouse_hide_time is not None) and (self.mouse_event_time + renpy.config.mouse_hide_time < renpy.display.core.get_time()):
|
|
visible = False
|
|
else:
|
|
visible = renpy.store.mouse_visible and (not renpy.game.less_mouse)
|
|
|
|
visible = visible and self.show_mouse and not (renpy.display.video.fullscreen)
|
|
|
|
# If not visible, hide the mouse.
|
|
if not visible:
|
|
return False, 0, 0, None
|
|
|
|
# Deal with a hardware mouse, the easy way.
|
|
if not renpy.config.mouse:
|
|
return True, 0, 0, None
|
|
|
|
# Deal with the mouse going offscreen.
|
|
if not self.mouse_focused:
|
|
return False, 0, 0, None
|
|
|
|
mouse_kind = renpy.display.focus.get_mouse() or self.mouse
|
|
|
|
# Figure out the mouse animation.
|
|
if mouse_kind in renpy.config.mouse:
|
|
anim = renpy.config.mouse[mouse_kind]
|
|
else:
|
|
anim = renpy.config.mouse[getattr(renpy.store, 'default_mouse', 'default')]
|
|
|
|
img, x, y = anim[self.ticks % len(anim)]
|
|
rend = renpy.display.im.load_image(img)
|
|
|
|
tex = rend.children[0][0]
|
|
xo = rend.children[0][1]
|
|
yo = rend.children[0][2]
|
|
|
|
return False, x - xo, y - yo, tex
|
|
|
|
def set_mouse_pos(self, x, y, duration):
|
|
"""
|
|
Sets the mouse position. Duration can be a number of seconds or
|
|
None.
|
|
"""
|
|
|
|
self.mouse_move = MouseMove(x, y, duration)
|
|
self.force_redraw = True
|
|
|
|
def drawn_since(self, seconds_ago):
|
|
"""
|
|
Returns true if the screen has been drawn in the last `seconds_ago`,
|
|
and false otherwise.
|
|
"""
|
|
|
|
return (get_time() - self.frame_time) <= seconds_ago
|
|
|
|
def check_suspend(self, ev):
|
|
"""
|
|
Handles the SDL2 suspend process.
|
|
"""
|
|
|
|
def save():
|
|
if renpy.config.save_on_mobile_background and (not renpy.store.main_menu):
|
|
renpy.loadsave.save("_reload-1")
|
|
|
|
renpy.persistent.update(True)
|
|
|
|
if ev.type == pygame.APP_TERMINATING:
|
|
save()
|
|
sys.exit(0)
|
|
|
|
if ev.type != pygame.APP_WILLENTERBACKGROUND:
|
|
return False
|
|
|
|
# At this point, we're about to enter the background.
|
|
|
|
renpy.audio.audio.pause_all()
|
|
|
|
if android:
|
|
android.wakelock(False)
|
|
|
|
pygame.time.set_timer(PERIODIC, 0)
|
|
pygame.time.set_timer(REDRAW, 0)
|
|
pygame.time.set_timer(TIMEEVENT, 0)
|
|
|
|
save()
|
|
|
|
if renpy.config.quit_on_mobile_background:
|
|
sys.exit(0)
|
|
|
|
renpy.exports.free_memory()
|
|
|
|
print("Entered background.")
|
|
|
|
while True:
|
|
ev = pygame.event.wait()
|
|
|
|
if ev.type == pygame.APP_DIDENTERFOREGROUND:
|
|
break
|
|
|
|
if ev.type == pygame.APP_TERMINATING:
|
|
sys.exit(0)
|
|
|
|
print("Entering foreground.")
|
|
|
|
# Since we came back to life, we can get rid of the
|
|
# auto-reload.
|
|
renpy.loadsave.unlink_save("_reload-1")
|
|
|
|
pygame.time.set_timer(PERIODIC, PERIODIC_INTERVAL)
|
|
|
|
renpy.audio.audio.unpause_all()
|
|
|
|
if android:
|
|
android.wakelock(True)
|
|
|
|
# Reset the display so we get the GL context back.
|
|
self.display_reset = True
|
|
self.restart_interaction = True
|
|
|
|
return True
|
|
|
|
def iconified(self):
|
|
"""
|
|
Called when we become an icon.
|
|
"""
|
|
|
|
if self.minimized:
|
|
return
|
|
|
|
self.minimized = True
|
|
|
|
renpy.display.log.write("The window was minimized.")
|
|
|
|
def restored(self):
|
|
"""
|
|
Called when we are restored from being an icon.
|
|
"""
|
|
|
|
# This is necessary on Windows/DirectX/Angle, as otherwise we get
|
|
# a blank screen.
|
|
|
|
if not self.minimized:
|
|
return
|
|
|
|
self.minimized = False
|
|
|
|
renpy.display.log.write("The window was restored.")
|
|
|
|
if renpy.windows:
|
|
self.display_reset = True
|
|
self.set_mode(self.last_resize)
|
|
|
|
def enter_context(self):
|
|
"""
|
|
Called when we enter a new context.
|
|
"""
|
|
|
|
# Stop ongoing transitions.
|
|
self.ongoing_transition.clear()
|
|
self.transition_from.clear()
|
|
self.transition_time.clear()
|
|
|
|
def post_time_event(self):
|
|
"""
|
|
Posts a time_event object to the queue.
|
|
"""
|
|
|
|
try:
|
|
pygame.event.post(self.time_event)
|
|
except:
|
|
pass
|
|
|
|
def after_longpress(self):
|
|
"""
|
|
Called after a longpress, to ignore the mouse button release.
|
|
"""
|
|
|
|
self.ignore_touch = True
|
|
renpy.display.focus.mouse_handler(None, -1, -1, default=False)
|
|
|
|
def text_event_in_queue(self):
|
|
"""
|
|
Returns true if the next event in the queue is a text editing event.
|
|
"""
|
|
|
|
ev = self.event_peek()
|
|
if ev is None:
|
|
return False
|
|
else:
|
|
return ev.type in (pygame.TEXTINPUT, pygame.TEXTEDITING)
|
|
|
|
def update_text_rect(self):
|
|
"""
|
|
Updates the text input state and text rectangle.
|
|
"""
|
|
|
|
if renpy.store._text_rect is not None: # @UndefinedVariable
|
|
self.text_rect = renpy.store._text_rect # @UndefinedVariable
|
|
|
|
if self.text_rect is not None:
|
|
|
|
not_shown = pygame.key.has_screen_keyboard_support() and not pygame.key.is_screen_keyboard_shown() # @UndefinedVariable
|
|
|
|
if self.old_text_rect != self.text_rect:
|
|
x, y, w, h = self.text_rect
|
|
x0, y0 = renpy.display.draw.untranslate_point(x, y)
|
|
x1, y1 = renpy.display.draw.untranslate_point(x + w, y + h)
|
|
rect = (x0, y0, x1 - x0, y1 - y0)
|
|
|
|
pygame.key.set_text_input_rect(rect) # @UndefinedVariable
|
|
|
|
if not self.old_text_rect or not_shown:
|
|
pygame.key.start_text_input() # @UndefinedVariable
|
|
|
|
else:
|
|
if self.old_text_rect:
|
|
pygame.key.stop_text_input() # @UndefinedVariable
|
|
pygame.key.set_text_input_rect(None) # @UndefinedVariable
|
|
|
|
self.old_text_rect = self.text_rect
|
|
|
|
def maximum_framerate(self, t):
|
|
"""
|
|
Forces Ren'Py to draw the screen at the maximum framerate for `t` seconds.
|
|
"""
|
|
|
|
if t is None:
|
|
self.maximum_framerate_time = 0
|
|
else:
|
|
self.maximum_framerate_time = max(self.maximum_framerate_time, get_time() + t)
|
|
|
|
def interact(self, clear=True, suppress_window=False, trans_pause=False, **kwargs):
|
|
"""
|
|
This handles an interaction, restarting it if necessary. All of the
|
|
keyword arguments are passed off to interact_core.
|
|
"""
|
|
|
|
renpy.plog(1, "start of new interaction")
|
|
|
|
if not self.started:
|
|
self.start()
|
|
|
|
if self.clear_screenshot:
|
|
self.lose_screenshot()
|
|
|
|
self.clear_screenshot = False
|
|
|
|
self.trans_pause = trans_pause
|
|
|
|
# Cancel magic error reporting.
|
|
renpy.bootstrap.report_error = None
|
|
|
|
context = renpy.game.context()
|
|
|
|
if context.interacting:
|
|
raise Exception("Cannot start an interaction in the middle of an interaction, without creating a new context.")
|
|
|
|
context.interacting = True
|
|
|
|
# Show a missing window.
|
|
if not suppress_window:
|
|
self.show_window()
|
|
|
|
# These things can be done once per interaction.
|
|
|
|
preloads = self.preloads
|
|
self.preloads = [ ]
|
|
|
|
try:
|
|
for i in renpy.config.start_interact_callbacks:
|
|
i()
|
|
|
|
repeat = True
|
|
|
|
self.start_interact = True
|
|
|
|
while repeat:
|
|
repeat, rv = self.interact_core(preloads=preloads, trans_pause=trans_pause, **kwargs)
|
|
self.start_interact = False
|
|
|
|
return rv
|
|
|
|
finally:
|
|
|
|
context.interacting = False
|
|
|
|
# Clean out transient stuff at the end of an interaction.
|
|
if clear:
|
|
scene_lists = renpy.game.context().scene_lists
|
|
scene_lists.replace_transient()
|
|
|
|
self.ongoing_transition = { }
|
|
self.transition_time = { }
|
|
self.transition_from = { }
|
|
|
|
self.restart_interaction = True
|
|
|
|
renpy.game.context().mark_seen()
|
|
renpy.game.context().scene_lists.shown_window = False
|
|
|
|
if renpy.game.log is not None:
|
|
renpy.game.log.did_interaction = True
|
|
|
|
if renpy.store._side_image_attributes_reset:
|
|
renpy.store._side_image_attributes = None
|
|
renpy.store._side_image_attributes_reset = False
|
|
|
|
def consider_gc(self):
|
|
"""
|
|
Considers if we should peform a garbage collection.
|
|
"""
|
|
|
|
if not renpy.config.manage_gc:
|
|
return
|
|
|
|
count = gc.get_count()
|
|
|
|
if count[0] >= renpy.config.idle_gc_count:
|
|
renpy.plog(2, "before gc")
|
|
|
|
if count[2] >= renpy.config.gc_thresholds[2]:
|
|
gen = 2
|
|
elif count[1] >= renpy.config.gc_thresholds[1]:
|
|
gen = 1
|
|
else:
|
|
gen = 0
|
|
|
|
gc.collect(gen)
|
|
|
|
if gc.garbage:
|
|
renpy.memory.print_garbage(gen)
|
|
gc.garbage[:] = [ ]
|
|
|
|
renpy.plog(2, "after gc")
|
|
|
|
def idle_frame(self, can_block, expensive):
|
|
"""
|
|
Tasks that are run during "idle" frames.
|
|
"""
|
|
|
|
if expensive:
|
|
renpy.plog(1, "start idle_frame (expensive)")
|
|
else:
|
|
renpy.plog(1, "start idle_frame (inexpensive)")
|
|
|
|
# We want this to include the GC time, so we don't predict on
|
|
# frames where we GC.
|
|
start = get_time()
|
|
|
|
step = 1
|
|
|
|
while True:
|
|
|
|
if self.event_peek():
|
|
break
|
|
|
|
if not (can_block and expensive):
|
|
if get_time() > (start + .0005):
|
|
break
|
|
|
|
# Step 1: Run gc.
|
|
if step == 1:
|
|
self.consider_gc()
|
|
step += 1
|
|
|
|
# Step 2: Push textures to GPU.
|
|
elif step == 2:
|
|
renpy.display.draw.ready_one_texture()
|
|
step += 1
|
|
|
|
# Step 3: Predict more images.
|
|
elif step == 3:
|
|
|
|
if not self.prediction_coroutine:
|
|
step += 1
|
|
continue
|
|
|
|
try:
|
|
result = self.prediction_coroutine.send(expensive)
|
|
except ValueError:
|
|
# Saw this happen once during a quit, giving a
|
|
# ValueError: generator already executing
|
|
result = None
|
|
|
|
if result is None:
|
|
self.prediction_coroutine = None
|
|
step += 1
|
|
|
|
elif result is False:
|
|
if not expensive:
|
|
step += 1
|
|
|
|
# Step 4: Preload images (on emscripten)
|
|
elif step == 4:
|
|
|
|
if expensive and renpy.emscripten:
|
|
renpy.display.im.cache.preload_thread_pass()
|
|
|
|
step += 1
|
|
|
|
# Step 5: Autosave.
|
|
elif step == 5:
|
|
|
|
if not self.did_autosave:
|
|
renpy.loadsave.autosave()
|
|
renpy.persistent.check_update()
|
|
self.did_autosave = True
|
|
|
|
step += 1
|
|
|
|
else:
|
|
break
|
|
|
|
if expensive:
|
|
renpy.plog(1, "end idle_frame (expensive)")
|
|
else:
|
|
renpy.plog(1, "end idle_frame (inexpensive)")
|
|
|
|
def interact_core(self,
|
|
show_mouse=True,
|
|
trans_pause=False,
|
|
suppress_overlay=False,
|
|
suppress_underlay=False,
|
|
mouse='default',
|
|
preloads=[],
|
|
roll_forward=None,
|
|
):
|
|
"""
|
|
This handles one cycle of displaying an image to the user,
|
|
and then responding to user input.
|
|
|
|
@param show_mouse: Should the mouse be shown during this
|
|
interaction? Only advisory, and usually doesn't work.
|
|
|
|
@param trans_pause: If given, we must have a transition. Should we
|
|
add a pause behavior during the transition?
|
|
|
|
@param suppress_overlay: This suppresses the display of the overlay.
|
|
@param suppress_underlay: This suppresses the display of the underlay.
|
|
"""
|
|
|
|
renpy.plog(1, "start interact_core")
|
|
|
|
suppress_overlay = suppress_overlay or renpy.store.suppress_overlay
|
|
|
|
# Store the various parameters.
|
|
self.suppress_overlay = suppress_overlay
|
|
self.suppress_underlay = suppress_underlay
|
|
self.trans_pause = trans_pause
|
|
|
|
# Show default screens.
|
|
renpy.display.screen.show_overlay_screens(suppress_overlay)
|
|
|
|
# Prepare screens, if need be.
|
|
renpy.display.screen.prepare_screens()
|
|
|
|
self.roll_forward = roll_forward
|
|
self.show_mouse = show_mouse
|
|
|
|
suppress_transition = renpy.config.skipping or renpy.game.less_updates
|
|
|
|
# The global one.
|
|
self.suppress_transition = False
|
|
|
|
# Figure out transitions.
|
|
if suppress_transition:
|
|
self.ongoing_transition.clear()
|
|
self.transition_from.clear()
|
|
self.transition_time.clear()
|
|
else:
|
|
for k in self.transition:
|
|
if k not in self.old_scene:
|
|
continue
|
|
|
|
self.ongoing_transition[k] = self.transition[k]
|
|
self.transition_from[k] = self.old_scene[k]._in_current_store()
|
|
self.transition_time[k] = None
|
|
|
|
self.transition.clear()
|
|
|
|
# Safety condition, prevents deadlocks.
|
|
if trans_pause:
|
|
if not self.ongoing_transition:
|
|
return False, None
|
|
if None not in self.ongoing_transition:
|
|
return False, None
|
|
if suppress_transition:
|
|
return False, None
|
|
if not self.old_scene:
|
|
return False, None
|
|
|
|
# Check to see if the language has changed.
|
|
renpy.translation.check_language()
|
|
|
|
# We just restarted.
|
|
self.restart_interaction = False
|
|
|
|
# Setup the mouse.
|
|
self.mouse = mouse
|
|
|
|
# The start and end times of this interaction.
|
|
start_time = get_time()
|
|
end_time = start_time
|
|
|
|
# frames = 0
|
|
|
|
for i in renpy.config.interact_callbacks:
|
|
i()
|
|
|
|
# Set the window caption.
|
|
self.set_window_caption()
|
|
|
|
# Tick time forward.
|
|
renpy.display.im.cache.tick()
|
|
renpy.text.text.text_tick()
|
|
renpy.display.predict.reset()
|
|
|
|
# Clear the size groups.
|
|
renpy.display.layout.size_groups.clear()
|
|
|
|
# Clear the set of updated screens.
|
|
renpy.display.screen.updated_screens.clear()
|
|
|
|
# Clear some events.
|
|
pygame.event.clear((pygame.MOUSEMOTION,
|
|
PERIODIC,
|
|
TIMEEVENT,
|
|
REDRAW))
|
|
|
|
# Add a single TIMEEVENT to the queue.
|
|
self.post_time_event()
|
|
|
|
# Figure out the scene list we want to show.
|
|
scene_lists = renpy.game.context().scene_lists
|
|
|
|
# Remove the now-hidden things.
|
|
scene_lists.remove_hidden()
|
|
|
|
# Compute the overlay.
|
|
if not suppress_overlay:
|
|
self.compute_overlay()
|
|
|
|
# The root widget of everything that is displayed on the screen.
|
|
root_widget = renpy.display.layout.MultiBox(layout='fixed')
|
|
root_widget.layers = { }
|
|
|
|
# A list of widgets that are roots of trees of widgets that are
|
|
# considered for focusing.
|
|
focus_roots = [ ]
|
|
|
|
# Add the underlay to the root widget.
|
|
if not suppress_underlay:
|
|
for i in renpy.config.underlay:
|
|
root_widget.add(i)
|
|
focus_roots.append(i)
|
|
|
|
if roll_forward is not None:
|
|
rfw = renpy.display.behavior.RollForward(roll_forward)
|
|
root_widget.add(rfw)
|
|
focus_roots.append(rfw)
|
|
|
|
# Figure out the scene. (All of the layers, and the root.)
|
|
scene = self.compute_scene(scene_lists)
|
|
renpy.display.tts.set_root(scene[None])
|
|
|
|
renpy.plog(1, "computed scene")
|
|
|
|
# If necessary, load all images here.
|
|
for w in scene.itervalues():
|
|
try:
|
|
renpy.display.predict.displayable(w)
|
|
except:
|
|
pass
|
|
|
|
renpy.plog(1, "final predict")
|
|
|
|
# The root widget of all of the layers.
|
|
layers_root = renpy.display.layout.MultiBox(layout='fixed')
|
|
layers_root.layers = { }
|
|
|
|
def add_layer(where, layer):
|
|
|
|
scene_layer = scene[layer]
|
|
focus_roots.append(scene_layer)
|
|
|
|
if (self.ongoing_transition.get(layer, None) and
|
|
not suppress_transition):
|
|
|
|
trans = self.ongoing_transition[layer](
|
|
old_widget=self.transition_from[layer],
|
|
new_widget=scene_layer)
|
|
|
|
if not isinstance(trans, Displayable):
|
|
raise Exception("Expected transition to be a displayable, not a %r" % trans)
|
|
|
|
transition_time = self.transition_time.get(layer, None)
|
|
|
|
where.add(trans, transition_time, transition_time)
|
|
where.layers[layer] = trans
|
|
|
|
else:
|
|
where.layers[layer] = scene_layer
|
|
where.add(scene_layer)
|
|
|
|
# Add layers (perhaps with transitions) to the layers root.
|
|
for layer in renpy.config.layers:
|
|
add_layer(layers_root, layer)
|
|
|
|
# Add layers_root to root_widget, perhaps through a transition.
|
|
if (self.ongoing_transition.get(None, None) and
|
|
not suppress_transition):
|
|
|
|
old_root = renpy.display.layout.MultiBox(layout='fixed')
|
|
old_root.layers = { }
|
|
|
|
for layer in renpy.config.layers:
|
|
d = self.transition_from[None].layers[layer]
|
|
old_root.layers[layer] = d
|
|
old_root.add(d)
|
|
|
|
trans = self.ongoing_transition[None](
|
|
old_widget=old_root,
|
|
new_widget=layers_root)
|
|
|
|
if not isinstance(trans, Displayable):
|
|
raise Exception("Expected transition to be a displayable, not a %r" % trans)
|
|
|
|
trans._show()
|
|
|
|
transition_time = self.transition_time.get(None, None)
|
|
root_widget.add(trans, transition_time, transition_time)
|
|
|
|
if trans_pause:
|
|
|
|
if renpy.store._dismiss_pause:
|
|
sb = renpy.display.behavior.SayBehavior()
|
|
else:
|
|
sb = renpy.display.behavior.SayBehavior(dismiss='dismiss_hard_pause')
|
|
|
|
root_widget.add(sb)
|
|
focus_roots.append(sb)
|
|
|
|
pb = renpy.display.behavior.PauseBehavior(trans.delay)
|
|
root_widget.add(pb, transition_time, transition_time)
|
|
focus_roots.append(pb)
|
|
|
|
else:
|
|
root_widget.add(layers_root)
|
|
|
|
# Add top_layers to the root_widget.
|
|
for layer in renpy.config.top_layers:
|
|
add_layer(root_widget, layer)
|
|
|
|
for i in renpy.display.emulator.overlay:
|
|
root_widget.add(i)
|
|
|
|
del add_layer
|
|
|
|
self.prediction_coroutine = renpy.display.predict.prediction_coroutine(root_widget)
|
|
self.prediction_coroutine.send(None)
|
|
|
|
# Clean out the registered adjustments.
|
|
renpy.display.behavior.adj_registered.clear()
|
|
|
|
# Clean up some movie-related things.
|
|
renpy.display.video.early_interact()
|
|
|
|
# Call per-interaction code for all widgets.
|
|
renpy.display.behavior.input_pre_per_interact()
|
|
root_widget.visit_all(lambda i : i.per_interact())
|
|
renpy.display.behavior.input_post_per_interact()
|
|
|
|
# Now, update various things regarding scenes and transitions,
|
|
# so we are ready for a new interaction or a restart.
|
|
self.old_scene = scene
|
|
|
|
# Okay, from here on we now have a single root widget (root_widget),
|
|
# which we will try to show to the user.
|
|
|
|
# Figure out what should be focused.
|
|
renpy.display.focus.before_interact(focus_roots)
|
|
|
|
# Something updated the screens. Deal with it now, so the player doesn't
|
|
# see it.
|
|
if self.restart_interaction:
|
|
return True, None
|
|
|
|
# Redraw the screen.
|
|
needs_redraw = True
|
|
|
|
# First pass through the while loop?
|
|
first_pass = True
|
|
|
|
# We don't yet know when the interaction began.
|
|
self.interact_time = None
|
|
|
|
# We only want to do autosave once.
|
|
self.did_autosave = False
|
|
|
|
old_timeout_time = None
|
|
old_redraw_time = None
|
|
|
|
rv = None
|
|
|
|
# Start sound.
|
|
renpy.audio.audio.interact()
|
|
|
|
# How long until we redraw.
|
|
_redraw_in = 3600
|
|
|
|
# Have we drawn a frame yet?
|
|
video_frame_drawn = False
|
|
|
|
# We're no longer after rollback.
|
|
renpy.game.after_rollback = False
|
|
|
|
# How many frames have we shown so far?
|
|
frame = 0
|
|
|
|
can_block = False
|
|
|
|
# This try block is used to force cleanup even on termination
|
|
# caused by an exception propagating through this function.
|
|
try:
|
|
|
|
while rv is None:
|
|
|
|
renpy.plog(1, "start of interact while loop")
|
|
|
|
renpy.execution.not_infinite_loop(10)
|
|
|
|
# Check for a change in fullscreen preference.
|
|
if ((self.fullscreen != renpy.game.preferences.fullscreen) or
|
|
self.display_reset or (renpy.display.draw is None)):
|
|
|
|
self.set_mode()
|
|
needs_redraw = True
|
|
|
|
# Check for autoreload.
|
|
if renpy.loader.needs_autoreload:
|
|
renpy.loader.needs_autoreload = False
|
|
renpy.exports.reload_script()
|
|
|
|
for i in renpy.config.needs_redraw_callbacks:
|
|
if i():
|
|
needs_redraw = True
|
|
|
|
# Redraw the screen.
|
|
if (self.force_redraw or
|
|
((first_pass or not pygame.event.peek(ALL_EVENTS)) and
|
|
renpy.display.draw.should_redraw(needs_redraw, first_pass, can_block))):
|
|
|
|
self.force_redraw = False
|
|
|
|
renpy.display.render.process_redraws()
|
|
|
|
# If we have a movie, start showing it.
|
|
fullscreen_video = renpy.display.video.interact()
|
|
|
|
# Clean out the redraws, if we have to.
|
|
# renpy.display.render.kill_redraws()
|
|
|
|
self.text_rect = None
|
|
|
|
# Draw the screen.
|
|
self.frame_time = get_time()
|
|
|
|
renpy.audio.audio.advance_time() # Sets the time of all video frames.
|
|
|
|
self.draw_screen(root_widget, fullscreen_video, (not fullscreen_video) or video_frame_drawn)
|
|
|
|
if first_pass:
|
|
if not self.interact_time:
|
|
self.interact_time = max(self.frame_time, get_time() - self.frame_duration)
|
|
|
|
scene_lists.set_times(self.interact_time)
|
|
|
|
for k, v in self.transition_time.iteritems():
|
|
if v is None:
|
|
self.transition_time[k] = self.interact_time
|
|
|
|
renpy.display.render.adjust_render_cache_times(self.frame_time, self.interact_time)
|
|
|
|
frame += 1
|
|
renpy.config.frames += 1
|
|
|
|
# If profiling is enabled, report the profile time.
|
|
if renpy.config.profile or self.profile_once:
|
|
|
|
renpy.plog(0, "end frame")
|
|
renpy.performance.analyze()
|
|
renpy.performance.clear()
|
|
renpy.plog(0, "start frame")
|
|
|
|
self.profile_once = False
|
|
|
|
if first_pass and self.last_event and self.last_event.type in [ pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION ]:
|
|
|
|
x, y = renpy.display.draw.get_mouse_pos()
|
|
ev, x, y = renpy.display.emulator.emulator(self.last_event, x, y)
|
|
|
|
if self.ignore_touch:
|
|
x = -1
|
|
y = -1
|
|
|
|
if renpy.android and self.last_event.type == pygame.MOUSEBUTTONUP:
|
|
x = -1
|
|
y = -1
|
|
|
|
renpy.display.focus.mouse_handler(None, x, y, default=False)
|
|
|
|
needs_redraw = False
|
|
first_pass = False
|
|
|
|
pygame.time.set_timer(REDRAW, 0)
|
|
pygame.event.clear([REDRAW])
|
|
old_redraw_time = None
|
|
|
|
self.update_text_rect()
|
|
|
|
renpy.test.testexecution.execute()
|
|
|
|
# Move the mouse, if necessary.
|
|
if self.mouse_move is not None:
|
|
if not self.mouse_move.perform():
|
|
self.mouse_move = None
|
|
|
|
# Draw the mouse, if it needs drawing.
|
|
renpy.display.draw.update_mouse()
|
|
|
|
# See if we want to restart the interaction entirely.
|
|
if self.restart_interaction:
|
|
return True, None
|
|
|
|
# Determine if we need a redraw. (We want to run these
|
|
# functions, so we put them first to prevent short-circuiting.)
|
|
|
|
if renpy.display.video.frequent():
|
|
needs_redraw = True
|
|
video_frame_drawn = True
|
|
|
|
if renpy.display.render.check_redraws():
|
|
needs_redraw = True
|
|
|
|
# How many seconds until we timeout.
|
|
_timeout_in = 3600
|
|
|
|
# Handle the redraw timer.
|
|
redraw_time = renpy.display.render.redraw_time()
|
|
|
|
# We only need to set the REDRAW timer if we can block.
|
|
can_block = renpy.display.draw.can_block()
|
|
|
|
if self.maximum_framerate_time > get_time():
|
|
can_block = False
|
|
|
|
if (redraw_time is not None) and (not needs_redraw) and can_block:
|
|
if redraw_time != old_redraw_time:
|
|
time_left = redraw_time - get_time()
|
|
time_left = min(time_left, 3600)
|
|
_redraw_in = time_left
|
|
|
|
if time_left <= 0:
|
|
try:
|
|
pygame.event.post(self.redraw_event)
|
|
except:
|
|
pass
|
|
pygame.time.set_timer(REDRAW, 0)
|
|
else:
|
|
pygame.time.set_timer(REDRAW, max(int(time_left * 1000), 1))
|
|
|
|
old_redraw_time = redraw_time
|
|
else:
|
|
_redraw_in = 3600
|
|
pygame.time.set_timer(REDRAW, 0)
|
|
|
|
# Handle the timeout timer.
|
|
if not self.timeout_time:
|
|
pygame.time.set_timer(TIMEEVENT, 0)
|
|
else:
|
|
time_left = self.timeout_time - get_time()
|
|
time_left = min(time_left, 3600)
|
|
_timeout_in = time_left
|
|
|
|
if time_left <= 0:
|
|
self.timeout_time = None
|
|
pygame.time.set_timer(TIMEEVENT, 0)
|
|
self.post_time_event()
|
|
elif self.timeout_time != old_timeout_time:
|
|
# Always set to at least 1ms.
|
|
pygame.time.set_timer(TIMEEVENT, int(time_left * 1000 + 1))
|
|
old_timeout_time = self.timeout_time
|
|
|
|
if can_block or (frame >= renpy.config.idle_frame):
|
|
expensive = not ( needs_redraw or (_redraw_in < .2) or (_timeout_in < .2) or renpy.display.video.playing() )
|
|
self.idle_frame(can_block, expensive)
|
|
|
|
if needs_redraw or (not can_block) or self.mouse_move or renpy.display.video.playing():
|
|
renpy.plog(1, "pre peek")
|
|
ev = self.event_poll()
|
|
renpy.plog(1, "post peek {!r}", ev)
|
|
else:
|
|
renpy.plog(1, "pre wait")
|
|
ev = self.event_wait()
|
|
renpy.plog(1, "post wait {!r}", ev)
|
|
|
|
if ev.type == pygame.NOEVENT:
|
|
|
|
if can_block and (not needs_redraw) and (not self.prediction_coroutine) and (not self.mouse_move):
|
|
pygame.time.wait(1)
|
|
|
|
continue
|
|
|
|
# Recognize and ignore AltGr on Windows.
|
|
if ev.type == pygame.KEYDOWN:
|
|
if ev.key == pygame.K_LCTRL:
|
|
|
|
ev2 = self.event_peek()
|
|
|
|
if (ev2 is not None) and (ev2.type == pygame.KEYDOWN):
|
|
if ev2.key == pygame.K_RALT:
|
|
continue
|
|
|
|
# Check to see if the OS is asking us to suspend (on Android
|
|
# and iOS.)
|
|
if self.check_suspend(ev):
|
|
continue
|
|
|
|
# Try to merge an TIMEEVENT with other timeevents.
|
|
if ev.type == TIMEEVENT:
|
|
old_timeout_time = None
|
|
pygame.event.clear([TIMEEVENT])
|
|
|
|
# On Android, where we have multiple mouse buttons, we can
|
|
# merge a mouse down and mouse up event with its successor. This
|
|
# prevents us from getting overwhelmed with too many events on
|
|
# a multitouch screen.
|
|
if android and (ev.type == pygame.MOUSEBUTTONDOWN or ev.type == pygame.MOUSEBUTTONUP):
|
|
pygame.event.clear(ev.type)
|
|
|
|
# Handle redraw timeouts.
|
|
if ev.type == REDRAW:
|
|
pygame.event.clear([REDRAW])
|
|
old_redraw_time = None
|
|
continue
|
|
|
|
# Handle periodic events. This includes updating the mouse timers (and through the loop,
|
|
# the mouse itself), and the audio system periodic calls.
|
|
if ev.type == PERIODIC:
|
|
events = 1 + len(pygame.event.get([PERIODIC]))
|
|
self.ticks += events
|
|
|
|
for i in renpy.config.periodic_callbacks:
|
|
i()
|
|
|
|
renpy.audio.audio.periodic()
|
|
renpy.display.tts.periodic()
|
|
continue
|
|
|
|
# Handle quit specially for now.
|
|
if ev.type == pygame.QUIT:
|
|
self.quit_event()
|
|
continue
|
|
|
|
# Ignore KEY-events while text is being edited (usually with an IME).
|
|
if ev.type == pygame.TEXTEDITING:
|
|
if ev.text:
|
|
self.text_editing = ev
|
|
else:
|
|
self.text_editing = None
|
|
elif ev.type == pygame.TEXTINPUT:
|
|
self.text_editing = None
|
|
elif self.text_editing and ev.type in [ pygame.KEYDOWN, pygame.KEYUP ]:
|
|
continue
|
|
|
|
if ev.type == pygame.VIDEOEXPOSE:
|
|
# Needed to force the display to redraw after expose in
|
|
# the software renderer.
|
|
renpy.game.interface.full_redraw = True
|
|
renpy.game.interface.force_redraw = True
|
|
|
|
if isinstance(renpy.display.draw, renpy.display.swdraw.SWDraw):
|
|
renpy.display.draw.full_redraw = True
|
|
|
|
continue
|
|
|
|
# Handle videoresize.
|
|
if ev.type == pygame.VIDEORESIZE:
|
|
evs = pygame.event.get([pygame.VIDEORESIZE])
|
|
|
|
if len(evs):
|
|
ev = evs[-1]
|
|
|
|
# We seem to get a spurious event like this when leaving
|
|
# fullscreen mode on windows.
|
|
if ev.w < 256 or ev.h < 256:
|
|
continue
|
|
|
|
size = (ev.w // self.dpi_scale, ev.h // self.dpi_scale)
|
|
|
|
# Refresh fullscreen status (e.g. user pressed Esc. in browser)
|
|
main_window = pygame.display.get_window()
|
|
self.fullscreen = main_window is not None and bool(main_window.get_window_flags() & (pygame.WINDOW_FULLSCREEN_DESKTOP|pygame.WINDOW_FULLSCREEN))
|
|
renpy.game.preferences.fullscreen = self.fullscreen
|
|
|
|
if pygame.display.get_surface().get_size() != ev.size:
|
|
self.set_mode(size)
|
|
|
|
if not self.fullscreen:
|
|
self.last_resize = size
|
|
|
|
continue
|
|
|
|
# If we're ignoring touch events, and get a mouse up, stop
|
|
# ignoring those events.
|
|
if self.ignore_touch and \
|
|
ev.type == pygame.MOUSEBUTTONUP and \
|
|
ev.button == 1:
|
|
|
|
self.ignore_touch = False
|
|
continue
|
|
|
|
# Merge mousemotion events.
|
|
if ev.type == pygame.MOUSEMOTION:
|
|
evs = pygame.event.get([pygame.MOUSEMOTION])
|
|
if len(evs):
|
|
ev = evs[-1]
|
|
|
|
if renpy.windows:
|
|
self.mouse_focused = True
|
|
|
|
# Handle mouse event time, and ignoring touch.
|
|
if ev.type == pygame.MOUSEMOTION or \
|
|
ev.type == pygame.MOUSEBUTTONDOWN or \
|
|
ev.type == pygame.MOUSEBUTTONUP:
|
|
|
|
self.mouse_event_time = renpy.display.core.get_time()
|
|
|
|
if self.ignore_touch:
|
|
renpy.display.focus.mouse_handler(None, -1, -1, default=False)
|
|
|
|
# Handle focus notifications.
|
|
if ev.type == pygame.ACTIVEEVENT:
|
|
|
|
if ev.state & 1:
|
|
if not ev.gain:
|
|
renpy.display.focus.clear_focus()
|
|
|
|
self.mouse_focused = ev.gain
|
|
|
|
if ev.state & 2:
|
|
self.keyboard_focused = ev.gain
|
|
|
|
if ev.state & 4:
|
|
if ev.gain:
|
|
self.restored()
|
|
else:
|
|
self.iconified()
|
|
|
|
pygame.key.set_mods(0)
|
|
|
|
# This returns the event location. It also updates the
|
|
# mouse state as necessary.
|
|
x, y = renpy.display.draw.mouse_event(ev)
|
|
x, y = renpy.test.testmouse.get_mouse_pos(x, y)
|
|
|
|
ev, x, y = renpy.display.emulator.emulator(ev, x, y)
|
|
if ev is None:
|
|
continue
|
|
|
|
if not self.mouse_focused or self.ignore_touch:
|
|
x = -1
|
|
y = -1
|
|
|
|
# This can set the event to None, to ignore it.
|
|
ev = renpy.display.controller.event(ev)
|
|
if not ev:
|
|
continue
|
|
|
|
# Handle skipping.
|
|
renpy.display.behavior.skipping(ev)
|
|
|
|
self.event_time = end_time = get_time()
|
|
|
|
try:
|
|
|
|
if self.touch:
|
|
renpy.display.gesture.recognizer.event(ev, x, y) # @UndefinedVariable
|
|
|
|
# Handle the event normally.
|
|
rv = renpy.display.focus.mouse_handler(ev, x, y)
|
|
|
|
if rv is None:
|
|
rv = root_widget.event(ev, x, y, 0)
|
|
|
|
if rv is None:
|
|
rv = renpy.display.focus.key_handler(ev)
|
|
|
|
if rv is not None:
|
|
break
|
|
|
|
# Handle displayable inspector.
|
|
if renpy.config.inspector:
|
|
if renpy.display.behavior.map_event(ev, "inspector"):
|
|
l = self.surftree.main_displayables_at_point(x, y, renpy.config.transient_layers + renpy.config.context_clear_layers + renpy.config.overlay_layers)
|
|
renpy.game.invoke_in_new_context(renpy.config.inspector, l)
|
|
elif renpy.display.behavior.map_event(ev, "full_inspector"):
|
|
l = self.surftree.main_displayables_at_point(x, y, renpy.config.layers)
|
|
renpy.game.invoke_in_new_context(renpy.config.inspector, l)
|
|
|
|
except IgnoreEvent:
|
|
# An ignored event can change the timeout. So we want to
|
|
# process an TIMEEVENT to ensure that the timeout is
|
|
# set correctly
|
|
|
|
if ev.type != TIMEEVENT:
|
|
self.post_time_event()
|
|
|
|
# Check again after handling the event.
|
|
needs_redraw |= renpy.display.render.check_redraws()
|
|
|
|
if self.restart_interaction:
|
|
return True, None
|
|
|
|
# If we were trans-paused and rv is true, suppress
|
|
# transitions up to the next interaction.
|
|
if trans_pause and rv:
|
|
self.suppress_transition = True
|
|
|
|
# But wait, there's more! The finally block runs some cleanup
|
|
# after this.
|
|
return False, rv
|
|
|
|
except EndInteraction as e:
|
|
return False, e.value
|
|
|
|
finally:
|
|
|
|
renpy.game.context().say_attributes = None
|
|
|
|
# Clean out the overlay layers.
|
|
for i in renpy.config.overlay_layers:
|
|
scene_lists.clear(i)
|
|
|
|
# Stop ongoing preloading.
|
|
renpy.display.im.cache.end_tick()
|
|
|
|
# We no longer disable periodic between interactions.
|
|
# pygame.time.set_timer(PERIODIC, 0)
|
|
|
|
pygame.time.set_timer(TIMEEVENT, 0)
|
|
pygame.time.set_timer(REDRAW, 0)
|
|
|
|
self.consider_gc()
|
|
|
|
renpy.game.context().runtime += end_time - start_time
|
|
|
|
# Restart the old interaction, which also causes a
|
|
# redraw if needed.
|
|
self.restart_interaction = True
|
|
|
|
renpy.plog(1, "end interact_core")
|
|
|
|
# print("It took", frames, "frames.")
|
|
|
|
def timeout(self, offset):
|
|
if offset < 0:
|
|
return
|
|
|
|
if self.timeout_time:
|
|
self.timeout_time = min(self.event_time + offset, self.timeout_time)
|
|
else:
|
|
self.timeout_time = self.event_time + offset
|
|
|
|
def finish_pending(self):
|
|
"""
|
|
Called before a quit or restart to finish any pending work that might
|
|
block other threads.
|
|
"""
|
|
|
|
self.check_background_screenshot()
|