Compare commits

...

1 Commits

Author SHA1 Message Date
tom eb60f83e4d Tagging 4.8
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/tags/renpy-4.8@81 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-22 15:13:32 +00:00
32 changed files with 2635 additions and 1234 deletions
+100
View File
@@ -1,3 +1,103 @@
New in 4.8
----------
A new system was implemented to manage widget focus. This new system
allows the focus to be moved from widget to widget using the keyboard,
without requiring the keymouse behavior be used. Because of this new
system, the keymouse behavior is now a no-op.
To simplify the code and make every widget compatible with the new
focus code, a number of widgets were reimplemented in terms of
buttons. This includes the ui.menu widget, which is the widget that is
used when presenting the user with choices. Menu buttons are of the
new style style.menu_choice_button, while their labels remain of style
style.menu_choice. Background images and padding can now be styled
onto these menu buttons.
A number of new image manipulators have been made available in the
new im package. These are objects that can load an image (im.Image),
crop an image (im.Crop), scale images (im.Scale), rotozoom images
(im.Rotozoom) and composite images (im.Composite). The latter is much
more flexible than the previous image composition system, as it now
supports placing images at arbitrary locations on an arbitrarily big
canvas. Image (by itself) is now a function that creates the
appropriate image manipulators, retaining compatibility with its
behavior in previous releases.
The image cache is now based on image size, rather than number of
images. This prevents a large number of small images from filling up
the cache.
We now load images before starting the timer for a transition. This
prevents us from popping into the middle of a transition because we
spent some time loading images. This behavior can be controlled by
config.load_before_transition.
The ui.imagemap widget has been rewritten in terms of ui.imagebuttons
and im.Crops. Some styles have changed names as a result.
Ren'Py now keeps track of the images that have been shown to the
user. This is the basis of this release's new extra, gallery.rpy,
which manages a gallery of unlockable CG.
The audio system has been rewritten, and a number of low-level audio
functions have been exposed in the new audio file. These functions
control audio directly, but do not save the state of audio in a save
file. The high-level music functions have been rewritten and placed in
the music.rpy file, allowing a sufficently adventurous game author to
change the way music is played.
Two changes have been made to the renpy.music_start() function in the
high-level sound api. The startpos argument was removed (as it was a
pain to support), and a new fadeout argument was added, which lets the
fadeout time be controlled for each new song that is played.
The low-level sound api now lets us do the following things:
[list][*] Play up to eight sound effects at once.
[*] Cancel a playing sound effect.
[*] Pause and unpause the playing music.
[/list]
There is now a new, state that a widget can be in. Along with hover,
idle, and activate, a widget can now be insensitive. This is the state
that all non-focused widgets are in. Having the new insensitive state
means that we can eliminate the various disabled_button styles, so we
did.
In general, in this release, many styles have changed names.
In prior versions of Ren'Py, the bar widget could respond to
clicks. This was never used, and won't work well with the
keyboard focus control system. So, it's been eliminated.
Now, if the label "enter_game_menu" exists, it is called when entering
the game menu. It's expected that the main purpose of this label will
be to play music when in the game menu. Using the default music
system, the code to do this would look like:
[code]
label enter_game_menu:
$ renpy.music_start("game_menu_music.mid")
return
[/code]
A number of bugs were fixed, including bugs with the style system, and
with animation refresh.
Tab characters are now expanded to tab stop at eight spaces, rather
than to a fixed spacing of eight spaces. Old code using tabs may need
to be reformatted.
The 'h' key now hides the windows on the screen. This means that all
Ren'Py functionality is now available through the keyboard, at least
for people with a 1-button mouse. (cough ... Mac ... cough)
Finally, setting the environment variable RENPY_DISABLE_FULLSCREEN to
a non-empty value will disable fullscreen support, for those people
(that person?) who had it crash their systems.
New in 4.7.2
------------
+9 -6
View File
@@ -113,11 +113,11 @@ init -500:
style = type
if selected:
if selected and not disabled:
style += "_selected"
if disabled:
style += "_disabled"
clicked = None
style = style + "_button"
text_style = style + "_text"
@@ -302,7 +302,7 @@ init -500:
ui.fixed()
ui.window(style='gm_nav_window')
ui.vbox()
ui.vbox(focus='gm_nav')
for key, label, target, enabled in library.game_menu:
@@ -311,7 +311,7 @@ init -500:
if not eval(enabled):
disabled = True
clicked = ui.returns(None)
clicked = None
_button_factory(label, "gm_nav", selected=(key==selected),
disabled=disabled, clicked=clicked)
@@ -331,8 +331,8 @@ init -500:
clicked=ui.returns(("return", (name, False)))
enable_hover = True
else:
clicked=None
enable_hover = False
clicked = None
enable_hover = True
ui.button(style='file_picker_entry',
clicked=clicked,
@@ -517,6 +517,9 @@ label _enter_game_menu:
if library.enter_transition:
$ renpy.transition(library.enter_transition)
if renpy.has_label("enter_game_menu"):
call enter_game_menu
return
# Entry points from the game into menu-space.
+121
View File
@@ -0,0 +1,121 @@
# This file contains code to manage the playing of music. It's here,
# in an rpy file, because we now optionally let the user override all
# of this to implement his or her own music system, using calls to
# various functions found in audio.
init -1000:
python hide:
config.debug_sound = True
# This becomes renpy.music_start.
def music_start(filename, loops=-1, fadeout=None):
"""
This starts music playing. If a music track is already
playing, this pauses that music track in favor of this
one.
@param filename: The file that the music will be played from.
This is relative to the game directory, and must be a real
file (so it cannot be stored in an archive).
@param loops: The number of times the music will loop
after it finishes playing for the first time. This is
made somewhat less accurate by rollback and loading. If
this number is less than zero, the song will loop
forever.
@param fadeout: If this parameter is not None, it is
interpreted as a time in seconds, which gives how long
the fade will take.
"""
if loops >= 0:
loops += 1
ctx = renpy.context()
ctx._music_name = filename
ctx._music_loops = loops
if fadeout and audio.music_enabled() and not config.skipping:
audio.music_fadeout(fadeout)
def music_stop():
"""
This stops the currently playing music track.
"""
ctx = renpy.context()
ctx._music_name = None
ctx._music_loops = None
renpy.music_start = music_start
renpy.music_stop = music_stop
# This is called once for each interaction, to ensure that the
# music is appropriate for that interaction.
def music_interact():
if not audio.music_enabled():
return
ctx = renpy.context()
if not hasattr(ctx, '_music_name'):
ctx._music_name = None
ctx._music_loops = None
playing, queued = audio.music_filenames()
# If music is disabled, ensure that it is stopped.
if not _preferences.music:
if playing:
audio.music_stop()
return
# If we do not match what is playing, stop what's currently
# playing and get ready to play something else.
if ctx._music_name != playing:
# If we're not playing anything, immediately start the
# new track by calling music_end_event.
if not playing:
config.music_end_event()
# Otherwise, we will start a fade to the new music if no
# such fade is already in progress, and we're not skipping.
elif not audio.music_fading() and not config.skipping:
audio.music_fadeout(config.fade_music)
# This is called whenever a track of music ends, or also from the
# above when we want to start a new track when nothing else is
# playing.
def music_end_event():
if not _preferences.music:
return
ctx = renpy.context()
playing, queued = audio.music_filenames()
if not hasattr(ctx, '_music_name'):
ctx._music_name = None
ctx._music_loops = None
if not ctx._music_name:
return
if not playing and ctx._music_loops:
ctx._music_loops -= 1
audio.music_play(ctx._music_name)
if not queued and ctx._music_loops:
ctx._music_loops -= 1
audio.music_queue(ctx._music_name)
config.music_interact = music_interact
config.music_end_event = music_end_event
+16 -38
View File
@@ -66,8 +66,7 @@ init -1000:
style.default.yanchor = 'top'
# Sound properties.
style.default.hover_sound = None
style.default.activate_sound = None
style.default.sound = None
# The base style for the large windows.
style.create('window', 'default',
@@ -123,13 +122,18 @@ init -1000:
# Styles that are used for menus.
style.create('menu', 'default',
"(sound, position) The style that is used for menus themselves.")
"(position) The style that is used for the vbox containing a menu.")
style.create('menu_caption', 'default',
"(text) The style that is used to render a menu caption.")
style.create('menu_choice', 'default',
"""(text, hover, sound) The style that is used to render a menu choice.""")
"""(text, hover) The style that is used to render
the text of a menu choice.""")
style.create('menu_choice_button', 'default',
"""(window, hover, sound) The style that is used
to render the button containing a menu choice.""")
style.menu_choice.hover_color = (255, 255, 0, 255) # yellow
style.menu_choice.activate_color = (255, 255, 0, 255) # yellow
@@ -175,9 +179,12 @@ init -1000:
# Styles that are used by imagemaps
style.create('imagemap', 'image_placement',
'(sound, position) The style that is used for imagemaps.')
'(position) The style that is used for imagemaps.')
# Style that is used by imagebutttons.
style.create('imagemap_button', 'default',
'(window, sound, hover) The style that is used for buttons inside imagemaps.')
# Styles that are used by imagebutttons.
style.create('image_button', 'default',
'(window, sound, hover) The default style used for image buttons.')
@@ -201,6 +208,7 @@ init -1000:
style.button_text.color = dark_cyan
style.button_text.hover_color = bright_cyan
style.button_text.activate_color = bright_cyan
style.button_text.insensitive_color = (192, 192, 192, 255)
style.button_text.drop_shadow = (2, 2)
# Selected button.
@@ -214,30 +222,12 @@ init -1000:
style.selected_button_text.hover_color = bright_red
style.selected_button_text.activate_color = bright_red
# Disabled button.
style.create('disabled_button', 'button',
'(window, hover) The style that is used for a disabled button.')
style.disabled_button.hover_sound = None
style.disabled_button.activate_sound = None
style.create('disabled_button_text', 'button_text',
'(text, hover) The style that is used for the label of a disabled button.')
style.disabled_button_text.color = (128, 128, 128, 255)
style.disabled_button_text.hover_color = (128, 128, 128, 255)
style.disabled_button_text.activate_color = (128, 128, 128, 255)
# Bar.
style.create('bar', 'default',
'(bar) The style that is used by default for bars.')
style.bar.left_bar = Solid(bright_cyan)
style.bar.right_bar = Solid((0, 0, 0, 128))
style.bar.left_gutter = 0
style.bar.right_gutter = 0
# Styles that are used when laying out the main menu.
style.create('mm_root_window', 'default',
@@ -286,13 +276,6 @@ init -1000:
style.create('gm_nav_selected_button_text', 'selected_button_text',
'(text, hover) The style of the text of a selected game menu navigation button.')
style.create('gm_nav_disabled_button', 'disabled_button',
'(window, hover) The style of a disabled game menu navigation button.')
style.create('gm_nav_disabled_button_text', 'disabled_button_text',
'(text, hover) The style of the text of a disabled game menu navigation button.')
style.create('file_picker_window', 'default',
'(window, position) A window containing the file picker that is used to choose slots for loading and saving.')
@@ -312,11 +295,6 @@ init -1000:
'(window, hover) The style that is used for enabled file picker navigation buttons.')
style.create('file_picker_nav_button_text', 'button_text',
'(text) The style that is used for the label of enabled file picker navigation buttons.')
style.create('file_picker_nav_disabled_button', 'disabled_button',
'(window, hover) The style that is used for disabled file picker navigation buttons.')
style.create('file_picker_nav_disabled_button_text', 'disabled_button_text',
'(text) The style that is used for the label of disabled file picker navigation buttons.')
style.create('file_picker_entry', 'button',
'(window, hover) The style that is used for each of the slots in the file picker.')
@@ -327,7 +305,7 @@ init -1000:
style.file_picker_entry.xminimum = 400
style.file_picker_entry.ymargin = 2
style.file_picker_entry.idle_background = Solid((255, 255, 255, 255))
style.file_picker_entry.background = Solid((255, 255, 255, 255))
style.file_picker_entry.hover_background = Solid((255, 255, 192, 255))
style.file_picker_entry.activate_background = Solid((255, 255, 192, 255))
@@ -335,7 +313,7 @@ init -1000:
'(text) A base style for all text that is displayed in the file picker.')
style.file_picker_text.size = 18
style.file_picker_text.idle_color = dark_cyan
style.file_picker_text.color = dark_cyan
style.file_picker_text.hover_color = bright_cyan
style.create('file_picker_new', 'file_picker_text',
+9
View File
@@ -1,5 +1,6 @@
#!/usr/bin/python
import os
import re
import sys
import time
@@ -132,6 +133,14 @@ def main():
s = f.read()
f.close()
os.chdir("..")
try:
renpy.main.main("dump_styles")
except "foo":
pass
os.chdir("doc")
s = re.sub(r"<!-- func (\S+) -->", function, s)
s = re.sub(r"<!-- include (\S+) -->", include, s)
+158 -57
View File
@@ -1722,14 +1722,16 @@ e "Pleased to meet you, %(name)s."
<!-- func renpy.watch -->
<!-- func renpy.windows -->
<!-- func renpy.loadable -->
<!-- func renpy.exists -->
<!-- func renpy.context -->
<!-- func renpy.clear_game_runtime -->
<!-- func renpy.get_game_runtime -->
<!-- func color -->
<p>
@@ -2020,10 +2022,11 @@ init:
stdout (wherever that may go to).
</var>
<var name="config.image_cache_size" value="10">
This is the number of images that can be in the image cache at
once. This is mostly a suggestion, as if Ren'Py ever needs to
display more than this number of images at once, it will cache
<var name="config.image_cache_size" value="8">
This is used to set the size of the image cache, as a multiple of the
screen size. This is mostly a suggestion, as if Ren'Py ever needs to
display more than this amount of images at once, it will cache
them all. But it will stop predictively loading images when the
cache is filled beyond this, and it will try to drop images from
the cache until it shrinks below this size. If this is too
@@ -2047,6 +2050,11 @@ init:
the image cache change.
</var>
<var name="config.load_before_transitions" value="True">
If True, the start of transitions will be delayed until the images
used by said transitions have finished loading.
</var>
<var name="config.allow_skipping" value="True">
If set to False, the user is not able to skip over the text of the
game.
@@ -2079,9 +2087,9 @@ load scripts, as scripts will be loaded before it can be set.
is slightly increased.
</var>
<var name="config.keymouse_distance" value="5">
This gives the number of pixels that the mouse is moved every
1/20th of a second when the keymouse is being used.
<var name="config.focus_crossrange_penalty" value="1024">
This is the amount of penalty to apply to moves perpendicular to the
selected direction of motion, when moving focus with the keyboard.
</var>
<var name="config.sound_sample_rate" value="44100">
@@ -2560,12 +2568,7 @@ top_padding and bottom_padding to the same value.
<p>
The ui.bar() widget has a few properties that are specific to
it. These properties control the look of the bars, and the size of
the gutters on either side of a clickable widget. The gutters are
empty space on the left and right of a clickable bar, and serve to
make it easy to set the maximum and minimum value of the bar
widget. Clicking on the left gutter sets the minimum value, while the
right gutter sets the maximum value.
it, that control the look of the bars.
</p>
<prop name="left_bar">
@@ -2580,29 +2583,22 @@ Displayable needs to be one that can change its size, such as a Solid
or a Frame.
</prop>
<prop name="left_gutter">
The size of the left gutter of a clickable bar, in pixels.
</prop>
<prop name="right_gutter">
The size of the left gutter of a clickable bar, in pixels.
</prop>
<h4>Hovering</h4>
<p>
In Ren'Py, buttons and their contents support the idea of hovering:
using different sets of properties for different states of the
widget. If the widget has been selected, it is considered to be
activated. Otherwise, if the mouse is over the widget, then that widget
is hovered, else it is idle. On these widgets, Ren'Py will look up
a property prefixed with "activate_", "hover_", "idle_", (as appropriate) in a
style, before looking up an unprefixed property. If neither form
is found on a style, the process is repeated again on the parent
style. (The search pattern for background on a hovered button
goes: button.hover_background, button.background,
In Ren'Py, buttons and their contents support the idea of
hovering: using different sets of properties for different states
of the widget. If the widget cannot be given focus, it is
considered to be insensitive. If the widget has been selected by
the user, it is considered to be activated. Otherwise, if the
widget has focus, then that widget is hovered, else it is idle. On
these widgets, Ren'Py will look up a property prefixed with
"insensitive_", "activate_", "hover_", or "idle_" (as appropriate)
in a style, before looking up an unprefixed property. If neither
form is found on a style, the process is repeated again on the
parent style. (The search pattern for background on a hovered
button goes: button.hover_background, button.background,
default.hover_background, default.background.)
</p>
@@ -2655,7 +2651,7 @@ init:
init:
$ style.button.color = (128, 255, 128, 255)
$ style.button.xpos = 0
$ style.button_idled.background = \
$ style.button.idle_background = \
renpy.Frame(renpy.Image("button_idled.png"),
xborder=10, yborder=10)
$ style.button_hover.background = \
@@ -2954,31 +2950,26 @@ Ren'Py, and as of version 4.5 is as follows:
<example>
keymap = dict(
# Bindings present almost everywhere, unless explicitly
# disabled.
rollback = [ 'K_PAGEUP', 'mousedown_4' ],
screenshot = [ 's' ],
toggle_fullscreen = [ 'f' ],
toggle_music = [ 'm' ],
game_menu = [ 'K_ESCAPE', 'mousedown_3' ],
hide_windows = [ 'mouseup_2' ],
game_menu = [ 'K_ESCAPE', 'mouseup_3' ],
hide_windows = [ 'mouseup_2', 'h' ],
# Say.
rollforward = [ 'mousedown_5', 'K_PAGEDOWN' ],
dismiss = [ 'mouseup_1', 'K_RETURN', 'K_SPACE', 'K_KP_ENTER' ],
# Keymouse.
keymouse_left = [ 'K_LEFT' ],
keymouse_right = [ 'K_RIGHT' ],
keymouse_up = [ 'K_UP' ],
keymouse_down = [ 'K_DOWN' ],
# Menu.
menu_mouseselect = [ 'mouseup_1' ],
menu_keyselect = ['K_RETURN', 'K_KP_ENTER' ],
menu_keyup = [ 'K_UP' ],
menu_keydown = [ 'K_DOWN' ],
# Focus.
focus_left = [ 'K_LEFT' ],
focus_right = [ 'K_RIGHT' ],
focus_up = [ 'K_UP' ],
focus_down = [ 'K_DOWN' ],
# Button.
button_select = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER' ],
@@ -2986,12 +2977,6 @@ keymap = dict(
input_backspace = [ 'K_BACKSPACE' ],
input_enter = [ 'K_RETURN', 'K_KP_ENTER' ],
# Imagemap.
imagemap_select = [ 'K_RETURN', 'K_KP_ENTER', 'mouseup_1' ],
# Bar.
bar_click = [ 'mouseup_1' ],
# These keys control skipping.
skip = [ 'K_LCTRL', 'K_RCTRL' ],
toggle_skip = [ 'K_TAB' ],
@@ -3079,6 +3064,18 @@ changed by a call to ui.layer(), followed by a matching call to
ui.close() when done adding widgets to the new layer.
</p>
<p>
Widgets can be added to a focus group by supplying the name of the
focus group as the focus parameter to a ui widget. If the same focus
group exists on two interactions, Ren'Py will ensure that if the nth
widget is selected in the first interaction, the nth widget will be
given focus at the start of the second interaction. If no widget is
given focus in this way, then Ren'Py looks for one that has had the
default=True argument supplied to it. If it finds such a widget, then
it focuses it. Otherwise, it uses fallback rules to determine if a
widget should be given focus.
</p>
<p>
The following are the functions available in the ui module, which is
automatically present in the game namespace.
@@ -3122,8 +3119,6 @@ automatically present in the game namespace.
<!-- func ui.pausebehavior -->
<!-- func ui.keymousebehavior -->
<h4>Functions</h4>
<!-- func ui.interact -->
@@ -3151,6 +3146,112 @@ relying on this function.)
</p>
</function>
<h3>Image Manipulators</h3>
<p>
Image manipulators are objects that can return images. Every image
manipulator can be used as a widget, but the opposite is not
true. Image manipulators are used to implement the Image function,
but constructing them directly can give you finer control.
</p>
<p>
When an image manipulator requires another as input, the second image
manipulator can be specified in any of a number of ways. The first way
is as a string, which is turned into an im.Image. The second is a
tuple, which is turned into an im.Composite using the rules for tuples
given in Image. The third is that an image manipulator can be
specified directly, in which case it is passed through unchanged.
</p>
<p>
Widgets may not be used as arguments to image manipulators. This is
because image manipulators are very efficent, caching the computed
images on their first load.
</p>
<p>
The image manipulators are:
</p>
<!-- func im.Image -->
<!-- func im.Crop -->
<!-- func im.Composite -->
<!-- func im.Scale -->
<!-- func im.Rotozoom -->
<h3>Low-Level Audio Functions</h3>
<p>
These functions manipulate Ren'Py audio subsystem directly. Unlike the
high-level music functions, these functions do not manipulate
persistent state. As a result, calls to these functions will not
persistent across sessions.
</p>
<p>
Still, these functions are useful to allow access to behavior that
is not yet exposed through the high-level interface. They are also
useful for a person who wants to change the high-level music or sound
interfaces. So, we've documented them here.
</p>
<h4>Sound Functions</h4>
<!-- func audio.sound_enabled -->
<!-- func audio.sound_play -->
<!-- func audio.sound_stop -->
<h4>Music Functions</h4>
<p>
The low-level music system deals with two music files at once, a
playing file and a queued file. When the playing file finishes
playing, the queued file is begun, and queued file becomes
empty.
</p>
<!-- func audio.music_enabled -->
<!-- func audio.music_play -->
<!-- func audio.music_queue -->
<!-- func audio.music_stop -->
<!-- func audio.music_fadeout -->
<!-- func audio.music_fading -->
<!-- func audio.music_filenames -->
<!-- func audio.music_pause -->
<!-- func audio.music_unpause -->
<h4>Music Callbacks</h4>
<var name="config.music_interact" value="...">
This callback is called (without any arguments) at the start (or
restart) of each interaction. It's intended to allow the system to
restore music after a context change, or to implement changes in
the music that occured before that interaction.
</var>
<var name="config.music_end_event" value="...">
This callback is called (without any arguments) when a track has
finished playing. If a second track has been queued, it will have
started before this callback is called. Usually, this callback is
used to queue up another track when one ends. It can also be used
to start the music playing again after a fadeout.
</var>
<h3>Function Index</h3>
+4 -3
View File
@@ -1,7 +1,8 @@
init:
image black = Solid((0, 0, 0, 255))
label start:
label main_menu:
$ style._write_docs("doc/styles.xml")
"Dumped style."
$ renpy.renpy.style.write_docs("doc/styles.xml")
$ raise "foo"
+262
View File
@@ -0,0 +1,262 @@
# This extra implements an image gallery, complete with automatic
# unlocking of the images that have been shown to the user. The images
# are divided into pages, with a fixed number of images on each page.
# Right now, this is configured to used the images found in the demo.
# The images are repeated quite a bit, so that we can get four pages
# of them. You probably wouldn't do that if you were using this in a
# real game.
# To see this in action, drop it into the game directory of the
# demo.
# Configuration.
init:
python hide:
# The number of columns and rows of images to show in the
# gallery.
store.gallery_cols = 3
store.gallery_rows = 4
# The size that each image should be scaled to, and that
# thumbnails should be.
store.gallery_width = 160
store.gallery_height = 120
# The contents of each of the page. Each of these is a list of
# tuples with the first element of the tuple being the image
# filename and the second element being the name of the image
# that will unlock the image with this filename. (That is,
# the name that is used in show or scene statements, as a
# string.)
#
# When displaying an image as a thumbnail, this code first
# looks for the file thumbnail_<filename>. If that file
# exists, it should be a gallery_width x gallery_height
# thumbnail. Otherwise, a thumbnail is automatically
# generated, but it may screw up the aspect ratio of the
# image.
#
# You probably want to create thumbnails for most images, to
# limit memory consumption.
page1 = [
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
]
page2 = [
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
]
page3 = [
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
]
page4 = [
( "9a_happy.png", "eileen happy" ),
( "9a_vhappy.png", "eileen vhappy" ),
( "9a_concerned.png", "eileen concerned" ),
]
# This is the actual list of gallery pages. It's a list
# of tuples, with the first element being the name of the
# page, the second being the contents of the page (one of
# the lists created above), and the third image being the
# image used as the background of the page.
store.gallery_pages = [
("Backgrounds 1", page1, "washington.jpg"),
("Backgrounds 2", page2, "whitehouse.jpg"),
("Backgrounds 3", page3, "carillon.jpg"),
("Character Art", page4, "washington.jpg"),
]
# A window containing the gallery page buttons.
style.create('gallery_pages', 'default')
style.gallery_pages.xpos = 0.99
style.gallery_pages.xanchor='right'
style.gallery_pages.ypos = 0.02
style.gallery_pages.yanchor = 'top'
# A button that links to a gallery page.
style.create('gallery_page_button', 'button')
style.create('gallery_page_button_text', 'button_text')
# A button that returns us to from whence we came.
style.create('gallery_return_button', 'button')
style.create('gallery_return_button_text', 'button_text')
style.gallery_return_button.xpos = 0.99
style.gallery_return_button.xanchor='right'
style.gallery_return_button.ypos = 0.98
style.gallery_return_button.yanchor = 'bottom'
# The grid containing the gallery image buttons.
style.create('gallery_grid', 'default')
# The style of the buttons in the gallery.
style.create('gallery_button', 'default')
# Right now, the backgrounds are all solids, but in a more
# professional version, the insensitive background would
# probably be a placeholder image that indicates that a
# picture has yet to be unlocked.
style.gallery_button.insensitive_background = Solid((192, 192, 192, 255))
style.gallery_button.idle_background = Solid((255, 255, 255, 255))
style.gallery_button.hover_background = Solid((255, 255, 192, 255))
style.gallery_button.left_margin = 5
style.gallery_button.right_margin = 5
style.gallery_button.top_margin = 5
style.gallery_button.bottom_margin = 5
style.gallery_button.left_padding = 5
style.gallery_button.right_padding = 5
style.gallery_button.top_padding = 5
style.gallery_button.bottom_padding = 5
# The style of the images in the buttons in the gallery.
style.create('gallery_button_image', 'default')
# The style of the images that are being shown to the user.
style.create('gallery_image', 'image_placement')
# The transition used when switching gallery pages.
store.gallery_transition = Dissolve(0.5)
python:
# The function that actually manages the display of the image
# gallery.
def gallery():
page = 0
while True:
images = gallery_pages[page][1]
ui.image(gallery_pages[page][2])
# Show the names of the various gallery pages.
ui.window(style='gallery_pages')
ui.vbox(focus="gallery_pages")
for i in range(0, len(gallery_pages)):
if i == page:
clicked = None
else:
clicked = ui.returns(("page", i))
ui.textbutton(gallery_pages[i][0],
style='gallery_page_button',
text_style='gallery_page_button_text',
clicked=clicked)
ui.close()
# Show the return button.
ui.textbutton('Return',
style='gallery_return_button',
text_style='gallery_return_button_text',
clicked=ui.returns(("return", None)))
# Show the grid for this page.
ui.grid(gallery_cols, gallery_rows, style='gallery_grid')
# For each grid cell.
for i in range(0, gallery_cols * gallery_rows):
# Fill empty space with nulls.
if i >= len(images):
ui.null()
continue
# Otherwise, get the filename and spec and see if
# we've unlocked it.
filename, spec = images[i]
if spec:
spec = spec.split()
if spec and tuple(spec) not in persistent._seen_images:
filename = None
clicked = None
else:
clicked = ui.returns(('show', filename))
# Create the button, containing the appropriate
# image or a null if we haven't unlocked it yet.
ui.button(style='gallery_button', clicked=clicked)
if not filename:
ui.null(width=gallery_width, height=gallery_height)
else:
if renpy.loadable("thumbnail_" + filename):
ui.image("thumbnail_" + filename,
style="gallery_button_image")
else:
ui.add(im.Scale(filename,
gallery_width,
gallery_height))
ui.close()
# Interact with the user.
renpy.transition(gallery_transition)
cmd, arg = ui.interact(suppress_overlay=True, suppress_underlay=True)
# Process the user's commands.
if cmd == "show":
ui.add(Solid((0, 0, 0, 255)))
ui.image(arg)
ui.saybehavior()
renpy.transition(gallery_transition)
ui.interact(suppress_overlay=True, suppress_underlay=True)
if cmd == "page":
page = arg
if cmd == "return":
renpy.transition(gallery_transition)
return
library.main_menu.insert(2, ( "CG Gallery", "gallery"))
label gallery:
$ gallery()
jump _main_menu
+6 -4
View File
@@ -2,9 +2,9 @@
# order.
# Some version numbers and things.
version = "Ren'Py 4.7.2"
script_version = 7
savegame_suffix = "-7.save"
version = "Ren'Py 4.8"
script_version = 8000
savegame_suffix = "-8.save"
# Can be first, because has no dependencies, and may be imported
@@ -32,8 +32,10 @@ import renpy.display.text # core
import renpy.display.layout # core
import renpy.display.behavior # layout
import renpy.display.transition # core
import renpy.display.image # core, behavior
import renpy.display.im
import renpy.display.image # core, behavior, im
import renpy.display.video
import renpy.display.focus
import renpy.ui
+4
View File
@@ -286,6 +286,10 @@ def imspec_common(imspec, hide=False):
for i in at_list:
img = renpy.python.py_eval(i)(img)
# Update the set of images that have ever been seen.
if not hide:
renpy.game.persistent._seen_images[tuple(name)] = True
return key, img
def predict_imspec(imspec, callback):
+26 -26
View File
@@ -47,9 +47,9 @@ profile = False
# The directory save files will be saved to.
savedir = None
# The number of images that are allowed to live in the image cache
# at once.
image_cache_size = 10
# The number of screens worth of images that are allowed to live in the image
# cache at once.
image_cache_size = 8
# The number of statements we will analyze when doing predictive
# loading. Please note that this is a total number of statements in a
@@ -83,9 +83,6 @@ force_archives = False
# An image file containing the mouse cursor, if one is defined.
mouse = None
# The distance the keyboard moves the mouse, per 50 ms tick, in pixels.
keymouse_distance = 5
# The default sound playback sample rate.
sound_sample_rate = 44100
@@ -113,6 +110,14 @@ overlay_layers = [ 'overlay' ]
# false otherwise.
overlay_during_wait = True
# When using the keyboard to navigate, how much we penalize
# distance out of the preferred direction.
focus_crossrange_penalty = 1024
# If True, then we force all loading to occur before transitions
# start.
load_before_transition = True
# The keymap that is used to change keypresses and mouse events.
keymap = dict(
@@ -123,24 +128,18 @@ keymap = dict(
toggle_fullscreen = [ 'f' ],
toggle_music = [ 'm' ],
game_menu = [ 'K_ESCAPE', 'mouseup_3' ],
hide_windows = [ 'mouseup_2' ],
hide_windows = [ 'mouseup_2', 'h' ],
# Say.
rollforward = [ 'mousedown_5', 'K_PAGEDOWN' ],
dismiss = [ 'mouseup_1', 'K_RETURN', 'K_SPACE', 'K_KP_ENTER' ],
# Keymouse.
keymouse_left = [ 'K_LEFT' ],
keymouse_right = [ 'K_RIGHT' ],
keymouse_up = [ 'K_UP' ],
keymouse_down = [ 'K_DOWN' ],
# Menu.
menu_mouseselect = [ 'mouseup_1' ],
menu_keyselect = ['K_RETURN', 'K_KP_ENTER' ],
menu_keyup = [ 'K_UP' ],
menu_keydown = [ 'K_DOWN' ],
# Focus.
focus_left = [ 'K_LEFT' ],
focus_right = [ 'K_RIGHT' ],
focus_up = [ 'K_UP' ],
focus_down = [ 'K_DOWN' ],
# Button.
button_select = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER' ],
@@ -148,18 +147,19 @@ keymap = dict(
input_backspace = [ 'K_BACKSPACE' ],
input_enter = [ 'K_RETURN', 'K_KP_ENTER' ],
# Imagemap.
imagemap_select = [ 'K_RETURN', 'K_KP_ENTER', 'mouseup_1' ],
# Bar.
bar_click = [ 'mouseup_1' ],
# These keys control skipping.
skip = [ 'K_LCTRL', 'K_RCTRL' ],
toggle_skip = [ 'K_TAB' ],
)
# A function that is called before each interaction, to update the
# music that is currently playing.
music_interact = None
# A function that is called when a music track ends, perhaps to
# play another track.
music_end_event = None
# The number of frames that Ren'Py has shown.
frames = 0
+392 -157
View File
@@ -168,164 +168,9 @@ def init():
playing_midi = False
# This detects if the filename is a midi, and sets playing_midi
# appropriately.
def detect_midi(fn):
if mixer_works:
pygame.mixer.music.set_endevent(renpy.display.core.MUSICEND)
fn = fn.lower()
global playing_midi
playing_midi = fn.endswith(".mid") or fn.endswith(".midi")
# Information about the currently playing track.
current_music = None
def music_delay(offset):
"""
Returns the time left until the current music has been playing for
offset seconds. If music is not playing, return None. May return
a negative time.
"""
if not mixer_works:
return None
mo = pygame.mixer.music.get_pos()
if mo < 0:
return None
mo /= 1000.0
return offset - mo
def music_start(filename, loops=-1, startpos=0.0):
"""
This starts music playing. If a music track is already playing,
stops that track in favor of this one.
@param filename: The file that the music will be played from. This
is relative to the game directory, and must be a real file (so it
cannot be stored in an archive.)
@param loops: The number of times the music will loop after it
finishes playing. If negative, the music will loop indefinitely.
Please note that even once the song has finished, rollback or load
may cause it to start playing again. So it may not be safe to have
this set to a non-negative value.
@param startpos: The number of seconds into the music to start playing.
"""
if not mixer_works:
return
music_stop()
renpy.game.context().scene_lists.music = (filename, loops, startpos)
restore_music()
def music_stop():
"""
Stops the currently playing music track.
"""
if not mixer_works:
return
renpy.game.context().scene_lists.music = None
restore_music()
def restore_music():
"""
This makes sure that the current music matches the music found in
the context.
"""
if not mixer_works:
return
global current_music
global fading
compute_midi_msf()
set_music_volume(1.0)
new_music = renpy.game.context().scene_lists.music
if not renpy.game.preferences.music:
new_music = None
if current_music == new_music:
return
if not mixer_enabled:
return
# Usually, ignore errors.
try:
if current_music != new_music and current_music:
current_music = None
pygame.mixer.music.fadeout(int(renpy.config.fade_music * 1000))
fading = True
else:
if not pygame.mixer.music.get_busy():
fn, loops, startpos = new_music
fading = False
detect_midi(fn)
pygame.mixer.music.load(renpy.game.basepath + "/" + fn)
pygame.mixer.music.play(loops, startpos)
set_music_volume(master_music_volume)
current_music = new_music
except pygame.error, e:
if renpy.config.debug_sound:
raise
else:
print "Error while trying to play music:", str(e)
# Plays sounds.
def play(fn, loops=0):
"""
This plays the given sound. The sound must be in a wav file,
and expected to have a sample rate 44100hz (changable with
config.sound_sample_rate), 16 bit, stereo. These expectations may
be violated, but that may lead to conversion delays.
Once a sound has been started, there's no way to stop it.
@param fn: The name of the file that the sound is read from. This
file may be contained in a game directory or an archive.
@param loops: The number of extra times the sound will be
played. (The default, 0, will play the sound once.)
"""
if not mixer_works:
return
if not fn:
return
if not renpy.game.preferences.sound:
return
if not mixer_enabled:
return
try:
sound = pygame.mixer.Sound(renpy.loader.load(fn))
sound.play()
except:
if renpy.config.debug_sound:
raise
def pre_init():
try:
bufsize = 4096
@@ -356,6 +201,7 @@ def disable_mixer():
if mixer_enabled:
try:
music_stop()
pygame.mixer.quit()
except:
if renpy.config.debug_sound:
@@ -383,3 +229,392 @@ def enable_mixer():
mixer_enabled = True
# This detects if the filename is a midi, and sets playing_midi
# appropriately.
def detect_midi(fn):
fn = fn.lower()
global playing_midi
playing_midi = fn.endswith(".mid") or fn.endswith(".midi")
def music_delay(offset):
"""
Returns the time left until the current music has been playing for
offset seconds. If music is not playing, return None. May return
a negative time.
"""
if not mixer_works:
return None
mo = pygame.mixer.music.get_pos()
if mo < 0:
return None
mo /= 1000.0
return offset - mo
# def music_start(filename, loops=-1, startpos=0.0):
# """
# This starts music playing. If a music track is already playing,
# stops that track in favor of this one.
# @param filename: The file that the music will be played from. This
# is relative to the game directory, and must be a real file (so it
# cannot be stored in an archive.)
# @param loops: The number of times the music will loop after it
# finishes playing. If negative, the music will loop indefinitely.
# Please note that even once the song has finished, rollback or load
# may cause it to start playing again. So it may not be safe to have
# this set to a non-negative value.
# @param startpos: The number of seconds into the music to start playing.
# """
# if not mixer_works:
# return
# music_stop()
# renpy.game.context().scene_lists.music = (filename, loops, startpos)
# restore_music()
# def music_stop():
# """
# Stops the currently playing music track.
# """
# if not mixer_works:
# return
# renpy.game.context().scene_lists.music = None
# restore_music()
# The filename of the currently playing piece of music.
playing_filename = None
# The filename of the currently queued piece of music.
queued_filename = None
# True if the music is in the process of fading out, or
# False otherwise.
fading = False
def music_update_volume():
"""
Sets the volume as appropriate for a midi.
"""
if not playing_filename:
return
detect_midi(playing_filename)
set_music_volume(master_music_volume)
def music_interact():
"""
This is called before each interaction, to update the playing music
(if necessary).
"""
# Call the appropriate config function.
if renpy.config.music_interact:
renpy.config.music_interact()
def music_end_event():
"""
This is called by renpy.display.core when a track of music has
endend.
"""
if not music_enabled():
return
try:
# shift the filenames.
global playing_filename
global queued_filename
global fading
playing_filename = queued_filename
queued_filename = None
fading = False
music_update_volume()
# Call the appropriate function.
if renpy.config.music_end_event:
renpy.config.music_end_event()
except:
if renpy.config.debug_sound:
raise
def music_enabled():
"""
This should be called to check to see if music is enabled. If this
returns False, then no music call should be made. Please note that
this does not check preferences.music_enabled, so user code should
also check that to see if music should be played.
This will return True if the mixer works and it has not been
pre-empted by the video player, and False otherwise.
If this does not return True, none of the other music functions should
be called.
"""
return mixer_works and mixer_enabled
def music_play(filename):
"""
This causes the named music filename to be loaded in and played.
Music loaded in this way immediately replaces the currently
playing music.
The track is played once, and then stops. It's up to the higher-level
music layer to ensure that a track that needs to be looped actually
is.
The filename must refer to a real file, and not a file hidden in an
archive.
"""
if not music_enabled():
return
try:
global playing_filename
global queued_filename
global fading
playing_filename = filename
queued_filename = None
fading = False
pygame.mixer.music.load(renpy.loader.transfn(filename))
if not pygame.mixer.music.get_busy():
pygame.mixer.music.play()
music_update_volume()
except:
if renpy.config.debug_sound:
raise
def music_queue(filename):
"""
This causes the given filename to be placed into the queue, to be
played immediately after the currently playing track finishes.
The filename must refer to a real file, and not a file hidden in an
archive.
"""
if not music_enabled():
return
try:
global queued_filename
queued_filename = filename
pygame.mixer.music.queue(renpy.loader.transfn(filename))
except:
if renpy.config.debug_sound:
raise
def music_stop():
"""
This causes the music to be stopped immediately.
"""
if not music_enabled():
return
try:
global playing_filename
global queued_filename
global fading
playing_filename = None
queued_filename = None
fading = False
pygame.mixer.music.stop()
except:
if renpy.config.debug_sound:
raise
def music_fadeout(seconds):
"""
This causes the music to be faded out over a period of
time.
"""
if not music_enabled():
return
try:
global queued_filename
global fading
queued_filename = None
fading = True
pygame.mixer.music.fadeout(int(1000 * seconds))
except:
if renpy.config.debug_sound:
raise
def music_filenames():
"""
Returns a tuple giving the currently playing music filename and
the filename of the track in the queue. It returns None if there
is no filename in either slot.
"""
if not music_enabled():
return None, None
try:
return playing_filename, queued_filename
except:
if renpy.config.debug_sound:
raise
else:
return None, None
def music_fading():
"""
Returns True if the music is in the process of fading out, or False
otherwise.
"""
return fading
def music_pause():
"""
Causes the currently playing music to be paused.
"""
if not music_enabled():
return
try:
pygame.mixer.music.pause()
except:
if renpy.config.debug_sound:
raise
def music_unpause():
"""
Causes the currently playing music to be unpaused, if it is
currently paused.
"""
if not music_enabled():
return
try:
pygame.mixer.music.unpause()
except:
if renpy.config.debug_sound:
raise
def sound_enabled():
"""
Returns True if it's possible to play sound, or False if sound
should not be played.
Please note that this does not check preferences.sound. It's up
to higher-level code (like renpy.play) to do that.
"""
return mixer_works and mixer_enabled
def sound_play(filename, loops=0, channel=0):
"""
This causes the sound contained in the given filename to be played.
@param loops: The number of extra times that the sound will be
played. If -1, the sound is played forever (until stopped).
@param channel: The channel that the sound will be played on, an
integer from 0 to 7. This allows us to support playing up to 8
sounds at once.
"""
if not sound_enabled():
return
try:
chan = pygame.mixer.Channel(channel)
chan.play(pygame.mixer.Sound(renpy.loader.load(filename)), loops)
except:
if renpy.config.debug_sound:
raise
def sound_stop(channel=0):
"""
This causes the sound currently playing in the specified channel
to be stopped.
"""
if not music_enabled():
return
try:
chan = pygame.mixer.Channel(channel)
chan.stop()
except:
if renpy.config.debug_sound:
raise
def play(fn, loops=0):
"""
This plays the given sound. The sound must be in a wav file,
and expected to have a sample rate 44100hz (changeable with
config.sound_sample_rate), 16 bit, stereo. These expectations may
be violated, but that may lead to conversion delays.
Once a sound has been started, there's no way to stop it.
@param fn: The name of the file that the sound is read from. This
file may be contained in a game directory or an archive.
@param loops: The number of extra times the sound will be
played. (The default, 0, will play the sound once.)
This plays the sound on channel 0.
"""
if not fn:
return
if not renpy.game.preferences.sound:
return
sound_play(fn, loops=loops)
+50 -291
View File
@@ -111,35 +111,6 @@ class Keymap(renpy.display.layout.Null):
# def render(self, width, height, st):
# return None
class KeymouseBehavior(renpy.display.layout.Null):
"""
This is a class that causes the keyboard to move the mouse. It's
useful on the game and key menus, as well as in imagemaps and the
like.
"""
def event(self, ev, x, y):
if ev.type == renpy.display.core.DISPLAYTIME:
pressed = pygame.key.get_pressed()
x, y = pygame.mouse.get_pos()
ox, oy = x, y
if is_pressed(pressed, "keymouse_left"):
x -= renpy.config.keymouse_distance
if is_pressed(pressed, "keymouse_right"):
x += renpy.config.keymouse_distance
if is_pressed(pressed, "keymouse_up"):
y -= renpy.config.keymouse_distance
if is_pressed(pressed, "keymouse_down"):
y += renpy.config.keymouse_distance
if (x, y) != (ox, oy):
pygame.mouse.set_pos((x, y))
return None
class PauseBehavior(renpy.display.layout.Null):
"""
This is a class implementing the Pause behavior, which is to
@@ -168,10 +139,11 @@ class SayBehavior(renpy.display.layout.Null):
mouse button.
"""
def __init__(self):
super(SayBehavior, self).__init__()
focusable = True
def __init__(self, default=True, **properties):
super(SayBehavior, self).__init__(default=default, **properties)
def event(self, ev, x, y):
if ev.type == renpy.display.core.DISPLAYTIME and \
@@ -183,8 +155,7 @@ class SayBehavior(renpy.display.layout.Null):
elif renpy.game.context().seen_current(True):
return True
if map_event(ev, "dismiss"):
if map_event(ev, "dismiss") and self.is_focused():
return True
if map_event(ev, "rollforward"):
@@ -192,211 +163,70 @@ class SayBehavior(renpy.display.layout.Null):
return True
return None
class Menu(renpy.display.layout.VBox):
def __init__(self, menuitems, style='menu', **properties):
"""
@param menuitems: A list of menuitem tuples. The first element
of each tuple is the string that should be displayed to the
user. The second item is the value that should be returned if
this item is selected, or None to indicate that this item is a
caption.
"""
super(Menu, self).__init__(style=style, **properties)
self.selected = None
self.results = [ ]
# self.caption_style = renpy.style.Style('menu_caption', { })
# self.selected_style = renpy.style.Style('menu_choice', { })
# self.unselected_style = renpy.style.Style('menu_choice', { })
# self.selected_style.set_prefix('hover_')
# self.unselected_style.set_prefix('idle_')
for i, (caption, result) in enumerate(menuitems):
if result is not None:
style = 'menu_choice'
else:
style = 'menu_caption'
self.add(renpy.display.text.Text(caption, style=style))
if self.selected is None and result is not None:
self.selected = i
self.results.append(result)
self.update_styles()
def update_styles(self):
"""
This updates the colors of our children to reflect the
one that has been selected by the user.
"""
for i, (child, result) in enumerate(zip(self.children, self.results)):
# Captions should stay the default text color.
if result is None:
continue
# Actual choices change color if they are selected or not.
if i == self.selected:
child.set_style_prefix('hover_')
else:
child.set_style_prefix('idle_')
def event(self, ev, x, y):
"""
Processes events.
"""
# print ev
# print x, y
old_selected = self.selected
mouse_select = False
# Change selection based on mouse position.
if ev.type == MOUSEMOTION or map_event(ev, "menu_mouseselect"):
target = self.child_at_point(x, y)
if target is None:
return None
if self.results[target] is not None:
self.selected = target
mouse_select = True
# Change selection based on keypress.
if map_event(ev, "menu_keydown"):
selected = self.selected
while selected < len(self.results) - 1:
selected += 1
if self.results[selected] is not None:
self.selected = selected
break
# Change selection based on keypress.
if map_event(ev, "menu_keyup"):
selected = self.selected
while selected > 0:
selected -= 1
if self.results[selected] is not None:
self.selected = selected
break
# If the selected item changed, update the display.
if self.selected != old_selected:
self.children[self.selected].set_style_prefix("hover_")
self.children[old_selected].set_style_prefix("idle_")
renpy.display.audio.play(self.style.hover_sound)
# renpy.display.render.redraw(self, 0)
# Make selection based on keypress or mouse click.
if map_event(ev, "menu_keyselect") or \
(mouse_select and map_event(ev, "menu_mouseselect")):
self.children[self.selected].set_style_prefix("activate_")
renpy.display.audio.play(self.style.activate_sound)
return self.results[self.selected]
return None
class Button(renpy.display.layout.Window):
def __init__(self, child, style='button', clicked=None,
hovered=None, **properties):
super(Button, self).__init__(child, style=style, **properties)
self.style.set_prefix('idle_')
self.activated = False
self.old_hover = False
self.clicked = clicked
self.hovered = hovered
self.focusable = clicked is not None
def render(self, width, height, st):
if self.activated:
self.set_style_prefix('activate_')
elif self.old_hover:
self.set_style_prefix('hover_')
else:
self.set_style_prefix('idle_')
rv = super(Button, self).render(width, height, st)
if self.clicked:
rv.add_focus(self,
None,
self.style.left_margin,
self.style.top_margin,
rv.width - self.style.right_margin,
rv.height - self.style.bottom_margin)
return rv
return super(Button, self).render(width, height, st)
def set_hover(self, hover):
"""
Called when we change from hovered to un-hovered, or
vice-versa.
"""
# if hover:
# self.style.set_prefix('hover_')
# else:
# self.style.set_prefix('idle_')
renpy.display.render.redraw(self, 0)
def event(self, ev, x, y):
# We deactivate on an event.
self.activated = False
if self.activated:
self.activated = False
inside = False
width, height = self.window_size
if x >= 0 and x < width and y >= 0 and y < height:
inside = True
if self.style.enable_hover and ev.type == MOUSEMOTION:
if self.old_hover != inside:
self.old_hover = inside
self.set_hover(inside)
if inside:
if self.hovered:
self.hovered()
renpy.display.audio.play(self.style.hover_sound)
if map_event(ev, "button_select"):
if inside and self.clicked:
renpy.display.audio.play(self.style.activate_sound)
self.activated = True
renpy.display.render.redraw(self, 0)
rv = self.clicked()
if rv is not None:
return rv
if self.focusable:
if self.is_focused():
self.set_style_prefix('hover_')
else:
raise renpy.display.core.IgnoreEvent()
self.set_style_prefix('idle_')
else:
self.set_style_prefix('insensitive_')
# If not focused, ignore all events.
if not self.is_focused():
return None
# If clicked,
if map_event(ev, "button_select") and self.clicked:
self.activated = True
self.set_style_prefix('activate_')
renpy.display.audio.play(self.style.sound)
rv = self.clicked()
if rv is not None:
return rv
else:
raise renpy.display.core.IgnoreEvent()
return None
return super(Button, self).event(ev, x, y)
# Reimplementation of the TextButton widget as a Button and a Text
# widget.
def TextButton(text, style='button', text_style='button_text',
@@ -405,29 +235,6 @@ def TextButton(text, style='button', text_style='button_text',
text = renpy.display.text.Text(text, style=text_style)
return Button(text, style=style, clicked=clicked, **properties)
# class TextButton(Button):
# def __init__(self, text, style='button', text_style='button_text',
# clicked=None):
# self.text_widget = renpy.display.text.Text(text, style=text_style)
# super(TextButton, self).__init__(self.text_widget,
# style=style,
# clicked=clicked)
# self.text_widget.style.set_prefix('idle_')
# def set_hover(self, hover):
# super(TextButton, self).set_hover(hover)
# if hover:
# self.text_widget.style.set_prefix("hover_")
# else:
# self.text_widget.style.set_prefix("idle_")
class Input(renpy.display.text.Text):
"""
@@ -457,13 +264,12 @@ class Input(renpy.display.text.Text):
self.set_text(self.content.replace("{", "{{") + "_")
renpy.display.render.redraw(self, 0)
elif map_event(ev, "input_enter"):
return self.content
elif ev.type == KEYDOWN and ev.unicode:
if ord(ev.unicode[0]) < 32:
return None
return None
if self.length and len(self.content) >= self.length:
raise renpy.display.core.IgnoreEvent()
@@ -488,7 +294,7 @@ class Bar(renpy.display.core.Displayable):
to clicks on that value.
"""
def __init__(self, width, height, range, value, clicked=None,
def __init__(self, width, height, range, value,
style='bar', **properties):
super(Bar, self).__init__()
@@ -500,61 +306,14 @@ class Bar(renpy.display.core.Displayable):
self.range = range
self.value = value
self.clicked = clicked
def event(self, ev, x, y):
if not self.clicked:
return
if not map_event(ev, 'bar_click'):
return
if not (0 <= x < self.width and 0 <= y <= self.height):
return
# print x, y
lgutter = self.style.left_gutter
rgutter = self.style.right_gutter
if x < lgutter:
value = 0
elif x > self.width - rgutter:
value = self.range
else:
barwidth = self.width - lgutter - rgutter
# This makes it easier to select 100%.
x = x - lgutter
x = x + (barwidth / self.range // 2)
value = x * self.range / barwidth
value = max(value, 0)
rv = self.clicked(value)
if rv is not None:
return rv
else:
raise renpy.display.core.IgnoreEvent()
def render(self, width, height, st):
width = self.width
height = self.height
# The amount of space taken up by the bars.
if self.clicked:
lgutter = self.style.left_gutter
rgutter = self.style.right_gutter
else:
lgutter = 0
rgutter = 0
lgutter = 0
rgutter = 0
barwidth = width - lgutter - rgutter
@@ -587,7 +346,7 @@ class Conditional(renpy.display.layout.Container):
self.condition = condition
self.null = renpy.display.layout.Null()
self.state = eval(self.condition, renpy.game.store)
self.state = eval(self.condition, vars(renpy.store))
def render(self, width, height, st):
if self.state:
@@ -597,7 +356,7 @@ class Conditional(renpy.display.layout.Container):
def event(self, ev, x, y):
state = eval(self.condition, renpy.game.store)
state = eval(self.condition, vars(renpy.store))
if state != self.state:
renpy.display.render.redraw(self, 0)
+98 -42
View File
@@ -2,15 +2,16 @@
# window.
import renpy
from renpy.display.render import render
import pygame
from pygame.constants import *
import os
import time
import cStringIO
# KEYREPEATEVENT = USEREVENT + 1
DISPLAYTIME = USEREVENT + 2
MUSICEND = USEREVENT + 3
# The number of msec
DISPLAYTIME_INTERVAL = 50
@@ -33,9 +34,40 @@ class Displayable(renpy.object.Object):
their fields.
"""
def __init__(self):
self.style = None
self.style_prefix = None
focusable = False
def __init__(self, focus=None, default=False, style='default', **properties):
self.style = renpy.style.Style(style, properties)
self.style_prefix = 'insensitive_'
self.focus_name = focus
self.default = default
def find_focusable(self, callback, focus_name):
if self.focusable:
callback(self, self.focus_name or focus_name)
def focus(self, default=False):
"""
Called to indicate that this widget has the focus.
"""
if self.style.enable_hover:
self.set_style_prefix("hover_")
if not default:
renpy.display.audio.play(self.style.sound)
def unfocus(self):
"""
Called to indicate that this widget has become unfocused.
"""
if self.style.enable_hover:
self.set_style_prefix("idle_")
def is_focused(self):
return renpy.game.context().scene_lists.focused is self
def set_style_prefix(self, prefix):
"""
@@ -46,9 +78,7 @@ class Displayable(renpy.object.Object):
if prefix == self.style_prefix:
return
if self.style:
self.style.set_prefix(prefix)
self.style.set_prefix(prefix)
self.style_prefix = prefix
renpy.display.render.redraw(self, 0)
@@ -106,8 +136,8 @@ class Displayable(renpy.object.Object):
the images it may want to load.
"""
return
if self.style and self.style.background:
self.style.background.predict(callback)
def place(self, dest, x, y, width, height, surf):
"""
@@ -195,6 +225,7 @@ class SceneLists(object):
@ivar music: Opaque information about the music that is being played.
@ivar focused: The widget that is currently focused.
"""
def __init__(self, oldsl=None):
@@ -211,6 +242,7 @@ class SceneLists(object):
self.music = oldsl.music
self.sticky_positions = oldsl.sticky_positions.copy()
self.movie = oldsl.movie
self.focused = oldsl.focused
else:
for i in renpy.config.layers:
@@ -219,6 +251,7 @@ class SceneLists(object):
self.music = None
self.movie = None
self.sticky_positions = { }
self.focused = None
def rollback_copy(self):
"""
@@ -231,6 +264,8 @@ class SceneLists(object):
for i in renpy.config.overlay_layers:
rv.layers[i] = [ ]
rv.focused = None
# rv.master = self.master[:]
# rv.transient = self.transient[:]
@@ -350,7 +385,9 @@ class Display(object):
self.fullscreen = renpy.game.preferences.fullscreen
fsflag = 0
if self.fullscreen:
fullscreen = self.fullscreen and not os.environ.get('RENPY_DISABLE_FULLSCREEN', False)
if fullscreen:
fsflag = FULLSCREEN
# The window we display things in.
@@ -372,12 +409,12 @@ class Display(object):
pygame.display.set_caption(renpy.config.window_title)
if renpy.config.window_icon:
pygame.display.set_icon(renpy.display.image.cache.load_image(renpy.config.window_icon))
pygame.display.set_icon(renpy.display.im.load_image(renpy.config.window_icon))
# Load the mouse image, if any.
if renpy.config.mouse:
self.mouse = renpy.display.image.cache.load_image(renpy.config.mouse)
self.mouse = renpy.display.im.load_image(renpy.config.mouse)
pygame.mouse.set_visible(False)
else:
self.mouse = None
@@ -461,6 +498,7 @@ class Display(object):
self.suppress_mouse = suppress_blit
renpy.display.focus.take_focuses(surftree.focuses)
def save_screenshot(self, filename):
"""
@@ -611,12 +649,12 @@ class Interface(object):
return rv
# We load at most one image per wait.
if renpy.display.image.cache.needs_preload():
ev = pygame.event.poll()
if ev.type != NOEVENT:
return ev
if renpy.display.im.cache.needs_preload():
ev = pygame.event.poll()
if ev.type != NOEVENT:
return ev
renpy.display.image.cache.preload()
renpy.display.im.cache.preload()
return pygame.event.wait()
@@ -633,7 +671,7 @@ class Interface(object):
root = renpy.display.layout.Fixed()
for layer in renpy.config.layers:
f = renpy.display.layout.Fixed()
f = renpy.display.layout.Fixed(focus=layer)
f.append_scene_list(scene_lists.layers[layer])
rv[layer] = f
@@ -652,16 +690,21 @@ class Interface(object):
# These things can be done once per interaction.
repeat = True
try:
while repeat:
repeat, rv = self.interact_core(**kwargs)
repeat = True
while repeat:
repeat, rv = self.interact_core(**kwargs)
# Clean out transient stuff at the end of an interaction.
scene_lists = renpy.game.context().scene_lists
scene_lists.replace_transient()
return rv
finally:
# Clean out transient stuff at the end of an interaction.
scene_lists = renpy.game.context().scene_lists
scene_lists.replace_transient()
return rv
def interact_core(self,
@@ -699,8 +742,14 @@ class Interface(object):
# frames = 0
# Update the music, if necessary.
for i in pygame.event.get([ MUSICEND ]):
renpy.display.audio.music_end_event()
renpy.display.audio.music_interact()
# Tick time forward.
renpy.display.image.cache.tick()
renpy.display.im.cache.tick()
# Set up key repeats.
# pygame.time.set_timer(KEYREPEATEVENT, renpy.config.skip_delay)
@@ -711,12 +760,9 @@ class Interface(object):
# Clear some events.
pygame.event.clear((MOUSEMOTION, DISPLAYTIME,
MOUSEBUTTONUP, MOUSEBUTTONDOWN))
# Figure out the scene list we want to show.
start_time = time.time()
# Figure out the scene list we want to show.
scene_lists = renpy.game.context().scene_lists
# Figure out what the overlay layer should look like.
for i in renpy.config.overlay_layers:
@@ -742,7 +788,15 @@ class Interface(object):
# Figure out the scene. (All of the layers, and the root.)
scene = self.compute_scene(scene_lists)
# If necessary, load all images here.
if renpy.config.load_before_transition:
for w in scene.itervalues():
w.predict(renpy.display.im.cache.get)
# The start time of transitions.
start_time = time.time()
# The root widget of all of the layers.
layers_root = renpy.display.layout.Fixed()
@@ -784,13 +838,16 @@ class Interface(object):
# 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(root_widget)
# Redraw the screen.
renpy.display.render.process_redraws()
needs_redraw = True
# Post an event that moves us to the current mouse position.
pygame.event.post(pygame.event.Event(MOUSEMOTION,
pos=pygame.mouse.get_pos()))
# pygame.event.post(pygame.event.Event(MOUSEMOTION,
# pos=pygame.mouse.get_pos()))
# We only want to do prediction once, but we will defer it as
# long as possible.
@@ -848,14 +905,18 @@ class Interface(object):
# Predict images, if we haven't done so already.
if not did_prediction and not self.event_peek():
renpy.game.context().predict(renpy.display.image.cache.preload_image)
root_widget.predict(renpy.display.image.cache.preload_image)
root_widget.predict(renpy.display.im.cache.preload_image)
renpy.game.context().predict(renpy.display.im.cache.preload_image)
did_prediction = True
try:
ev = self.event_wait()
self.profile_time = time.time()
# A song just ended.
if ev.type == MUSICEND:
renpy.display.audio.music_end_event()
if ev.type == DISPLAYTIME:
events = 1 + len(pygame.event.get([DISPLAYTIME]))
@@ -865,12 +926,6 @@ class Interface(object):
ev = pygame.event.Event(DISPLAYTIME, {},
duration=(time.time() - start_time))
# Update the playing music, if necessary.
# This needs to be here so that we eventually start a
# new song at the end of a fadeout.
renpy.display.audio.restore_music()
# Handle skipping.
renpy.display.behavior.skipping(ev)
@@ -890,8 +945,9 @@ class Interface(object):
if len(evs):
ev = evs[-1]
# x, y = getattr(ev, 'pos', (0, 0))
renpy.display.focus.event_handler(ev)
# x, y = getattr(ev, 'pos', (0, 0))
x, y = pygame.mouse.get_pos()
rv = root_widget.event(ev, x, y)
+355
View File
@@ -0,0 +1,355 @@
# This file contains code to manage focus on the display.
import renpy
import pygame
from pygame.constants import *
class Focus(object):
def __init__(self, widget, arg, x, y, w, h):
self.widget = widget
self.arg = arg
self.x = x
self.y = y
self.w = w
self.h = h
def __iter__(self):
return iter((self.widget, self.arg, self.x, self.y, self.w, self.h))
# Sets the currently focused widget.
def set_focused(widget):
renpy.game.context().scene_lists.focused = widget
# Gets the currently focused widget.
def get_focused():
return renpy.game.context().scene_lists.focused
# The current list of focuses that we know about.
focus_list = [ ]
# This takes in a focus list from the rendering system.
def take_focuses(fl):
global focus_list
focus_list = fl
# This is called before each interaction. It's purpose is to choose
# the widget that is focused, and to mark it as focused and all of
# the other widgets as unfocused.
def before_interact(root):
# a list of focusable, name tuples.
fwn = [ ]
def callback(f, n):
fwn.append((f, n))
root.find_focusable(callback, None)
# Assign a full name to each focusable.
namecount = { }
for f, n in fwn:
serial = namecount.get(n, 0)
namecount[n] = serial + 1
f.full_focus_name = n, serial
# If there's something with the same full name as the current widget,
# it becomes the new current widget.
current = get_focused()
if current is not None:
current_name = current.full_focus_name
for f, n in fwn:
if f.full_focus_name == current.full_focus_name:
current = f
set_focused(f)
break
else:
current = None
# Otherwise, focus the default widget, or nothing.
if current is None:
for f, n in fwn:
if f.default:
current = f
set_focused(f)
break
else:
set_focused(None)
# Finally, mark the current widget as the focused widget, and
# all other widgets as unfocused.
for f, n in fwn:
if f is current:
f.focus(default=True)
else:
f.unfocus()
# This changes the focus to be the widget contained inside the new
# focus object.
def change_focus(newfocus):
if newfocus is None:
widget = None
else:
widget = newfocus.widget
current = get_focused()
# Nothing to do.
if current is widget:
return
if current is not None:
current.unfocus()
current = widget
if widget is not None:
widget.focus()
set_focused(current)
# This handles mouse events, to see if they change the focus.
def mouse_handler(ev):
x, y = ev.pos
newfocus = None
default = None
for f in focus_list:
if f.x is None:
default = f
continue
if f.x <= x <= f.x + f.w and f.y <= y <= f.y + f.h:
newfocus = f
break
else:
newfocus = default
change_focus(newfocus)
# This focuses an extreme widget, which is one of the widgets that's
# at an edge. To do this, we multiply the x, y, width, and height by
# the supplied multiplers, add them all up, and take the focus with
# the largest value.
def focus_extreme(xmul, ymul, wmul, hmul):
max_focus = None
max_score = -(65536**2)
for f in focus_list:
if not f.x:
continue
score = (f.x * xmul +
f.y * ymul +
f.w * wmul +
f.h * hmul)
if score > max_score:
max_score = score
max_focus = f
if max_focus:
change_focus(max_focus)
# This calculates the distance between two points, applying
# the given fudge factors. The distance is left squared.
def points_dist(x0, y0, x1, y1, xfudge, yfudge):
return (( x0 - x1 ) * xfudge ) ** 2 + \
(( y0 - y1 ) * yfudge ) ** 2
# This computes the distance between two horizontal lines. (So the
# distance is either vertical, or has a vertical component to it.)
#
# The distance is left squared.
def horiz_line_dist(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1):
# The lines overlap in x.
if bx0 <= ax0 <= ax1 <= bx1 or \
ax0 <= bx0 <= bx1 <= ax1 or \
ax0 <= bx0 <= ax1 <= bx1 or \
bx0 <= ax0 <= bx1 <= ax1:
return (ay0 - by0) ** 2
# The right end of a is to the left of the left end of b.
if ax0 <= ax1 <= bx0 <= bx1:
return points_dist(ax1, ay1, bx0, by0, renpy.config.focus_crossrange_penalty, 1.0)
if bx0 <= bx1 <= ax0 <= ax1:
return points_dist(ax0, ay0, bx1, by1, renpy.config.focus_crossrange_penalty, 1.0)
assert False
# This computes the distance between two vertical lines. (So the
# distance is either hortizontal, or has a horizontal component to it.)
#
# The distance is left squared.
def verti_line_dist(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1):
# The lines overlap in x.
if by0 <= ay0 <= ay1 <= by1 or \
ay0 <= by0 <= by1 <= ay1 or \
ay0 <= by0 <= ay1 <= by1 or \
by0 <= ay0 <= by1 <= ay1:
return (ax0 - bx0) ** 2
# The right end of a is to the left of the left end of b.
if ay0 <= ay1 <= by0 <= by1:
return points_dist(ax1, ay1, bx0, by0, 1.0, renpy.config.focus_crossrange_penalty)
if by0 <= by1 <= ay0 <= ay1:
return points_dist(ax0, ay0, bx1, by1, 1.0, renpy.config.focus_crossrange_penalty)
assert False
# This focuses the widget that is nearest to the current widget. To
# determine nearest, we compute points on the widgets using the
# {from,to}_{x,y}off values. We pick the nearest, applying a fudge
# multiplier to the distances in each direction, that satisfies
# the condition (which is given a Focus object to evaluate).
#
# If no focus can be found matching the above, we look for one
# with an x of None, and make that the focus. Otherwise, we do
# nothing.
#
# If no widget is focused, we pick one and focus it.
#
# If the current widget has an x of None, we pass things off to
# focus_extreme to deal with.
def focus_nearest(from_x0, from_y0, from_x1, from_y1,
to_x0, to_y0, to_x1, to_y1,
line_dist,
condition,
xmul, ymul, wmul, hmul):
if not focus_list:
return
# No widget focused.
current = get_focused()
if not current:
change_focus(focus_list[0])
return
# Find the current focus.
for f in focus_list:
if f.widget == current:
from_focus = f
break
else:
# If we can't pick something.
change_focus(focus_list[0])
return
# If placeless, focus_extreme.
if from_focus.x is None:
focus_extreme(xmul, ymul, wmul, hmul)
return
fx0 = from_focus.x + from_focus.w * from_x0
fy0 = from_focus.y + from_focus.h * from_y0
fx1 = from_focus.x + from_focus.w * from_x1
fy1 = from_focus.y + from_focus.h * from_y1
placeless = None
new_focus = None
# a really big number.
new_focus_dist = (65536.0 * renpy.config.focus_crossrange_penalty) ** 2
for f in focus_list:
if f is from_focus:
continue
if f.x is None:
placeless = f
continue
if not condition(from_focus, f):
continue
tx0 = f.x + f.w * to_x0
ty0 = f.y + f.h * to_y0
tx1 = f.x + f.w * to_x1
ty1 = f.y + f.h * to_y1
dist = line_dist(fx0, fy0, fx1, fy1,
tx0, ty0, tx1, ty1)
if dist < new_focus_dist:
new_focus = f
new_focus_dist = dist
# If we couldn't find anything, try the placeless focus.
new_focus = new_focus or placeless
# If we have something, switch to it.
if new_focus:
change_focus(new_focus)
# And, we're done.
def key_handler(ev):
if renpy.display.behavior.map_event(ev, 'focus_right'):
focus_nearest(0.9, 0.1, 0.9, 0.9,
0.1, 0.1, 0.1, 0.9,
verti_line_dist,
lambda old, new : old.x + old.w <= new.x,
-1, 0, 0, 0)
if renpy.display.behavior.map_event(ev, 'focus_left'):
focus_nearest(0.1, 0.1, 0.1, 0.9,
0.9, 0.1, 0.9, 0.9,
verti_line_dist,
lambda old, new : new.x + new.w <= old.x,
1, 0, 1, 0)
if renpy.display.behavior.map_event(ev, 'focus_up'):
focus_nearest(0.1, 0.1, 0.9, 0.1,
0.1, 0.9, 0.9, 0.9,
horiz_line_dist,
lambda old, new : new.y + new.h <= old.y,
0, 1, 0, 1)
if renpy.display.behavior.map_event(ev, 'focus_down'):
focus_nearest(0.1, 0.9, 0.9, 0.9,
0.1, 0.1, 0.9, 0.1,
horiz_line_dist,
lambda old, new : old.y + old.h <= new.y,
0, -1, 0, 0)
# This handles pygame events that may change focus.
def event_handler(ev):
if ev.type in (MOUSEMOTION, MOUSEBUTTONUP, MOUSEBUTTONDOWN):
mouse_handler(ev)
key_handler(ev)
+576
View File
@@ -0,0 +1,576 @@
# This file contains the new image code, which includes provisions for
# size-based caching and constructing images from operations (like
# cropping and scaling).
import renpy
import pygame
from pygame.constants import *
# This is an entry in the image cache.
class CacheEntry(object):
def __init__(self, what, surf):
# The object that is being cached (which needs to be
# hashable and comparable).
self.what = what
# The pygame surface corresponding to the cached object.
self.surf = surf
# The size of this image.
w, h = surf.get_size()
self.size = w * h
# The time when this cache entry was last used.
self.time = 0
# This is the singleton image cache.
class Cache(object):
def __init__(self):
# The current arbitrary time. (Increments by one for each
# interaction.)
self.time = 0
# A map from Image object to CacheEntry.
self.cache = { }
# A list of Image objects that we want to preload.
self.preloads = [ ]
# False if this is not the first preload in this tick.
self.first_preload_in_tick = True
# The total size of the current generation of images.
self.size_of_current_generation = 0
# The total size of everything in the cache.
self.total_cache_size = 0
# Returns the maximum size of the cache, after which we start
# tossing things out.
def cache_limit(self):
return renpy.config.image_cache_size * renpy.config.screen_width * renpy.config.screen_height
# Increments time, and clears the list of images to be
# preloaded.
def tick(self):
self.time += 1
self.preloads = [ ]
self.first_preload_in_tick = True
self.size_of_current_generation = 0
# Called to report that a given image would like to be preloaded.
def preload_image(self, image):
self.preloads.append(image)
# Do we need to preload an image?
def needs_preload(self):
return (self.preloads and True) or self.first_preload_in_tick
# This returns the pygame surface corresponding to the provided
# image. It also takes care of updating the age of images in the
# cache to be current, and maintaining the size of the current
# generation of images.
def get(self, image):
if not isinstance(image, ImageBase):
raise Exception("Expected an image of some sort, but got something else.")
if image in self.cache:
ce = self.cache[image]
if ce.time == self.time:
return ce.surf
else:
ce = CacheEntry(image, image.load())
self.total_cache_size += ce.size
self.cache[image] = ce
# Indicate that this surface had changed.
renpy.display.render.mutated_surface(ce.surf)
if renpy.config.debug_image_cache:
print "IC Added", ce.what
# Move it into the current generation.
ce.time = self.time
self.size_of_current_generation += ce.size
return ce.surf
# This kills off a given cache entry.
def kill(self, ce):
# Should never happen... but...
if ce.time == self.time:
self.size_of_current_generation -= ce.size
self.total_cache_size -= ce.size
del self.cache[ce.what]
if renpy.config.debug_image_cache:
print "IC Removed", ce.what
# Calling this cleans out the image cache if it has gotten too large.
def cleanout(self):
cache_limit = self.cache_limit()
# If we're within the limit, return.
if self.total_cache_size <= cache_limit:
return
# If we're outside the cache limit, we need to go and start
# killing off some of the entries until we're back inside it.
# A list of time, cache_entry pairs.
ace = [ (ce.time, ce) for ce in self.cache.itervalues() ]
ace.sort()
while ace and self.total_cache_size > cache_limit:
time, ce = ace.pop(0)
if time == self.time:
# If we're bigger than the limit, and there's nothing
# to remove, we should stop the preloading right away.
self.preloads = [ ]
break
# Otherwise, kill off the given cache entry.
self.kill(ce)
if renpy.config.debug_image_cache:
print "IC is:", self.cache.keys()
print "IC size:", self.total_cache_size, "/", cache_limit
# This actually performs preloading.
def preload(self):
if self.first_preload_in_tick:
self.first_preload_in_tick = False
# Triage into stuff that's already in the cache and should
# be kept there, and stuff that isn't there already.
new_preloads = [ ]
for i in self.preloads:
if i in self.cache:
self.get(i)
else:
new_preloads.append(i)
self.preloads = new_preloads
# Clean out the cache.
self.cleanout()
# Return after doing said triage.
return
# Otherwise, new_preloads contains things that aren't in the
# cache already. So load one of them into the cache, maybe.
cache_limit = self.cache_limit()
# If the size of the current generation is bigger than the
# total cache size, stop preloading.
if self.size_of_current_generation > cache_limit:
self.preloads = [ ]
return
# Otherwise, preload the next image.
image = self.preloads.pop(0)
self.get(image)
# And, we're done.
self.cleanout()
cache = Cache()
class ImageBase(renpy.display.core.Displayable):
"""
This is the base class for all of the various kinds of images that
we can possibly have.
"""
def __init__(self, *args, **properties):
if 'style' not in properties:
properties = properties.copy()
properties['style'] = 'image_placement'
super(ImageBase, self).__init__(**properties)
self.identity = (type(self).__name__, ) + args
def __hash__(self):
return hash(self.identity)
def __eq__(self, other):
if not isinstance(other, ImageBase):
return False
return self.identity == other.identity
def __repr__(self):
return "<" + " ".join([repr(i) for i in self.identity]) + ">"
def load(self):
"""
This function is called by the image cache code to cause this
image to be loaded. It's expected that children of this class
would override this.
"""
assert False
def render(self, w, h, st):
im = cache.get(self)
w, h = im.get_size()
rv = renpy.display.render.Render(w, h)
rv.blit(im, (0, 0))
return rv
def get_placement(self):
return self.style
def predict(self, callback):
callback(self)
class Image(ImageBase):
"""
This image manipulator loads an image from a file.
"""
def __init__(self, filename, **properties):
"""
@param filename: The filename that the image will be loaded from.
"""
super(Image, self).__init__(filename, **properties)
self.filename = filename
def load(self):
im = pygame.image.load(renpy.loader.load(self.filename), self.filename)
if im.get_flags() & SRCALPHA:
im = im.convert_alpha()
else:
im = im.convert()
return im
class Composite(ImageBase):
"""
This image manipulator composites one or more images together.
"""
def __init__(self, size, *args, **properties):
"""
The first argument that this takes is size, which is either the
desired size of the image (in pixels), or None to indicate that
the size should be
This takes an even number of position arguments. Odd numbered
(starting the count with 1) arguments are positions, which
give the position of the image, in pixels, with the origin in
the upper-left corner of the image. The even-numbered
arguments give the images (image manipulators) that will be
composited in those positions. The images are composited in
bottom-to-top order.
@param size: If given, this will be the size of the new
image. Otherwise, the size will be the same as that of the
first image.
"""
super(Composite, self).__init__(size, *args, **properties)
if len(args) % 2 != 0:
raise Exception("Composite requires an even number of arguments.")
self.size = size
self.positions = args[0::2]
self.images = [ image(i) for i in args[1::2] ]
def load(self):
if self.size:
size = self.size
else:
size = cache.get(self.images[0]).get_size()
rv = pygame.Surface(size, 0,
renpy.game.interface.display.sample_surface)
for pos, im in zip(self.positions, self.images):
rv.blit(cache.get(im), pos)
return rv
class FrameImage(ImageBase):
"""
This is an image that implements a frame with a given size. Instances
of this are used by the frame object to return a new frame when
such a new frame is needed.
"""
def __init__(self, im, xborder, yborder, width, height):
"""
@param image: The image that will be used as the base of this
frame.
@param xborder: The number of pixels in the x direction to use as
a border.
@param yborder: The number of pixels in the y direction to use as
a border.
@param width: The width we are being rendered at.
@param height: The height we are being rendered at.
"""
im = image(im)
super(FrameImage, self).__init__(im, xborder, yborder, width, height)
self.image = im
self.xborder = xborder
self.yborder = yborder
self.width = width
self.height = height
def load(self):
dw = self.width
dh = self.height
dest = pygame.Surface((dw, dh), 0,
renpy.game.interface.display.sample_surface)
source = cache.get(self.image)
sw, sh = source.get_size()
def draw(x0, x1, y0, y1):
# Quick exit.
if x0 == x1 or y0 == y1:
return
# Compute the coordinates of the left, right, top, and
# bottom sides of the region, for both the source and
# destination surfaces.
# left side.
if x0 >= 0:
dx0 = x0
sx0 = x0
else:
dx0 = dw + x0
sx0 = sw + x0
# right side.
if x1 > 0:
dx1 = x1
sx1 = x1
else:
dx1 = dw + x1
sx1 = sw + x1
# top side.
if y0 >= 0:
dy0 = y0
sy0 = y0
else:
dy0 = dh + y0
sy0 = sh + y0
# bottom side
if y1 > 0:
dy1 = y1
sy1 = y1
else:
dy1 = dh + y1
sy1 = sh + y1
# Compute sizes.
srcsize = (sx1 - sx0, sy1 - sy0)
dstsize = (dx1 - dx0, dy1 - dy0)
# Get a subsurface.
surf = source.subsurface((sx0, sy0, srcsize[0], srcsize[1]))
# Scale if we have to.
if dstsize != srcsize:
surf = pygame.transform.scale(surf, dstsize)
# Blit.
dest.blit(surf, (dx0, dy0))
xb = self.xborder
yb = self.yborder
# Top row.
draw(0, xb, 0, yb)
draw(xb, -xb, 0, yb)
draw(-xb, 0, 0, yb)
# Middle row.
draw(0, xb, yb, -yb)
draw(xb, -xb, yb, -yb)
draw(-xb, 0, yb, -yb)
# Bottom row.
draw(0, xb, -yb, 0)
draw(xb, -xb, -yb, 0)
draw(-xb, 0, -yb, 0)
# And, finish up.
return dest
class SolidImage(ImageBase):
"""
This is an image that is a solid rectangle with a given size. It's
used to implement Solid.
"""
def __init__(self, color, width, height):
super(SolidImage, self).__init__(color, width, height)
self.color = color
self.width = width
self.height = height
def load(self):
rv = pygame.Surface((self.width, self.height), 0,
renpy.game.interface.display.sample_surface)
rv.fill(self.color)
return rv
class Scale(ImageBase):
"""
This is an image manipulator that scales another image manipulator
to the specified width and height. This scalling is unfiltered, so
you can expect your image to look a bit jagged.
"""
def __init__(self, im, width, height):
im = image(im)
super(Scale, self).__init__(im, width, height)
self.image = im
self.width = width
self.height = height
def load(self):
return pygame.transform.scale(cache.get(self.image),
(self.width, self.height))
class Rotozoom(ImageBase):
"""
This is an image manipulator that is a smooth rotation and zoom of another image manipulator.
"""
def __init__(self, im, angle, zoom):
"""
@param im: The image to be rotozoomed.
@param angle: The number of degrees counterclockwise the image is
to be rotated.
@param zoom: The zoom factor. Numbers that are greater than 1.0
lead to the image becoming larger.
"""
im = image(im)
super(Rotozoom, self).__init__(im, angle, zoom)
self.image = im
self.angle = angle
self.zoom = zoom
def load(self):
return pygame.transform.rotozoom(cache.get(self.image),
self.angle, self.zoom)
class Crop(ImageBase):
"""
This crops the image that is its child.
"""
def __init__(self, im, x, y, w, h):
im = image(im)
super(Crop, self).__init__(im, x, y, w, h)
self.image = im
self.x = x
self.y = y
self.w = w
self.h = h
def load(self):
return cache.get(self.image).subsurface((self.x, self.y,
self.w, self.h))
def image(arg, **properties):
"""
This takes as input one of a number of ways of specifying an
image, and returns the Displayable image object that has been so
specified. Specifically, this can take as input:
<ul>
<li> An image object. In that case, it's returned unchanged.</li>
<li> A string. If a string is given, then the string is interpreted
as a filename, and what is returned is an im.Image object, which
loads the image from disk.</li>
<li> A tuple. If this is the case, then what is returned is an
im.Composite object, which aligns the upper-left corner of all
of the images supplied as arguments. </li>
</ul>
"""
if isinstance(arg, ImageBase):
return arg
elif isinstance(arg, basestring):
return Image(arg, **properties)
elif isinstance(arg, tuple):
params = [ ]
for i in arg:
params.append((0, 0))
params.append(i)
return Composite(None, *params)
elif isinstance(arg, renpy.display.core.Displayable):
raise Exception("Expected an image, but got a general displayable.")
else:
raise Exception("Could not construct image from argument.")
def load_image(fn):
"""
This loads an image from the given filename, using the cache.
"""
return cache.get(image(fn))
+43 -398
View File
@@ -1,209 +1,14 @@
# This file contains some miscellanious displayables that involve images.
# Most of the guts of this file have been moved into im.py, with only some
# of the stuff thar uses images remaining.
import renpy
from renpy.display.render import render
import pygame
from pygame.constants import *
class ImageCache(object):
def __init__(self):
# A monotonically increasing time.
self.time = 0
# A map from image filename to surface.
self.surface_map = { }
# A map from image filename to last access time.
self.time_map = { }
# The list of things we want to preload.
self.preloads = [ ]
def tick(self):
self.time += 1
self.preloads = [ ]
def really_load_image(self, fn):
"""
This is called by load_image, and does the actual loading of
images. This may be a load of an image, or perhaps the
compositing of images if fn is a tuple.
"""
# If fn is not a single filename but a tuple, we composite the
# elements of the tuple.
if isinstance(fn, tuple):
if not tuple:
raise Exception("Trying to create a composite image from an empty tuple.")
base = self.load_image(fn[0])
rv = pygame.Surface(base.get_size(), 0,
renpy.game.interface.display.sample_surface)
rv.blit(base, (0, 0))
for i in fn[1:]:
layer = self.load_image(i)
rv.blit(layer, (0, 0))
renpy.display.render.mutated_surface(rv)
return rv
im = pygame.image.load(renpy.loader.load(fn), fn)
if im.get_flags() & SRCALPHA:
im = im.convert_alpha()
else:
im = im.convert()
renpy.display.render.mutated_surface(im)
return im
# Forces an image load, regardless of if the cache is full or not.
def load_image(self, fn):
self.time_map[fn] = self.time
if fn in self.surface_map:
return self.surface_map[fn]
if fn in self.preloads:
self.preloads.remove(fn)
# iw, ih = im.get_size()
# surf = renpy.display.render.Render(iw, ih)
# surf.blit(im, (0, 0))
im = self.really_load_image(fn)
self.surface_map[fn] = im
if renpy.config.debug_image_cache:
print "Image cache:", self.surface_map.keys()
return im
# Queues an image to be preloaded if not already loaded and there's
# room in the cache for it.
def preload_image(self, fn):
self.time_map[fn] = self.time
if fn in self.surface_map:
return
if fn not in self.preloads:
self.preloads.append(fn)
# This tries to ensure that there are n empty spaces in the image
# cache. Returns the number of empty spaces that are actually in
# the image cache. (A number that may be negative.)
def clear_image_cache(self, n):
rv = renpy.config.image_cache_size - len(self.surface_map)
if rv >= n:
return rv
# The number of images to remove. (This is the amount we are over
# the cache limit + the number of images we have been requested to
# pull.)
num_to_remove = len(self.surface_map) - renpy.config.image_cache_size + n
time_files = [ (self.time_map[fn], fn) for fn in self.surface_map ]
time_files = [ (time, fn) for time, fn in time_files if time != self.time ]
time_files.sort()
time_files = time_files[:num_to_remove]
for time, fn in time_files:
del self.surface_map[fn]
del self.time_map[fn]
if renpy.config.debug_image_cache:
print "Image cache:", self.surface_map.keys()
rv = renpy.config.image_cache_size - len(self.surface_map)
return rv
def needs_preload(self):
"""
Returns True if calling preload would do anything.
"""
return self.preloads and True
def preload(self):
# If we have nothing to preload, bail early.
if not self.preloads:
return
# Try to clear up enough space for the preloads.
avail = self.clear_image_cache(len(self.preloads))
if avail < 0:
avail = 0
self.preloads = self.preloads[:avail]
# If no space is available, bail here.
if not self.preloads:
return
# Get the first thing to preload.
fn = self.preloads[0]
# Actually load the image.
try:
self.load_image(fn)
except:
if renpy.config.debug:
raise
cache = ImageCache()
class Image(renpy.display.core.Displayable):
"""
Returns a Displayable that is an image that is loaded from a file
on disk.
"""
def __init__(self, filename, style='image_placement', **properties):
"""
@param filename: The filename that the image is loaded
from. Many common file formats are supported.
If the filename is not a single string but instead a tuple of
strings, the image is considered to be"layered". In this case,
the image will be the size of the first image in the tuple, and
other images will be aligned with the upper-left corner of the
image.
"""
super(Image, self).__init__()
self.filename = filename
self.style = renpy.style.Style(style, properties)
def render(self, w, h, st):
im = cache.load_image(self.filename)
w, h = im.get_size()
rv = renpy.display.render.Render(w, h)
rv.blit(im, (0, 0))
return rv
def get_placement(self):
return self.style
def predict(self, callback):
callback(self.filename)
Image = renpy.display.im.image
class UncachedImage(renpy.display.core.Displayable):
"""
@@ -265,8 +70,7 @@ class ImageReference(renpy.display.core.Displayable):
parameters = [ ]
def error(msg):
self.target = renpy.display.text.Text(msg,
color=(255, 0, 0, 255))
self.target = renpy.display.text.Text(msg, color=(255, 0, 0, 255))
if renpy.config.debug:
raise Exception(msg)
@@ -323,7 +127,8 @@ class Solid(renpy.display.core.Displayable):
def __init__(self, color):
"""
@param color: An RGBA tuple, giving the color that the display will be filled with.
@param color: An RGBA tuple, giving the color that the display
will be filled with.
"""
super(Solid, self).__init__()
@@ -331,10 +136,11 @@ class Solid(renpy.display.core.Displayable):
def render(self, width, height, st):
rv = renpy.display.render.Render(width, height)
rv.fill(self.color)
si = renpy.display.im.SolidImage(self.color,
width,
height)
return rv
return render(si, width, height, st)
class Frame(renpy.display.core.Displayable):
"""
@@ -348,11 +154,10 @@ class Frame(renpy.display.core.Displayable):
the center of the image is scaled in both x and y directions.
"""
nosave = [ 'cache' ]
def __init__(self, filename, xborder, yborder):
def __init__(self, image, xborder, yborder):
"""
@param filename: The file that the original image will be read from.
@param image: The image (which may be a filename or image
object) that will be scaled.
@param xborder: The number of pixels in the x direction to use as
a border.
@@ -360,105 +165,29 @@ class Frame(renpy.display.core.Displayable):
@param yborder: The number of pixels in the y direction to use as
a border.
For better performance, have the image file share a dimension
For better performance, have the image share a dimension
length in common with the size the frame will be rendered
at. We detect this and avoid scaling if possible.
"""
super(Frame, self).__init__()
self.filename = filename
self.image = Image(image)
self.xborder = xborder
self.yborder = yborder
def render(self, width, height, st):
dest = renpy.display.render.Render(width, height)
dw, dh = width, height
source = cache.load_image(self.filename)
sw, sh = source.get_size()
fi = renpy.display.im.FrameImage(self.image,
self.xborder,
self.yborder,
width,
height)
def draw(x0, x1, y0, y1):
# Quick exit.
if x0 == x1 or y0 == y1:
return
# Compute the coordinates of the left, right, top, and
# bottom sides of the region, for both the source and
# destination surfaces.
# left side.
if x0 >= 0:
dx0 = x0
sx0 = x0
else:
dx0 = dw + x0
sx0 = sw + x0
# right side.
if x1 > 0:
dx1 = x1
sx1 = x1
else:
dx1 = dw + x1
sx1 = sw + x1
# top side.
if y0 >= 0:
dy0 = y0
sy0 = y0
else:
dy0 = dh + y0
sy0 = sh + y0
# bottom side
if y1 > 0:
dy1 = y1
sy1 = y1
else:
dy1 = dh + y1
sy1 = sh + y1
# Compute sizes.
srcsize = (sx1 - sx0, sy1 - sy0)
dstsize = (dx1 - dx0, dy1 - dy0)
# Get a subsurface.
surf = source.subsurface((sx0, sy0, srcsize[0], srcsize[1]))
# Scale if we have to.
if dstsize != srcsize:
surf = pygame.transform.scale(surf, dstsize)
# Blit.
dest.blit(surf, (dx0, dy0))
xb = self.xborder
yb = self.yborder
# Top row.
draw(0, xb, 0, yb)
draw(xb, -xb, 0, yb)
draw(-xb, 0, 0, yb)
# Middle row.
draw(0, xb, yb, -yb)
draw(xb, -xb, yb, -yb)
draw(-xb, 0, yb, -yb)
# Bottom row.
draw(0, xb, -yb, 0)
draw(xb, -xb, -yb, 0)
draw(-xb, 0, -yb, 0)
# And, finish up.
return dest
return render(fi, width, height, st)
def predict(self, callback):
callback(self.filename)
self.image.predict(callback)
class Animation(renpy.display.core.Displayable):
"""
@@ -476,7 +205,7 @@ class Animation(renpy.display.core.Displayable):
animation will restart after the final delay time.
"""
super(Animation, self).__init__()
super(Animation, self).__init__(style='image_placement')
self.images = [ ]
self.delays = [ ]
@@ -484,7 +213,7 @@ class Animation(renpy.display.core.Displayable):
for i, arg in enumerate(args):
if i % 2 == 0:
self.images.append(arg)
self.images.append(Image(arg))
else:
self.delays.append(arg)
@@ -499,7 +228,7 @@ class Animation(renpy.display.core.Displayable):
if t < delay:
renpy.display.render.redraw(self, delay - t)
im = cache.load_image(image)
im = render(image, width, height, st)
width, height = im.get_size()
rv = renpy.display.render.Render(width, height)
rv.blit(im, (0, 0))
@@ -511,105 +240,21 @@ class Animation(renpy.display.core.Displayable):
def predict(self, callback):
for i in self.images:
callback(i)
i.predict(callback)
def get_placement(self):
return renpy.game.style.image_placement
class ImageMap(renpy.display.core.Displayable):
"""
The displayable that implements renpy.imagemap.
"""
def __init__(self, ground, selected, hotspots, unselected=None,
style='imagemap', **properties):
super(ImageMap, self).__init__()
self.ground = ground
self.selected = selected
self.hotspots = hotspots
if not unselected:
self.unselected = self.ground
else:
self.unselected = unselected
self.active = None
self.last_active = None
self.style = renpy.style.Style(style, properties)
def get_placement(self):
return self.style
# This doesn't do anything quite yet.
def predict(self, callback):
callback(self.ground)
callback(self.selected)
callback(self.unselected)
def render(self, width, height, st):
ground = cache.load_image(self.ground)
selected = cache.load_image(self.selected)
unselected = cache.load_image(self.unselected)
width, height = ground.get_size()
rv = renpy.display.render.Render(width, height)
rv.blit(ground, (0, 0))
for i, hotspot in enumerate(self.hotspots):
x0, y0, x1, y1, result = hotspot
if i == self.active:
source = selected
else:
source = unselected
subsurface = source.subsurface((x0, y0, x1-x0, y1-y0))
renpy.display.render.mutated_surface(subsurface)
rv.blit(subsurface, (x0, y0))
return rv
def event(self, ev, x, y):
old_active = self.active
active = None
for i, (x0, y0, x1, y1, result) in enumerate(self.hotspots):
if x >= x0 and x <= x1 and y >= y0 and y <= y1:
active = i
break
# result stays set.
if old_active != active:
self.active = active
renpy.display.render.redraw(self, 0)
if active is not None:
renpy.display.audio.play(self.style.hover_sound)
if active is None:
return None
if renpy.display.behavior.map_event(ev, "imagemap_select"):
renpy.display.audio.play(self.style.activate_sound)
return result
return None
class ImageButton(renpy.display.behavior.Button):
"""
Used to implement the guts of an image button.
"""
def __init__(self, idle_image, hover_image, style='image_button',
def __init__(self, idle_image, hover_image,
style='image_button',
image_style='image_button_image',
clicked=None, hovered=None):
clicked=None, hovered=None, **properties):
self.idle_image = Image(idle_image, style=image_style)
self.idle_image.style.set_prefix("idle_")
@@ -619,18 +264,18 @@ class ImageButton(renpy.display.behavior.Button):
super(ImageButton, self).__init__(self.idle_image,
style=style,
clicked=clicked,
hovered=hovered)
hovered=hovered,
**properties)
def predict(self, callback):
self.idle_image.predict(callback)
self.hover_image.predict(callback)
def set_hover(self, hover):
super(ImageButton, self).set_hover(hover)
def focus(self, default=False):
self.child = self.hover_image
super(ImageButton, self).focus(default=default)
if hover:
self.child = self.hover_image
else:
self.child = self.idle_image
def unfocus(self):
self.child = self.idle_image
super(ImageButton, self).unfocus()
+24 -16
View File
@@ -27,9 +27,7 @@ class Null(renpy.display.core.Displayable):
"""
def __init__(self, width=0, height=0, style='default', **properties):
super(Null, self).__init__()
self.style = renpy.style.Style(style, properties)
super(Null, self).__init__(style=style, **properties)
self.width = width
self.height = height
@@ -37,7 +35,12 @@ class Null(renpy.display.core.Displayable):
return self.style
def render(self, width, height, st):
return renpy.display.render.Render(self.width, self.height)
rv = renpy.display.render.Render(self.width, self.height)
if self.focusable:
rv.add_focus(self, None, None, None, None, None)
return rv
class Container(renpy.display.core.Displayable):
@@ -60,16 +63,24 @@ class Container(renpy.display.core.Displayable):
"""
def __init__(self, *args):
def __init__(self, *args, **properties):
super(Container, self).__init__()
self.children = []
self.child = None
for i in args:
self.add(i)
super(Container, self).__init__(**properties)
def find_focusable(self, callback, focus_name):
super(Container, self).find_focusable(callback, focus_name)
for i in self.children:
i.find_focusable(callback, self.focus_name or focus_name)
def set_style_prefix(self, prefix):
super(Container, self).set_style_prefix(prefix)
@@ -132,6 +143,8 @@ class Container(renpy.display.core.Displayable):
def predict(self, callback):
super(Container, self).predict(callback)
for i in self.children:
i.predict(callback)
@@ -150,8 +163,7 @@ class Fixed(Container):
"""
def __init__(self, style='default', **properties):
super(Fixed, self).__init__()
self.style = renpy.style.Style(style, properties)
super(Fixed, self).__init__(style=style, **properties)
self.times = [ ]
def add(self, widget, time=None):
@@ -214,9 +226,7 @@ class Position(Container):
child of this widget is placed.
"""
super(Position, self).__init__()
self.style = renpy.style.Style(style, properties)
super(Position, self).__init__(style=style, **properties)
self.add(child)
def render(self, width, height, st):
@@ -456,17 +466,15 @@ class Window(Container):
def __init__(self, child, style='window', **properties):
super(Window, self).__init__()
super(Window, self).__init__(style=style, **properties)
self.add(child)
self.style = renpy.style.Style(style, properties)
def get_placement(self):
return self.style
def render(self, width, height, st):
# save typing and screen space.
# save some typing.
style = self.style
xminimum = scale(style.xminimum, width)
+69 -6
View File
@@ -132,7 +132,6 @@ def render_screen(widget, width, height, st):
global new_renders
global mutated_surfaces
redraw_queue = [ ]
mutated_surfaces = { }
rv = render(widget, width, height, st)
@@ -149,6 +148,15 @@ def render_screen(widget, width, height, st):
old_renders.update(new_renders)
new_renders.clear()
# Figure out which widgets are still alive.
live_widgets = sets.Set()
for widget, height, width in old_renders:
live_widgets.add(widget)
# Filter dead widgets from the redraw queue.
redraw_queue = [ (when, widget) for when, widget in redraw_queue if
widget in live_widgets ]
return rv
old_blits = [ ]
@@ -295,9 +303,11 @@ class Render(object):
# entries in old_renders that this render is in.
self.render_of = [ ]
# The width and height of this render.
self.width = width
self.height = height
# The parents of this render.
self.parents = [ ]
self.blittables = [ ]
@@ -310,6 +320,10 @@ class Render(object):
self.subsurfaces = { }
# The list of focusable widgets collected from this render
# and the children of this render. A list of (widget, arg, x, y, w, h,)
self.focuses = [ ]
# def __del__(self):
# Render.renders -= 1
# print "Render del", Render.renders, Render.liverenders, self
@@ -371,8 +385,9 @@ class Render(object):
# Removes cycles.
self.render_of = [ ]
self.focuses = [ ]
def blit(self, source, (x, y)):
def blit(self, source, (xo, yo), focus=True):
"""
Adds the source to the list of things that need to be blitted
to the screen. The source should be either a pygame.Surface,
@@ -385,8 +400,18 @@ class Render(object):
source.parents.append(self)
self.children.append(source)
self.blittables.append((x, y, source))
if focus and xo == 0 and yo == 0:
self.focuses.extend(source.focuses)
elif focus:
for widget, arg, x, y, w, h in source.focuses:
if x is not None:
x += xo
y += yo
self.add_focus(widget, arg, x, y, w, h)
self.blittables.append((xo, yo, source))
def blit_to(self, dest, x, y):
@@ -459,7 +484,7 @@ class Render(object):
return rv
def subsurface(self, pos):
def subsurface(self, pos, focus=False):
"""
Returns a subsurface of this render.
"""
@@ -477,6 +502,24 @@ class Render(object):
rv = Render(width, height)
if focus:
for fwidget, farg, fx, fy, fw, fh in self.focuses:
if fx is not None:
fx -= x
fx = max(fx, 0)
fy -= y
fy = max(fy, 0)
fw -= x
fw = min(fw, width)
fh -= y
fh = min(fh, height)
if fw <= 0 or fh <= 0:
continue
rv.add_focus(fwidget, farg, fx, fy, fw, fh)
for xo, yo, source in self.blittables:
# ulx, uly -- the coordinates of the upper-left hand corner of
@@ -539,4 +582,24 @@ class Render(object):
self.depends.append(child)
child.parents.append(self)
def add_focus(self, widget, arg=None, x=0, y=0, w=None, h=None):
"""
This is called to indicate a region of the screen that can be
focused.
@param widget: The widget that will be focused.
@param arg: A focus argument, which can be checked by the widget.
The rest of the parameters are a rectangle giving the portion of
this region corresponding to the focus. If they are all None, than
this focus is assumed to be the singular full-screen focus.
"""
if x is not None:
if w is None:
w = self.width
if h is None:
h = self.height
self.focuses.append(renpy.display.focus.Focus(widget, arg, x, y, w, h))
+1 -1
View File
@@ -452,7 +452,7 @@ class Text(renpy.display.core.Displayable):
self.layout(width - absxo)
rv = renpy.display.render.Render(self.laidout_width + absxo, self.laidout_height + absxo)
rv = renpy.display.render.Render(self.laidout_width + absxo, self.laidout_height + absyo)
if self.style.drop_shadow:
self.render_pass(rv, dsxo, dsyo, self.style.drop_shadow_color, False, length)
+12 -6
View File
@@ -60,6 +60,7 @@ class Transition(renpy.display.core.Displayable):
"""
def __init__(self, delay):
super(Transition, self).__init__()
self.delay = delay
self.offsets = [ ]
self.events = True
@@ -70,6 +71,9 @@ class Transition(renpy.display.core.Displayable):
else:
return None
def find_focusable(self, callback, focus_name):
self.new_widget.find_focusable(callback, focus_name)
class Fade(Transition):
"""
This returns an object that can be used as an argument to a with
@@ -134,7 +138,7 @@ class Fade(Transition):
if widget:
surf = render(widget, width, height, st)
rv.blit(surf, (0, 0))
rv.blit(surf, (0, 0), focus=events)
self.events = events
@@ -253,6 +257,8 @@ class Dissolve(Transition):
surf = top.pygame_surface(False)
renpy.display.render.mutated_surface(surf)
rv.focuses.extend(top.focuses)
if id(top) == self.old_top and id(bottom) == self.old_bottom:
# Fast rendering path.
@@ -271,7 +277,7 @@ class Dissolve(Transition):
# Complete rendering path.
rv.blit(bottom, (0, 0))
rv.blit(bottom, (0, 0), focus=False)
surf.set_alpha(alpha, RLEACCEL)
rv.blit(surf, (0, 0))
@@ -352,7 +358,7 @@ class CropMove(Transition):
image. Otherwise, the top layer contains the old image.
"""
super(CropMove, self).__init__(time)
self.time = time
if mode == "wiperight":
@@ -503,11 +509,11 @@ class CropMove(Transition):
rv = renpy.display.render.Render(width, height)
rv.blit(render(self.bottom, width, height, st), (0, 0))
rv.blit(render(self.bottom, width, height, st), (0, 0), focus=not self.topnew)
top = render(self.top, width, height, st)
ss = top.subsurface(crop)
rv.blit(ss, pos)
ss = top.subsurface(crop, focus=self.topnew)
rv.blit(ss, pos, focus=self.topnew)
renpy.display.render.redraw(self, 0)
return rv
-1
View File
@@ -160,7 +160,6 @@ class Movie(renpy.display.layout.Null):
def render(self, width, height, st):
renpy.display.render.redraw(self, 0)
if surface:
renpy.display.render.mutated_surface(surface)
+7 -1
View File
@@ -22,6 +22,8 @@ class Context(object):
@ivar rollback: True if this context participates in rollbacks.
@ivar runtime: The time spent in this context, in milliseconds.
@ivar info: A RevertableObject, which is made available to user code.
"""
def __init__(self, rollback, context=None):
@@ -30,12 +32,15 @@ class Context(object):
self.return_stack = [ ]
self.rollback = rollback
self.runtime = 0
self.info = renpy.python.RevertableObject()
oldsl = None
if context:
oldsl = context.scene_lists
self.runtime = context.runtime
vars(self.info).update(vars(context.info))
self.scene_lists = renpy.display.core.SceneLists(oldsl)
def goto_label(self, node_name):
@@ -115,6 +120,7 @@ class Context(object):
rv.current = self.current
rv.scene_lists = self.scene_lists.rollback_copy()
rv.runtime = self.runtime
rv.info = self.info
return rv
+32 -10
View File
@@ -6,16 +6,16 @@
import renpy
# Many of these shouldn't be used directly.
from renpy.display.layout import *
from renpy.display.text import *
from renpy.display.behavior import *
from renpy.display.image import *
# from renpy.display.layout import *
from renpy.display.text import ParameterizedText
from renpy.display.behavior import Keymap
# from renpy.display.image import *
from renpy.curry import curry
from renpy.display.audio import music_start, music_stop
# from renpy.display.audio import music_start, music_stop
from renpy.display.audio import play
from renpy.display.video import movie_start_fullscreen, movie_start_displayable, movie_stop
from renpy.loadsave import *
from renpy.loadsave import load, save, saved_games
from renpy.python import py_eval as eval
from renpy.python import rng as random
@@ -202,8 +202,11 @@ def display_menu(items, window_style='menu_window'):
class TagQuotingDict(object):
def __getitem__(self, key):
if key in renpy.game.store:
rv = renpy.game.store[key]
store = vars(renpy.store)
if key in store:
rv = store[key]
if isinstance(rv, (str, unicode)):
rv = rv.replace("{", "{{")
@@ -310,8 +313,6 @@ def imagemap(ground, selected, hotspots, unselected=None, overlays=False,
renpy.ui.imagemap(ground, selected, hotspots, unselected=unselected,
style=style, **properties)
renpy.ui.keymousebehavior()
rv = renpy.ui.interact(suppress_overlay=(not overlays))
checkpoint()
return rv
@@ -524,6 +525,19 @@ def get_game_runtime():
return renpy.game.context().runtime / 1000.0
def loadable(filename):
"""
Returns True if the given filename is loadable, meaning that it
can be loaded from the disk or from inside an archive. Returns
False if this is not the case.
"""
try:
renpy.loader.load(filename)
return True
except:
return False
def exists(filename):
"""
Returns true if the given filename can be found in the
@@ -549,6 +563,14 @@ def restart_interaction():
renpy.game.interface.restart_interaction = True
def context():
"""
Returns an object that is unique to the current context, that
participates in rollback and the like.
"""
return renpy.game.context().info
call_in_new_context = renpy.game.call_in_new_context
curried_call_in_new_context = renpy.curry.curry(renpy.game.call_in_new_context)
-5
View File
@@ -21,11 +21,6 @@ searchpath = [ ]
# A Script object, giving the script of the currently executing game.
script = None
# The store is where Ren'Py python results are stored. We first need
# to import in the module, and then we use the module's dictionary
# directly.
store = None
# A shallow copy of the store made at the end of the init phase. If
# a key in here points to the same value here as it does in the store,
# it is not saved.
+8 -3
View File
@@ -54,6 +54,11 @@ def run(restart=False):
game.persistent._seen_ever = { }
game.seen_ever = game.persistent._seen_ever
# Initialize the set of images seen ever.
if not game.persistent._seen_images:
game.persistent._seen_images = { }
# Clear the list of seen statements in this game.
game.seen_session = { }
@@ -66,11 +71,11 @@ def run(restart=False):
# Initialize the store.
renpy.store.store = renpy.store
game.store = vars(renpy.store)
renpy.store.persistent = game.persistent
renpy.store._preferences = game.preferences
# Set up styles.
renpy.style.reset()
game.style = renpy.style.StyleManager()
renpy.store.style = game.style
@@ -88,14 +93,14 @@ def run(restart=False):
renpy.game.exception_info = 'After initialization, but before game start.'
# Rebuild the various style caches.
game.style._build_style_caches()
renpy.style.build_styles()
# Index the archive files. We should not have loaded an image
# before this point. (As pygame will not have been initialized.)
renpy.loader.index_archives()
# Make a clean copy of the store.
game.clean_store = game.store.copy()
game.clean_store = vars(renpy.store).copy()
# Re-Initialize the log.
game.log = renpy.python.RollbackLog()
+10 -7
View File
@@ -143,7 +143,7 @@ def list_logical_lines(filename):
if line != "":
raise ParseError(filename, number, "is not terminated with a newline.")
raise ParseError(filename, start_number, "is not terminated with a newline (check quotes and parenthesis).")
return rv
@@ -172,13 +172,11 @@ def group_logical_lines(lines):
if l[index] == '\t':
index += 1
depth = depth + 8 - (16 % 8)
depth = depth + 8 - (depth % 8)
continue
break
# TODO: Fix to handle tabs properly. Or else update the docs to
# forbid tabs entirely.
return depth, l[depth:]
# i, min_depth -> block, new_i
@@ -467,12 +465,17 @@ class Lexer(object):
if c == 'u':
self.pos += 1
if self.eol():
if self.pos == len(self.text):
self.pos -= 1
return False
c = self.text[self.pos]
if c not in ('"', "'"):
if c not in ('"', "'"):
self.pos -= 1
return False
elif c not in ('"', "'"):
return False
delim = c
@@ -514,7 +517,7 @@ class Lexer(object):
while self.match(r'\.'):
n = self.name()
if not n:
self.parse_error('expecting name.')
self.error('expecting name.')
rv += "." + n
+28 -19
View File
@@ -62,7 +62,7 @@ def reached(obj, path, reachable):
def reached_vars(store, reachable):
"""
Marks everything reachable from the variables in the store
as reachable.
or from the context info objects as reachable.
@param store: A map from variable name to variable value.
@param reachable: A dictionary mapping reached object ids to
@@ -72,7 +72,9 @@ def reached_vars(store, reachable):
for k, v in store.iteritems():
reached(v, k, reachable)
for c in renpy.game.contexts:
reached(c.info, "#context", reachable)
##### Code that replaces literals will calls to magic constructors.
@@ -356,10 +358,10 @@ class Rollback(renpy.object.Object):
for t in self.store:
if len(t) == 2:
k, v = t
renpy.game.store[k] = v
vars(renpy.store)[k] = v
else:
k, = t
del renpy.game.store[k]
del vars(renpy.store)[k]
renpy.game.contexts = [ self.context ]
rng.pushback(self.random)
@@ -428,7 +430,7 @@ class RollbackLog(renpy.object.Object):
self.log.append(self.current)
self.mutated = { }
self.old_store = renpy.game.store.copy()
self.old_store = vars(renpy.store).copy()
def complete(self):
"""
@@ -439,7 +441,7 @@ class RollbackLog(renpy.object.Object):
occurs.
"""
new_store = renpy.game.store
new_store = vars(renpy.store)
store = [ ]
@@ -484,9 +486,11 @@ class RollbackLog(renpy.object.Object):
rv = { }
store = vars(renpy.store)
for k in self.ever_been_changed.keys():
if k in renpy.game.store:
rv[k] = renpy.game.store[k]
if k in store:
rv[k] = store[k]
return rv
@@ -617,14 +621,15 @@ class RollbackLog(renpy.object.Object):
renpy.game.log = self
# Restore the store.
renpy.game.store.clear()
renpy.game.store.update(renpy.game.clean_store)
store = vars(renpy.store)
store.clear()
store.update(renpy.game.clean_store)
for k in self.ever_been_changed:
if k in renpy.game.store:
del renpy.game.store[k]
if k in store:
del store[k]
renpy.game.store.update(self.frozen_roots)
store.update(self.frozen_roots)
self.frozen_roots = None
# Now, rollback to an acceptable point.
@@ -634,31 +639,35 @@ class RollbackLog(renpy.object.Object):
def py_exec_bytecode(bytecode, hide=False):
store = vars(renpy.store)
if hide:
locals = { }
else:
locals = renpy.game.store
locals = store
exec marshal.loads(bytecode) in renpy.game.store, locals
exec marshal.loads(bytecode) in store, locals
def py_exec(source, hide=False):
store = vars(renpy.store)
if hide:
locals = { }
else:
locals = renpy.game.store
locals = store
exec py_compile(source, 'exec') in renpy.game.store, locals
exec py_compile(source, 'exec') in store, locals
def py_eval_bytecode(bytecode):
return eval(marshal.loads(bytecode), renpy.game.store)
return eval(marshal.loads(bytecode), vars(renpy.store))
def py_eval(source):
source = source.strip()
return eval(py_compile(source, 'eval'),
renpy.game.store)
vars(renpy.store))
+2
View File
@@ -6,6 +6,8 @@
import renpy
import renpy.ui as ui
import renpy.display.im as im
import renpy.display.audio as audio
from renpy.python import RevertableList as __renpy__list__
list = __renpy__list__
+155 -121
View File
@@ -1,158 +1,192 @@
import renpy
# A list of style prefixes we care about, including no prefix.
prefixes = [ 'hover_', 'idle_', 'activate_', '' ]
prefixes = [ 'hover_', 'idle_', 'activate_' , 'insensitive_' ]
def startswith_prefix(s):
for i in prefixes:
if s.startswith(i):
return True
else:
return False
substitutes = dict(
xmargin = [ 'left_margin', 'right_margin' ],
ymargin = [ 'top_margin', 'bottom_margin' ],
xpadding = [ 'left_padding', 'right_padding' ],
ypadding = [ 'top_padding', 'bottom_padding' ],
)
# Expand out substitutes:
for k in substitutes.keys():
for p in prefixes:
substitutes[p + k] = [ p + i for i in substitutes[k] ]
# A map from a style name to the style associated with that name.
style_map = { }
# True if we have expanded all of the style caches, False otherwise.
styles_built = False
# A list of styles that are pending expansion.
styles_pending = [ ]
# A list of created styles giving the style's name, parent, and description.
style_info = [ ]
def reset():
"""
This resets all of the data structures associated with style
management.
"""
global style_map
global styles_built
global styles_pending
global style_info
style_map = { }
styles_built = False
styles_pending = [ ]
style_info = [ ]
class StyleManager(object):
"""
This is the singleton object that is exported into the store as
'style', and to everyone as renpy.game.style. It's responsible for
mapping style names to styles.
This is the singleton object that is exported into the store
as style
"""
def __init__(self):
self._style_list = [ ]
def __getattr__(self, name):
try:
return style_map[name]
except:
raise Exception('The style %s does not exist.' % name)
def create(self, name, parent='default', description=''):
"""
Creates a new style with the given parent and description, and
adds it to the StyleManager.
"""
def create(self, name, parent, description=None):
style_map[name] = Style(parent, { })
if parent and not hasattr(self, parent):
raise Exception("Style '%s' has non-existent parent '%s'." % (name, parent))
if description:
style_info.append((name, parent, description))
# This expands out property names and adds them to style's property
# dictionary.
def compute_properties(style, properties):
props = { }
# Expand substitutions.
for k, v in properties.items():
if k in substitutes:
for j in substitutes[k]:
props[j] = v
else:
props[k] = v
# Expand prefixes, where necessary.
for k, v in props.items():
if startswith_prefix(k):
continue
del props[k]
for p in prefixes:
props[p + k] = v
style.properties.update(props)
style.cache.update(props)
# This builds the style. If recurse is True, this also builds the
# parent style.
def build_style(style, recurse=False):
style.cache.clear()
if style.parent:
try:
parent = style_map[style.parent]
except:
raise Exception('Style %s is not known.' % style.parent)
if recurse:
build_style(parent)
s = Style(parent, defer=True)
s.name = name
s.parent = parent
s.description = description
style.cache.update(parent.cache)
setattr(self, name, s)
style.cache.update(style.properties)
self._style_list.append(s)
# This builds all pending styles, recursing to ensure that they are built
# in the right order.
def build_styles():
def _build_style_caches(self):
global styles_pending
global styles_built
for i in self._style_list:
i.build_cache()
for s in styles_pending:
build_style(s, True)
def _write_docs(self, filename):
f = file(filename, "w")
import re
for s in self._style_list:
f.write(' <renpy_style name="%s">' % s.name)
if s.parent:
f.write('<renpy_style_inherits>%s</renpy_style_inherits>' % s.parent)
f.write(re.sub(r'\s+', ' ', s.description))
f.write("</renpy_style>\n\n")
f.close()
styles_pending = None
styles_built = True
class Style(object):
"""
This is an individual style object, which can have properties
looked up on it or its parent. Call the constructor of this
to create an anonymous style.
"""
def __getstate__(self):
return dict(properties=self.properties,
prefix=self.prefix,
parent=self.parent)
return dict(prefix = self.prefix,
parent = self.parent,
cache = { },
properties = self.properties)
def __setstate__(self, state):
self.__dict__.update(state)
vars(self).update(state)
build_style(self)
# This should always work, as only one layer of these styles will
# be serialized.
def __init__(self, parent, properties):
self.build_cache()
fields = dict(
prefix = 'insensitive_',
parent = parent,
cache = { },
properties = { },
)
def __setattr__(self, key, value):
if key in substitutes:
for i in substitutes[key]:
self.__setattr__(i, value)
return
for prefix in prefixes:
prefkey = prefix + key
self.properties[prefkey] = value
self.cache[prefkey] = value
def __getattr__(self, key):
cache = self.cache
try:
return cache[self.prefix + key]
except KeyError:
raise AttributeError("Style property '%s' not found." % key)
def __delattr__(self, key):
del self.properties[key]
del self.cache[key]
def lookup(self, key, prefix):
cache = self.cache
try:
return cache.get(prefix + key, cache[key])
except KeyError:
raise AttributeError("Style property '%s' not found." % key)
vars(self).update(fields)
if styles_built:
build_style(self)
else:
styles_pending.append(self)
compute_properties(self, properties)
def set_prefix(self, prefix):
vars(self)["prefix"] = prefix
self.prefix = prefix
def build_cache(self):
vars(self)["cache"] = { }
self.cache = { }
if self.parent:
self.cache.update(getattr(renpy.game.style, self.parent).cache)
self.cache.update(self.properties)
def __init__(self, parent, properties=None, defer=False):
if parent and not hasattr(renpy.game.style, parent):
raise Exception("Style '%s' is not known." % parent)
if not properties:
properties = { }
for k in properties.keys():
for p in prefixes:
if p + k not in properties:
properties[p + k] = properties[k]
vars(self)["parent"] = parent
vars(self)["prefix"] = ''
vars(self)["properties"] = properties
vars(self)["cache"] = { }
if not defer:
self.build_cache()
def __getattr__(self, name):
return self.cache[self.prefix + name]
def __setattr__(self, name, value):
compute_properties(self, { name : value } )
def write_docs(filename):
f = file(filename, "w")
import re
for name, parent, description in style_info:
f.write(' <renpy_style name="%s">' % name)
if parent:
f.write('<renpy_style_inherits>%s</renpy_style_inherits>' % parent)
f.write(re.sub(r'\s+', ' ', description))
f.write("</renpy_style>\n\n")
f.close()
+48 -10
View File
@@ -169,7 +169,7 @@ def vbox(padding=0, **properties):
def grid(cols, rows, padding=0, xfill=False, yfill=False, **properties):
"""
This creates a layout that places widgets in an evenly spaced
grid. New widges are added to this vbox unil ui.close() is called.
grid. New widgets are added to this grid unil ui.close() is called.
Widgets are added by going from left to right within a single row,
and down to the start of the next row when a row is full. All cells
must be filled (that is, exactly col * rows widgets must be added to
@@ -194,8 +194,6 @@ def grid(cols, rows, padding=0, xfill=False, yfill=False, **properties):
return add(renpy.display.layout.Grid(cols, rows, padding, xfill=xfill, yfill=yfill, **properties), True)
def fixed(**properties):
"""
This creates a layout that places widgets at fixed locations
@@ -244,9 +242,12 @@ def keymousebehavior():
screen. The keymouse behavior allows the mouse to be controlled
by the keyboard. This widget should not be added to any other
widget, but should instead be only added to the screen itself.
As of 4.8, this does nothing, but is retained for compatability.
"""
return add(renpy.display.behavior.KeymouseBehavior())
return
def saybehavior():
"""
@@ -280,7 +281,12 @@ def pausebehavior(delay, result=False):
return add(renpy.display.behavior.PauseBehavior(delay, result))
def menu(menuitems, **properties):
def menu(menuitems,
style = 'menu',
caption_style='menu_caption',
choice_style='menu_choice',
choice_button_style='menu_choice_button',
**properties):
"""
This creates a new menu widget. Unlike the menu statement or
renpy.menu function, this menu widget is not enclosed in any sort
@@ -293,7 +299,22 @@ def menu(menuitems, **properties):
if this item is a non-selectable caption.
"""
return add(renpy.display.behavior.Menu(menuitems, **properties))
# menu is now a conglomeration of other widgets. And bully for it.
renpy.ui.vbox(style=style, **properties)
for label, val in menuitems:
if val is None:
renpy.ui.text(label, style=caption_style)
else:
renpy.ui.textbutton(label,
style=choice_button_style,
text_style=choice_style,
clicked=renpy.ui.returns(val))
renpy.ui.close()
# return add(renpy.display.behavior.Menu(menuitems, **properties))
def input(default, length=None, allow=None, exclude='{}', **properties):
"""
@@ -323,16 +344,33 @@ def image(filename, **properties):
return add(renpy.display.image.Image(filename, **properties))
def imagemap(ground, selected, hotspots, unselected=None,
style='imagemap', button_style='imagemap_button',
**properties):
"""
This is the widget that implements imagemaps. Parameters are
This is called to create imagemaps. Parameters are
roughtly the same as renpy.imagemap. The value of the hotspot is
returned when ui.interact() returns.
"""
return add(renpy.display.image.ImageMap(ground, selected,
hotspots, unselected,
**properties))
rv = fixed(style=style, **properties)
if not unselected:
unselected = ground
image(ground)
for x0, y0, x1, y1, result in hotspots:
imagebutton(renpy.display.im.Crop(unselected, x0, y0, x1 - x0, y1 - y0),
renpy.display.im.Crop(selected, x0, y0, x1 - x0, y1 - y0),
clicked=returns(result),
style=button_style,
xpos=x0, xanchor='left',
ypos=y0, yanchor='top',
)
close()
return rv
def button(clicked=None, **properties):
+10 -6
View File
@@ -1,6 +1,6 @@
init:
$ config.window_title = "Test Game"
$ config.profile = True
$ config.profile = False
$ config.debug_sound = True
@@ -22,11 +22,11 @@ init:
# Styles.
$ style.window.background = Frame("frame.png", 120, 25)
$ style.menu.hover_sound = "chev1.wav"
$ style.menu.activate_sound = "chev2.wav"
$ style.menu_choice_button.hover_sound = "chev1.wav"
$ style.menu_choice_button.activate_sound = "chev2.wav"
$ style.imagemap.hover_sound = "chev3.wav"
$ style.imagemap.activate_sound = "chev4.wav"
$ style.imagemap_button.hover_sound = "chev3.wav"
$ style.imagemap_button.activate_sound = "chev4.wav"
$ style.button.hover_sound = "chev6.wav"
$ style.button.activate_sound = "chev7.wav"
@@ -45,7 +45,7 @@ init:
image eileen anim = Animation("9a_happy.png", 0.25,
"9a_vhappy.png", 0.25,
"9a_concerned.png")
"9a_concerned.png", 60.0)
image movie = Movie()
@@ -82,6 +82,10 @@ label main_menu:
# Stuff that happens before the real main menu.
scene black
show eileen anim
$ renpy.pause()
show text "American Bishoujo\nPresents" \