Compare commits

..

1 Commits

Author SHA1 Message Date
tom 655b2aeb58 Tagging Ren'Py 4.6.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/tags/renpy-4.6@30 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-02-08 19:10:18 +00:00
53 changed files with 2163 additions and 4431 deletions
-237
View File
@@ -1,240 +1,3 @@
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
------------
This release should be compatiable with the .rpyc and .save files
produced by 4.7.1.
The new readback extra caused save files to reach a size and/or
complexity that caused errors in cPickle on Windows, which in turn
lead to a repeatable crash of Ren'Py, and left a corrupt save file in
the saves directory that prevented further saving and loading of
games. This has been addressed in two ways. The first is the use of
pickle (as opposed to cPickle) throught Ren'Py, a change that may slow
down the engine a little but should ensure correctness. The second is
that we now catch the errors produced by corrupt files, and (if
config.debug is not set) proceed by ignoring that file.
There is now a new extra, readback.rpy. This implements readback, in
addition to, or instead of, rollback. Readback is limited in a number
of ways, insofar as it only shows text without changing pictures or
other things. As part of implementing readback, we added a new
function, say, that is called when the user gives dialogue consisting
of a pair of strings.
There are a number of changes that improve support of the Mac OS X
platform. The foremost among them is that we ship a new file,
run_game.pyw. This is identical to run_game.py, but is necessary for
the game to run in a graphical environment on the Mac. (It also will
run the game without a console window on Windows, if you have the
appropriate dependencies.) Ren'Py still requires that PyObjC and
pygame are installed on a OS X 10.3 box before it can run.
The way font line spacing is computed was changed in this release, in
the hope of making it consistent on all three platforms. We now ignore
font linesize hints, which seem to be computed incorrectly on some
platforms, and instead set the line spacing to be equal to the ascent
and descent of the font. The user can increase or decrease the line
spacing by setting the new line_spacing text property. The old
line_height_fudge property is now ignored. The style.rpy common file
was updated to take account of this change.
To aid the user in finding the source of error messages, we now report
the name of the file being parsed when an error occurs during the
parse phase.
The tutorial has now been renamed "The Ren'Py Reference Manual", as
it's really more of a poorly-organized comprehensive reference then a
tutorial. This is in the hope that the Ren'Py user community will one
day write a more reasonable tutorial. A number of errors in the
reference were corrected.
The functions used in keymaps can now return values, which are
returned to the ui.interact() that called them. This was needed
to support readback.
LICENSE.txt has been updated to include the names and URLs of software
that a windows build of Ren'Py depends on, the licenses of which you
will be required to comply with when releasing a Ren'Py game.
New in 4.7.1
------------
Added a new tool, dump_text.py/.exe, that dumps all of the text from a
Ren'Py script file. It uses a heuristic approach that is imperfect,
but is hopefully accurate enough to be useful for purposes like
spell-checking a script. The text is dumped into the file text.txt in
the current directory, and on windows that file is then displayed to
the user. By default all of the rpy files in the game directory are
displayed, but supplying a filename parameter can change that.
Fixed a bug that was preventing parser error messages from being
reported on Windows.
Fixed a bug in Render.subsurface that lead to weird effects when doing
an irisin or irisout on a sufficently complicated scene. See
http://lemmasoft.renai.us/forums/viewtopic.php?p=4612#4612 for the
discussion.
New in 4.7
----------
The demo was updated to show the new features in 4.7.
The big new feature in this release is text tags. Text tags is a
markup language that allows text properties to be changed inside a
single unit of text. It lets you emphasize individual words by making
them bold, bigger, or a different color. Rather than explaining it
fully here, let me point you to the new "Text Tags" section of the
documentation.
There were two changes that were made to Ren'Py to support text
tags. The first was that interpolation now quotes the interpolated
text, making it impossible to introduce a text tag via interpolation.
The second change is that renpy.input() and friends now take as
arguments strings giving allowed and excluded characters. The default
is to exclude the '{' and '}' characters, meaning that it's impossible
for a user to input a text tag.
Finally, this release features even more defensive programming
involving sound. Now, if the mixer does not initialize, no further
sound operations will be performed.
New in 4.6.2
------------
4.6.2 was never officially released, as it didn't cure all of the
sound woes, and didn't have much else in the way of new features.
Added a speed test, which tests the speed of the dissolve transition
on a user's system. This test can be accessed by typing 'S' during the
demo (that's shift+the 's' key).
Fixed a bug with sound on systems with no soundcard in them. (Also,
put in a note to remind myself not to re-introduce that bug.)
Fixed a bug in which imagemaps would not redraw properly.
New in 4.6.1
------------
Fixed a major memory leak in the rendering code. This leak was
rendering 4.6 and 4.5 unusable on smaller systems.
Executed 250,000 statements (well, 5 statements in a loop, while
skipping), and ensured that there was no memory leak during the
execution of these statements.
Added a memory profiler. You can see it by supplying the --leak option
to run_game.exe or .py, but don't expect to understand it.
Changed Dissolve so that it tries to use compositing when it can. This
means that it only draws to the screen once... which is a little bit
of a speedup on a really expensive operation.
Fixed a bug that was preventing predictive image loading from
working.
New in 4.6
----------
-25
View File
@@ -20,28 +20,3 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Please note that the above license only applies to Ren'Py
proper. A binary distribution of Ren'Py contains a number of other
packages, each with their own licenses, which you may be bound to
comply with when distributing a Ren'Py game.
These packages may include:
Python (Python License)
http://www.python.org
Pygame (GNU LGPL)
http://www.pygame.org
SDL (GNU LGPL)
http://www.libsdl.org
SDL_mixer (GNU LGPL)
http://www.libsdl.org/projects/SDL_mixer
SDL_ttf (GNU LGPL)
http://www.libsdl.org/projects/SDL_ttf
ctypes (MIT)
http://starship.python.net/crew/theller/ctypes/
+1 -4
View File
@@ -1,4 +1 @@
Have a new form of imagemap that uses a color image to distinguish
hotspots.
Keyd compiles.
Have a new form of imagemap that uses a color image to distinguish hotspots.
+1 -1
View File
@@ -26,6 +26,6 @@ sys.argv[1:] = [ 'py2exe' ]
setup(name="RenPy",
windows=programs,
console=[ "archiver.py", "add_from.py", "console.py", "dump_text.py" ],
console=[ "archiver.py", "add_from.py", "console.py" ],
zipfile='lib/renpy.zip',
)
+7 -13
View File
@@ -113,11 +113,11 @@ init -500:
style = type
if selected and not disabled:
if selected:
style += "_selected"
if disabled:
clicked = None
style += "_disabled"
style = style + "_button"
text_style = style + "_text"
@@ -302,7 +302,7 @@ init -500:
ui.fixed()
ui.window(style='gm_nav_window')
ui.vbox(focus='gm_nav')
ui.vbox()
for key, label, target, enabled in library.game_menu:
@@ -311,7 +311,7 @@ init -500:
if not eval(enabled):
disabled = True
clicked = None
clicked = ui.returns(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 = True
clicked=None
enable_hover = False
ui.button(style='file_picker_entry',
clicked=clicked,
@@ -382,10 +382,7 @@ init -500:
fpi = 0
if newest:
fpi = (int(newest) - 1) // library.file_page_length * library.file_page_length
if fpi < 0:
fpi = 0
fpi = int(newest) // library.file_page_length * library.file_page_length
# The length of a half-page of files.
@@ -517,9 +514,6 @@ 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
@@ -1,121 +0,0 @@
# 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
+44 -22
View File
@@ -36,9 +36,6 @@ init -1000:
style.default.antialias = True
style.default.size = 22
style.default.color = (255, 255, 255, 255)
style.default.bold = False
style.default.italic = False
style.default.underline = False
style.default.drop_shadow = (1, 1)
style.default.drop_shadow_color = (0, 0, 0, 128)
style.default.minwidth = 0
@@ -46,8 +43,13 @@ init -1000:
style.default.text_y_fudge = 0
style.default.first_indent = 0
style.default.rest_indent = 0
style.default.line_spacing = 0
# Change this if you're not using Vera 22.
if renpy.windows():
style.default.line_height_fudge = -4
else:
style.default.line_height_fudge = 0
# Window properties.
style.default.background = None
style.default.xpadding = 0
@@ -66,7 +68,8 @@ init -1000:
style.default.yanchor = 'top'
# Sound properties.
style.default.sound = None
style.default.hover_sound = None
style.default.activate_sound = None
# The base style for the large windows.
style.create('window', 'default',
@@ -104,8 +107,6 @@ init -1000:
the label of dialogue. The label is used to
indicate who is saying something.""")
style.say_label.bold = True
style.create('say_dialogue', 'default',
"""(text) The style that is used by default for
the text of dialogue.""")
@@ -122,18 +123,13 @@ init -1000:
# Styles that are used for menus.
style.create('menu', 'default',
"(position) The style that is used for the vbox containing a menu.")
"(sound, position) The style that is used for menus themselves.")
style.create('menu_caption', 'default',
"(text) The style that is used to render a menu caption.")
style.create('menu_choice', 'default',
"""(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.""")
"""(text, hover, sound) The style that is used to render a menu choice.""")
style.menu_choice.hover_color = (255, 255, 0, 255) # yellow
style.menu_choice.activate_color = (255, 255, 0, 255) # yellow
@@ -179,12 +175,9 @@ init -1000:
# Styles that are used by imagemaps
style.create('imagemap', 'image_placement',
'(position) The style that is used for imagemaps.')
'(sound, position) The style that is used for imagemaps.')
style.create('imagemap_button', 'default',
'(window, sound, hover) The style that is used for buttons inside imagemaps.')
# Styles that are used by imagebutttons.
# Style that is used by imagebutttons.
style.create('image_button', 'default',
'(window, sound, hover) The default style used for image buttons.')
@@ -208,7 +201,6 @@ 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.
@@ -222,12 +214,30 @@ 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',
@@ -276,6 +286,13 @@ 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.')
@@ -295,6 +312,11 @@ 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.')
@@ -305,7 +327,7 @@ init -1000:
style.file_picker_entry.xminimum = 400
style.file_picker_entry.ymargin = 2
style.file_picker_entry.background = Solid((255, 255, 255, 255))
style.file_picker_entry.idle_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))
@@ -313,7 +335,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.color = dark_cyan
style.file_picker_text.idle_color = dark_cyan
style.file_picker_text.hover_color = bright_cyan
style.create('file_picker_new', 'file_picker_text',
+9 -53
View File
@@ -2,8 +2,7 @@
import os.path
# Enable psyco. Warning: Check for memory leaks!
# Go psyco! (Compile where we can.)
try:
if not os.path.exists("nopsyco"):
import psyco
@@ -21,21 +20,18 @@ import sys
# Extra things used for distribution.
import encodings.utf_8
import encodings.zlib_codec
import encodings.unicode_escape
import encodings.string_escape
import encodings.raw_unicode_escape
# Load up all of Ren'Py, in the right order.
import renpy
def main():
if __name__ == "__main__":
# Stdout should be a utf-8 stream, so print works nicely.
# utf8writer = codecs.getwriter("utf-8")
# sys.stdout = utf8writer(sys.stdout)
name = os.path.basename(sys.argv[0])
dirname = os.path.dirname(sys.argv[0])
if dirname:
os.chdir(dirname)
if name.find(".") != -1:
name = name[:name.find(".")]
@@ -55,9 +51,6 @@ def main():
op.add_option('--python', dest='python', default=None,
help='Run the argument in the python interpreter.')
op.add_option('--leak', dest='leak', action='store_true', default=False,
help='When the game exits, dumps a profile of memory usage.')
options, args = op.parse_args()
if options.python:
@@ -113,45 +106,8 @@ def main():
os.startfile('traceback.txt')
except:
pass
if options.leak:
memory_profile()
sys.exit(0)
def memory_profile():
print "Memory Profile"
print
print "Showing all objects in memory at program termination."
print
import gc
gc.collect()
objs = gc.get_objects()
c = { } # count
dead_renders = 0
for i in objs:
t = type(i)
c[t] = c.get(t, 0) + 1
if isinstance(i, renpy.display.render.Render):
if i.dead:
dead_renders += 1
results = [ (count, ty) for ty, count in c.iteritems() ]
results.sort()
for count, ty in results:
print count, str(ty)
if dead_renders:
print
print "*** found", dead_renders, "dead Renders. ***"
if __name__ == "__main__":
main()
+4 -83
View File
@@ -124,14 +124,9 @@ label start:
scene washington with fade
show eileen vhappy with dissolve
# Store the current version of Ren'Py into a variable, so we can
# interpolate it into the next line.
$ version = renpy.version()
# Display a line of dialogue. In this case, we manually specify
# who's saying the line of dialogue. We also interpolate in the
# version of Ren'Py we're using.
"Girl" "Hi, and welcome to the %(version)s demo program."
# who's saying the line of dialoge.
"Girl" "Hi, and welcome to the Ren'Py 4 demo program."
# This instantly replaces the very happy picture of Eileen with
# one showing her merely happy. It demonstrates how the show
@@ -636,47 +631,6 @@ label ending:
$ renpy.full_restart()
label speedtest:
with None
scene whitehouse
show eileen happy
with dissolve
e "Okay, I'm going to run the speedtest on your system."
e "I'll only be testing the performance of the dissolve
transition. It taxes your system the most, as it needs to
redraw the entire screen each frame."
$ frames = config.frames
with None
scene washington
show eileen happy
with Dissolve(5.0)
$ frames = config.frames - frames
$ fps = frames / 5.0
e "Well, your system displayed %(frames)d frames in five
seconds. That's %(fps).1f fps."
e "Remember, this is the worst-case speed, as usually we can just
draw the parts of the screen that have changed."
e "Thanks for viewing the secret speed test."
return
# Setup the secret key for the speedtest.
init:
python:
config.keymap['speedtest'] = [ 'S' ]
config.underlay.append(renpy.Keymap(speedtest=renpy.curried_call_in_new_context('speedtest')))
init:
# This is just some example code to show the ui functions in
@@ -839,9 +793,6 @@ label whatsnew:
"I'd like to start with 4.6.":
jump whatsnew46
"I'd like to start with 4.7.":
jump whatsnew47
label whatsnew45:
show washington
@@ -972,38 +923,6 @@ label whatsnew46:
e "A few more obscure features involving things like overlays and
activated widgets round out the 4.6 release."
label whatsnew47:
e "Ren'Py 4.7 brought with it a total rewrite of the way text is
rendered to the screen."
e "It introduced text tags, which let a script writer control how
text is shown on the screen."
e "Text tags can make text {b}bold{/b}, {i}italic{/i}, or even
{u}underlined{/u}."
e "They can make the font size {size=+12}bigger{/size} or
{size=-8}smaller{/size}."
e "They can even change the
{color=#f00}color{/color}
{color=#ff0}of{/color}
{color=#0f0}the{/color}
{color=#0ff}text{/color}."
e "We also added bold, italic, and underline style properties, which can
be styled onto any text."
e "Used with care, text tags can enhance {b}your{/b} game."
e "{u}Used{/u} with {i}abandon,{/i} they {b}can{/b} make {b}your{/b}
game {color=#333}hard{/color} {color=#888}to{/color} {color=#ccc}read{/color}."
e "With great power comes great responsibility, after all."
e "And we want to give you all the power you need."
label whatsnewend:
e "Anyway, now that you've heard about some of the new features, is there anything
@@ -1011,4 +930,6 @@ label whatsnewend:
return
+1 -3
View File
@@ -97,7 +97,7 @@ def main():
doc_files = [
'example.html',
'reference.html',
'tutorial.html',
'style.css',
]
@@ -123,11 +123,9 @@ def main():
cp("README_RENPY.txt")
cp("archive_images.bat")
cp("run_game.py", license=license)
copy_file("run_game.py", target + "/run_game.pyw", license=license)
cp("archiver.py", license=license)
# cp("build_exe.py", license=license)
cp("add_from.py", license=license)
cp("dump_text.py", license=license)
cp("renpy-mode.el")
+5 -5
View File
@@ -1,9 +1,9 @@
all:: reference.html example.html
all:: tutorial.html example.html
reference.html: reference.xml preprocess.py stylesheet.xslt style.css styles.xml
python preprocess.py reference.xml > reference.hi.xml
xsltproc stylesheet.xslt reference.hi.xml > reference.html
cp reference.html style.css ~/ab/website/renpy/devel/doc
tutorial.html: tutorial.xml preprocess.py stylesheet.xslt style.css styles.xml
python preprocess.py tutorial.xml > tutorial.hi.xml
xsltproc stylesheet.xslt tutorial.hi.xml > tutorial.html
cp tutorial.html style.css ~/ab/website/renpy/devel/doc
example.html: example.xml preprocess.py stylesheet.xslt style.css styles.xml ../demo2/script.rpy
-9
View File
@@ -1,6 +1,5 @@
#!/usr/bin/python
import os
import re
import sys
import time
@@ -133,14 +132,6 @@ 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)
+73 -273
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<doc>
<title>The Ren'Py Reference Manual</title>
<title>Writing Visual Novels with Ren'Py</title>
<subtitle>The Ren'Py Tutorial</subtitle>
<p>
<b>Last Updated:</b> <!-- date -->
@@ -358,8 +359,7 @@ e "Starting today, I'll be living here."
<p>
Finally, the string in a dialogue is subject to interpolation of
variables. A string variable can be interpolated with %(name)s,
while a number requires %(name)d. The interpolated value is quoted
so that a text tag cannot be interpolated in by mistake. For example:
while a number requires %(name)d. For example:
</p>
<example>
@@ -1722,16 +1722,14 @@ e "Pleased to meet you, %(name)s."
<!-- func renpy.watch -->
<!-- func renpy.loadable -->
<!-- func renpy.windows -->
<!-- func renpy.exists -->
<!-- func renpy.context -->
<!-- func renpy.clear_game_runtime -->
<!-- func renpy.get_game_runtime -->
<!-- func color -->
<p>
@@ -2022,11 +2020,10 @@ init:
stdout (wherever that may go to).
</var>
<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
<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
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
@@ -2050,11 +2047,6 @@ screen size. This is mostly a suggestion, as if Ren'Py ever needs to
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.
@@ -2087,9 +2079,9 @@ load scripts, as scripts will be loaded before it can be set.
is slightly increased.
</var>
<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 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>
<var name="config.sound_sample_rate" value="44100">
@@ -2279,10 +2271,10 @@ performed when exiting the game menu.
init:
# Selected menu choices should be yellow and solid.
$ style.menu_choice.hover_color = (255, 255, 0, 255)
$ style.menu_choice_selected.color = (255, 255, 0, 255)
# Unselected menu choices should be cyan and translucent.
$ style.menu_choice.idle_color = (0, 255, 255, 128)
$ style.menu_choice_unselected = (0, 255, 255, 128)
</example>
@@ -2310,25 +2302,12 @@ init:
<prop name="color">
The color in which the text will be displayed on the screen, as an
RGBA tuple. Due to a limitation of pygame, currently the alpha is
ignored. Example: (255, 255, 255, 255)
RGBA tuple. Example: (255, 255, 255, 255)
</prop>
<prop name="bold">
If True, then this text will be rendered in bold.
</prop>
<prop name="italic">
If True, then this text will be rendered in italics.
</prop>
<prop name="underline">
If true, then this text will be rendered with an underline.
</prop>
<prop name="antialias">
If True, the text will be antialiased. Otherwise, it will be left
jagged.
jagged. Example: True
</prop>
<prop name="drop_shadow">
@@ -2351,10 +2330,13 @@ If true, then this text will be rendered with an underline.
at least this many pixels long.
</prop>
<prop name="line_spacing">
This is used to increase or decrease the spacing between lines of
text by a constant number of pixels. A positive value increases, while
a negative value decreases the spacing.
<prop name="line_height_fudge">
On some platforms (Windows), the height of a line of text isn't
reported correctly. This can lead to an overly-large spacing
between lines. This is a number that is added to the reported line
spacing. If negative, the lines will be pushed closer
together. This is very platform-dependent, so you may want to
condition it on the result of renpy.windows().
</prop>
<prop name="text_y_fudge">
@@ -2568,7 +2550,12 @@ top_padding and bottom_padding to the same value.
<p>
The ui.bar() widget has a few properties that are specific to
it, that control the look of the bars.
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.
</p>
<prop name="left_bar">
@@ -2583,22 +2570,29 @@ 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 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,
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,
default.hover_background, default.background.)
</p>
@@ -2651,7 +2645,7 @@ init:
init:
$ style.button.color = (128, 255, 128, 255)
$ style.button.xpos = 0
$ style.button.idle_background = \
$ style.button_idled.background = \
renpy.Frame(renpy.Image("button_idled.png"),
xborder=10, yborder=10)
$ style.button_hover.background = \
@@ -2708,95 +2702,6 @@ init:
</dl>
<h3>Text Tags</h3>
<p>
Styles are useful if you want to change the look of entire blocks
of text in the game. But what if you only want to change the look of a
small amount of text, or if you want to only change the look of entire
blocks of text. Text tags let you change the look of text within a
single text widget.
</p>
<p>
Text tags work wherever text can be placed. This means that they can
be used inside text widgets, text buttons, and menu and say
statements. But please note that say statement protect from
subsituting in a text tag, as that would most likely be an
error. Generally, though, if Ren'Py shows text to the user, that text
can be formatted through the use of text tags.
</p>
<p>
As an aside, let me point out that if you're using a text tag on every
line of a script, or even many lines, you're probably doing something
wrong. That's what styles are for.
</p>
<p>
Text tags start with a left brace, and continue to the matching right
brace. We chose to use braces because they do not require quoting in
HTML or BBCode. An open text tag may take a parameter, and then continues until
a closing tag, which is a tag that has a slash immediately after the
opening brace. Please note that text tags are sensitive to both case
and whitespace.
</p>
<p>
Some example tags are {b}, {i}, {color=#f88}, {size=+16}. The matching
closing tags are {/b}, {i}, {/color}, and {/size}. Tags must be
properly nested: "{b}{i}bold + italic{/i}{/b}" is okay,
but "{b}{i}this is wrong{/b}{/i}". Right now, improper nesting won't
cause an error but it probably won't do what you want. All open tags
must be closed by the end of the string.
</p>
<p>
If you, for some reason, want to include a left brace, just write two
left braces ({{) instead.
</p>
<h4>Supported Text Tags</h4>
<p>The following are supported text tags:</p>
<ul>
<li>{b}text{/b} makes the enclosed text bold.</li>
<li>{i}text{/i} makes the enclosed text italic.</li>
<li>{u}text{/u} makes the enclosed text underlined.</li>
<li>{plain}text{/plain} makes the enclosed text plain, which means that
it is not bold, italic, or underlined.</li>
<li>{size=<i>spec</i>}text{/size} changes the size of the text. The
supplied spec may be a number, in which case it is the number of
pixels high that the text will be. If it is a number preceded by a
plus (like "+10"), it means to increase the size by that number of
pixels, while if it is preceded by a minus it means to decrease.
</li>
<li>{color=<i>spec</i>}text{/color} changes the color of the text. The
color may be a hex triple or a hex sextuple, optionally preceded by a
hash mark. (This is the same format that color() accepts.)</li>
</ul>
<p>
Here's an example of a say statement that uses many of the text
tags. If you actually use this many text tags in a real game, you'll
probably drive your users mad.
</p>
<example>
"Using text tags, we can make text {size=+12}bigger{/size} or
{size=-8}smaller{/size}. We can make it {b}bold{/b}, {i}italic{/i},
or {u}underlined{/u}. We can even change its {color=#f88}color{/color}."
</example>
<h3>Obfuscating your Game</h3>
<p>
@@ -2823,13 +2728,13 @@ probably drive your users mad.
</p>
<p>
Images can be archived using the archiver.py or archiver.exe
Images can be archived using the archive.py or archive.exe
programs that ship as part of Ren'Py. To use this, one can change
into the game directory and run a command like:
</p>
<p>
<kbd>../archiver.exe images *.png *.jpg</kbd>
<kbd>../archive.exe images *.png *.jpg</kbd>
</p>
<p>
@@ -2950,26 +2855,31 @@ 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', 'mouseup_3' ],
hide_windows = [ 'mouseup_2', 'h' ],
game_menu = [ 'K_ESCAPE', 'mousedown_3' ],
hide_windows = [ 'mouseup_2' ],
# Say.
rollforward = [ 'mousedown_5', 'K_PAGEDOWN' ],
dismiss = [ 'mouseup_1', 'K_RETURN', 'K_SPACE', 'K_KP_ENTER' ],
# Focus.
focus_left = [ 'K_LEFT' ],
focus_right = [ 'K_RIGHT' ],
focus_up = [ 'K_UP' ],
focus_down = [ 'K_DOWN' ],
# 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' ],
# Button.
button_select = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER' ],
@@ -2977,6 +2887,12 @@ 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' ],
@@ -3064,18 +2980,6 @@ 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.
@@ -3119,6 +3023,8 @@ automatically present in the game namespace.
<!-- func ui.pausebehavior -->
<!-- func ui.keymousebehavior -->
<h4>Functions</h4>
<!-- func ui.interact -->
@@ -3146,112 +3052,6 @@ 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>
+3 -4
View File
@@ -1,8 +1,7 @@
init:
image black = Solid((0, 0, 0, 255))
label main_menu:
label start:
$ renpy.renpy.style.write_docs("doc/styles.xml")
$ raise "foo"
$ style._write_docs("doc/styles.xml")
"Dumped style."
-76
View File
@@ -1,76 +0,0 @@
#!/usr/bin/env python
# This program dumps all the text found in the script to the file text.txt.
# If on windows, it also tries to show text.txt to the user.
import codecs
import sys
import os
import re
import glob
import renpy
def process_block(block, out):
for fn, ln, text, child in block:
if text.startswith("$") or text.startswith("python"):
continue
if text.startswith("init"):
continue
if text.startswith("if") or text.startswith("while"):
process_block(child, out)
continue
for m in re.finditer(r'"((?:[^\\"]+|\\.)+)"|' +
r"'((?:[^\\']+|\\.)+)'", text):
s = m.group(1) or m.group(2)
s = re.sub(r'\s+', ' ', s)
s = re.sub(r'\\.', ' ', s)
s = re.sub(r'\{.*?\}', '', s)
print >>out, s.encode('utf-8')
print >>out
process_block(child, out)
def process(fn, out):
block = renpy.parser.group_logical_lines(renpy.parser.list_logical_lines(fn))
process_block(block, out)
def main():
pattern = "game/*.rpy"
if len(sys.argv) >= 2:
pattern = sys.argv[1]
files = glob.glob(pattern)
files = [ i for i in files if not i.startswith("common/") ]
out = file("text.txt", "w")
out.write(codecs.BOM_UTF8)
for fn in files:
process(fn, out)
out.close()
try:
os.startfile('text.txt')
except:
pass
if __name__ == "__main__":
main()
-35
View File
@@ -1,35 +0,0 @@
# This file adds a number of buttons to the lower-right hand corner of
# the screen. Three of these buttons jump to the game menu, which
# giving quick access to Load, Save, and Prefs. The fourth button
# toggles skipping, to make that more convenient.
init:
# Give us some space on the right side of the screen.
$ style.window.right_margin = 100
python:
def toggle_skipping():
config.skipping = not config.skipping
def button_game_menu():
# to save typing
ccinc = renpy.curried_call_in_new_context
ui.vbox(xpos=0.98, ypos=0.98, xanchor='right', yanchor='bottom')
ui.textbutton("Skip", clicked=toggle_skipping)
ui.textbutton("Save", clicked=ccinc("_game_menu_save"))
ui.textbutton("Load", clicked=ccinc("_game_menu_load"))
ui.textbutton("Prefs", clicked=ccinc("_game_menu_preferences"))
ui.close()
config.overlay_functions.append(button_game_menu)
-262
View File
@@ -1,262 +0,0 @@
# 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
-247
View File
@@ -1,247 +0,0 @@
# This is the annoying readback mode. I implemented this because
# people asked for it, not because I like it. I'd prefer you use
# rollback, which is better in every way.
# To use readback, drop this file into your game directory. Readback
# will take over when rollback no longer works, so you'll also need to
# either reduce config.hard_rollback_limit or set
# config.rollback_enabled to false.
# init:
# $ config.hard_rollback_limit = 1
# Readback works by replacing the default Character and Menu objects
# with ones that record what is said in a readback buffer. The user
# can then go back, and what has been said will be shown to them
# again. Images and the like will not change... if you want that, use
# rollback. Heck, use rollback anyway. :-P
# The number of lines of readback can be changed by assigning an
# integer to the readback_limit variable. But leaving it None will
# leave the entire game in the buffer. As this is actually fairly
# memory-efficent, you should probably leave it as None.
# Readback uses two styles, readback_dialogue and
# readback_thought. Change them to change the color and look of
# read-back text.
# If you want to add your own message, as narration, to the readback
# buffer, you can do it by calling the readback function or having
# the readback character say it:
# $ readback('A message for readbackers only.')
# readback "A similar message."
# Note: When using Readback, you can no longer use a string for a
# character's name. All dialogue must be routed through Character
# objects, if it is to show up in the readback buffer. This limitation
# will be fixed in a future version of Ren'Py.
init -100:
python:
# The limit of the number of readbacks to keep. None means no limit.
readback_limit = None
# Set this to true to print out the contents of the readback
# buffer when it is saved.
readback_debug = False
# Readback styles.
style.create('readback_dialogue', 'say_dialogue', '')
style.create('readback_thought', 'say_thought', '')
style.readback_dialogue.color = (255, 128, 128, 255)
style.readback_thought.color = (255, 128, 128, 255)
# No user-servicable parts below this line. ######################
# The readback buffer is a doubly-linked list of readback
# objects.
class Readback(object):
def __init__(self, obj, args):
self.older = None
self.newer = None
self.obj = obj
self.args = args
def show(self):
self.obj.readback(*self.args)
readback_oldest = None
readback_newest = None
readback_count = 0
# This saves a readback entry to the readback buffer.
def readback_save(obj, *args):
store.readback_count += 1
if readback_limit and readback_count > readback_limit:
store.readback_count -= 1
readback_oldest.newer.older = None
store.readback_oldest = readback_oldest.newer
rb = Readback(obj, args)
if readback_newest:
readback_newest.newer = rb
else:
store.readback_oldest = rb
rb.older = readback_newest
store.readback_newest = rb
if readback_debug:
print "---- Readback Buffer ----"
rb = readback_oldest
while rb:
print rb.obj, rb.args
rb = rb.newer
# The rest of this file is replacing the default objects and
# functions with versions that save things in the readback
# buffer.
# Save the old character object.
readback_OldCharacter = Character
readback_OldDynamicCharacter = DynamicCharacter
readback_oldmenu = menu
class Character(readback_OldCharacter):
def __init__(self, who,
readback_style='readback_dialogue',
**kwargs):
readback_OldCharacter.__init__(self, who, **kwargs)
self.readback_style = readback_style
def __call__(self, what, **kwargs):
readback_OldCharacter.__call__(self, what, **kwargs)
readback_save(self, what)
def readback(self, what):
renpy.display_say(self.name, what,
who_style=self.who_style,
what_style=self.readback_style,
window_style=self.window_style,
interact=False,
**self.properties)
class DynamicCharacter(readback_OldDynamicCharacter):
def __init__(self, who,
readback_style='readback_dialogue',
**kwargs):
readback_OldDynamicCharacter.__init__(self, who, **kwargs)
self.readback_style = readback_style
def __call__(self, what, **kwargs):
name = renpy.renpy.python.py_eval(self.name_expr)
readback_OldDynamicCharacter.__call__(self, what, **kwargs)
readback_save(self, name, what)
def readback(self, name, what):
renpy.display_say(self.name, what,
who_style=self.who_style,
what_style=self.readback_style,
window_style=self.window_style,
interact=False,
**self.properties)
class Sayer(object):
def __call__(self, who, what):
renpy.display_say(who, what)
readback_save(self, who, what)
def readback(self, who, what):
renpy.display_say(who, what,
what_style='readback_dialogue',
interact=False)
narrator = Character(None, what_style='say_thought')
say = Sayer()
def readback(what):
readback_save(narrator, what)
def menu(menuitems):
rv = readback_oldmenu(menuitems)
text = '\n'.join([ l for l, v in menuitems
if v is None or v == rv ])
readback(text)
return rv
# This stuff is involved in entering the readback mode.
def readback_mode():
# Try rollback, first.
renpy.rollback()
# If we made it here, we're into readback mode. So let's
# go there now.
renpy.call_in_new_context("readback")
# Add in the readback function.
config.underlay.append(renpy.Keymap(rollback=readback_mode))
# This label is called in a new context, when the user succesfully
# enters Readback mode.
label readback:
# If we have an empty readback buffer, go home.
if not readback_newest:
return
python hide:
rb = readback_newest
while True:
rb.show()
ui.add(renpy.Keymap(rollback=lambda : "older",
rollforward=lambda : "newer",
dismiss=lambda : "dismiss",
))
res = ui.interact()
if res == "newer":
rb = rb.newer
if not rb:
break
elif res == "older":
if rb.older:
rb = rb.older
elif res == "dismiss":
break
return
-134
View File
@@ -1,134 +0,0 @@
;;; Comments beginning with ";;;" are commentary added by PyTom to
;;; try to help you understand this file.
;;;
;;; This installer expects that you will have supplied LICENSE.txt
;;; and README.txt files in the root directory of your game.
;;;
;;; To use this, place it in the root directory of your game,
;;; right-click on it, and choose "Compile NSIS script..."
;;; First up, change the settings below to match your game.
;;; The exe used to run your game.
!define EXE "moonlight.exe"
;;; The exe containing the installer. This will be created in the directory
;;; above the root directory containing your game.
!define INSTALLER_EXE "moonlight-1.0ni.exe"
;;; The name and version.
!define PRODUCT_NAME "Moonlight Walks"
!define PRODUCT_VERSION "1.0"
;;; The following settings are only shown to the user in Add/Remove programs
;;; but you'll still want to use them.
!define PRODUCT_WEB_SITE "http://www.bishoujo.us/moonlight/"
!define PRODUCT_PUBLISHER "American Bishoujo"
;;; Ignore this next block of stuff. It's mostly boilerplate.
!include "MUI.nsh"
!define MUI_ABORTWARNING
;;; Change this to change the compression scheme.
SetCompressor lzma
;;; You can change these to customize the bitmaps and icons used for
;;; your installer and uninstaller. Bitmaps should be 150x57.
; !define MUI_HEADERIMAGE
; !define MUI_HEADERIMAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Header\nsis.bmp"
; !define MUI_HEADERIMAGE_UNBITMAP "${NSISDIR}\Contrib\Graphics\Header\nsis.bmp"
!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico"
;;; This is the sequencing of the pages that does the actual installation.
;;; you can comment pages out, if you want to.
; Welcome page
!insertmacro MUI_PAGE_WELCOME
; License page
!insertmacro MUI_PAGE_LICENSE "LICENSE.txt"
; Directory page
!insertmacro MUI_PAGE_DIRECTORY
; Instfiles page
!insertmacro MUI_PAGE_INSTFILES
; Finish page
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README.txt"
!insertmacro MUI_PAGE_FINISH
; Uninstaller pages
!insertmacro MUI_UNPAGE_INSTFILES
;;; Okay, that's it for the commentary. You're on your own from here...
;;; but you probably won't need to touch anything below this point.
; Various other defines.
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
!define PRODUCT_UNINST_ROOT_KEY "HKLM"
; Language files
!insertmacro MUI_LANGUAGE "English"
; Reserve files
!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
OutFile "..\${INSTALLER_EXE}"
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
Section "!${PRODUCT_NAME}" SEC01
SetOutPath "$INSTDIR"
SetOverwrite ifnewer
File /r /x renpy /x *.py /x *.pyw /x installer.nsi /x persistent *.*
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${EXE}"
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${EXE}"
SectionEnd
Section -AdditionalIcons
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninst.exe"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\README.lnk" "$INSTDIR\README.txt"
SectionEnd
Section -Post
WriteUninstaller "$INSTDIR\uninst.exe"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\${EXE}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
SectionEnd
Function un.onUninstSuccess
HideWindow
MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) was successfully removed from your computer."
FunctionEnd
Function un.onInit
MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove $(^Name) and all of its components?" IDYES +2
Abort
FunctionEnd
Section Uninstall
Delete "$SMPROGRAMS\${PRODUCT_NAME}\README.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk"
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
RMDir "$SMPROGRAMS\${PRODUCT_NAME}"
RMDir /r "$INSTDIR"
DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}"
SetAutoClose true
SectionEnd
+112
View File
@@ -0,0 +1,112 @@
Well, [url="http://www.noradsanta.org"]NORAD has locked its
interceptors onto Santa[/url], so I guess that means it's
Christmas. And what a better way to celebrate than a new release of
Ren'Py. (Well, how about with your families or something like that?)
Ren'Py 4.4 "Christmas Bonus" contains a number of feature
enhancements, speed improvements, and bug fixes. Along with improved
support for visual novels, this release lays the foundations for
Stats-Based Dating Simulations. And it's my gift to you, downloadable
from the Ren'Py website,
[url]http://www.bishoujo.us/renpy/[/url]
Upgrading a script from recent (4.2 or better) versions of Ren'Py
should be as simple as copying over your old game directory, and
perhaps deleting a line setting library.file_page_length.
This is a fairly major release, containing a bunch of new features and
bug fixes. This is what you should be using if you want to make a
Ren'Py game, at least until a new version comes out.
I'd like to thank Mikey for testing a pre-release of this to ensure
that all the bugs with UTF-8 were well and truly squashed. Thanks to
him, I now know what a LOWERCASE L WITH CARON is.
Anyway, as usual, feel free to post, email, or IM me if you have any
questions, problems, comments, or suggestions. And have a great
holiday, everyone.
[b]New in 4.4[/b]
Added a number of features to allow Ren'Py to support statistics-based
dating simulations. The first is a new ui.bar() widget, which allows
the display of a bar graph. The demo has been enhanced to include a
stats and schedule screen, to show how this could be used. You can get
to it by asking how to write your own games in the demo, or you can
follow the below link to get a screenshot.
[url]http://www.bishoujo.us/hosted/screenshot.jpg[/url]
Another feature that has been added were 'jump expression' and 'call
expression' statements, allowing computed jumps and calls. Together
with an imporoved scheduler and yet-to-be written event dispatcher,
this provides the foundation for SBDSes.
Now, setting a property on a style also sets the hover_ and idle_
variants of that property. This makes changing a property of an
inherited style a bit saner. In addition, hover now propagates to all
children of a button. This means that hover will now work with the
styles in the file chooser.
Added support for layering images. This support (invoked by passing a
tuple as an image filename) allows a single image to be constructed
from multiple image files, saving disk space while still keeping
performance.
Improved skipping. Now, along with control causing skipping of seen
dialogue, TAB toggles skip mode. An indicator displays to let you know
that skip mode is enabled. Finally, if a game author wants to disable
skipping, he can set config.allow_skipping to false.
(The following were also in 4.3.2, which was a private release to
test the fixes for UTF-8 support.)
There were two bugs with UTF-8 support. The first was that we didn't
understand the byte-order mark, and choked on the syntax error. The
second was that the error-reporting code wasn't passing unicode errors
through properly, so one couldn't even see the real error. Both are
now fixed.
Improved the reporting of errors that occur during script loading and
interpreter initialization by no longer giving a line number when none
is appropriate.
Ren'Py now uses the name of the executable to choose the directory to
read the script from. It does this by looking at the name of the
executable that was used to run Ren'Py. It strips off the extension,
and anything preceding the first underscore in the name, if such a
thing exists. It then looks to see if that directory exists, and if it
does, uses it. For example, if the program is named "run_en.exe", the
"en" directory is used, while if the program is named "homestay.exe",
the directory "homestay" is used.
The config variable config.searchpath is a list of directory that are
searched for image files and other media (but not scripts, since all
scripts are loaded before the variable can be set). This allows
multiple game directories to share images, music, and other data files.
Once again, I redid the file chooser and default styles. The new file
chooser displays 10 entries in two columns, with each entry being
shown with an image. The files are now orginized into numbered slots,
and it's possible to save in a slot without saving in all previous
slots. There were also some changes to the non-user-visible parts of
loading and saving.
[b]Speed Enhancements:[/b]
We now precompile python blocks in Ren'Py scripts, and store the
compiled code in the .rpyc files. This makes loading an unmodified
script significantly faster. On my system, this more than halved the
time it took for the demo script to load. On the dowside, when a
script is changed, it now can take somewhat longer for it to begin
running, as all the changed code needs to be recompiled. Overall,
it's a win, provided you ship the .rpyc files to your users.
Did another round of profiling, and found that styles were
significanly slowing the system down. So I rewrote them to be much
faster. This change shouldn't be user-visible, except that your game
will feel a bit peppier.
Added a 1-entry cache for solid fill surfaces. If your game uses them,
this might make it go faster.
+4 -6
View File
@@ -2,9 +2,9 @@
# order.
# Some version numbers and things.
version = "Ren'Py 4.8"
script_version = 8000
savegame_suffix = "-8.save"
version = "Ren'Py 4.6"
script_version = 5
savegame_suffix = "-6.save"
# Can be first, because has no dependencies, and may be imported
@@ -32,10 +32,8 @@ import renpy.display.text # core
import renpy.display.layout # core
import renpy.display.behavior # layout
import renpy.display.transition # core
import renpy.display.im
import renpy.display.image # core, behavior, im
import renpy.display.image # core, behavior
import renpy.display.video
import renpy.display.focus
import renpy.ui
+2 -7
View File
@@ -1,4 +1,3 @@
# This file contains the AST for the Ren'Py script language. Each class
# here corresponds to a statement in the script language.
@@ -118,6 +117,7 @@ def say_menu_with(expression):
if renpy.game.preferences.transitions:
renpy.game.interface.set_transition(what)
class Say(Node):
def __init__(self, loc, who, what, with):
@@ -136,7 +136,7 @@ class Say(Node):
who = None
say_menu_with(self.with)
renpy.exports.say(who, self.what)
renpy.exports.say(who, self.what % renpy.game.store)
return self.next
@@ -286,10 +286,6 @@ 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):
@@ -302,7 +298,6 @@ def predict_imspec(imspec, callback):
return
im = renpy.exports.images[imspec[0]]
im.predict(callback)
+26 -27
View File
@@ -47,9 +47,9 @@ profile = False
# The directory save files will be saved to.
savedir = None
# 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 images that are allowed to live in the image cache
# at once.
image_cache_size = 10
# The number of statements we will analyze when doing predictive
# loading. Please note that this is a total number of statements in a
@@ -83,6 +83,9 @@ 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
@@ -110,14 +113,6 @@ 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(
@@ -128,18 +123,24 @@ keymap = dict(
toggle_fullscreen = [ 'f' ],
toggle_music = [ 'm' ],
game_menu = [ 'K_ESCAPE', 'mouseup_3' ],
hide_windows = [ 'mouseup_2', 'h' ],
hide_windows = [ 'mouseup_2' ],
# Say.
rollforward = [ 'mousedown_5', 'K_PAGEDOWN' ],
dismiss = [ 'mouseup_1', 'K_RETURN', 'K_SPACE', 'K_KP_ENTER' ],
# Focus.
focus_left = [ 'K_LEFT' ],
focus_right = [ 'K_RIGHT' ],
focus_up = [ 'K_UP' ],
focus_down = [ 'K_DOWN' ],
# 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' ],
# Button.
button_select = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER' ],
@@ -147,21 +148,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
def backup():
+219 -520
View File
@@ -1,11 +1,6 @@
# This module contains code that handles the playing of sound and
# music files.
# NOTE TO SELF:
#
# Remember to code defensively against mikey's computer that
# doesn't have the sound card in it.
import pygame
import renpy
import sys # to detect windows.
@@ -20,157 +15,257 @@ import sys # to detect windows.
# read the same value from midiOutGetVolume() as we did before we tried
# doing that.
# True if the mixer works, False if it doesn't, None if we have no
# idea yet.
mixer_works = None
# Common stuff.
mixer_enabled = True
playing_midi = True
fading = False
master_music_volume = 1.0
# Windows stuff.
midi_msf = 0.0
last_raw_volume = -1
def init():
windows_magic = False
global mixer_works
global read_raw_volume
global compute_midi_msf
global set_music_volume
global playing_midi
if hasattr(sys, 'winver'):
if mixer_works is not None:
return
midi_msf = 0.0
try:
pygame.mixer.music.get_volume()
mixer_works = True
except:
if renpy.config.debug_sound:
raise
last_raw_volume = -1
def read_raw_volume():
res = c_uint()
for i in range(0, winmm.midiOutGetNumDevs()):
rv = winmm.midiOutGetVolume(i, byref(res))
if not rv:
return res.value
else:
mixer_works = False
print "Couldn't read raw midi volume."
return -1
windows_magic = False
if hasattr(sys, 'winver') and mixer_works:
try:
from ctypes import windll, c_uint, byref
winmm = windll.winmm
try:
from ctypes import windll, c_uint, byref
winmm = windll.winmm
def compute_midi_msf():
"""
Computes the Midi MSF. Returns True if successful, False if otherwise.
"""
# Don't update the MSF when fading is going on, or when not
# playing a midi. (Except before playing any music whatsoever.)
if fading or not playing_midi:
return False
def _read_raw_volume():
res = c_uint()
global last_raw_volume
raw_vol = read_raw_volume()
for i in range(0, winmm.midiOutGetNumDevs()):
rv = winmm.midiOutGetVolume(i, byref(res))
if not rv:
return res.value
else:
print "Couldn't read raw midi volume."
return -1
read_raw_volume = _read_raw_volume
def _compute_midi_msf():
"""
Computes the Midi MSF. Returns True if successful, False if otherwise.
"""
# Don't update the MSF when fading is going on, or when not
# playing a midi. (Except before playing any music whatsoever.)
if fading or not playing_midi:
return False
global last_raw_volume
raw_vol = read_raw_volume()
if raw_vol < 0:
return False
# The case in which the volume hasn't changed recently.
if raw_vol == last_raw_volume:
return True
last_raw_volume = raw_vol
# print "raw_vol", raw_vol
# The fraction that the midi mixer is at.
mixfrac = 1.0 * ( raw_vol & 0xffff ) / 0xffff
global midi_msf
midi_msf = mixfrac / master_music_volume
# print "Midi msf is now:", midi_msf
if raw_vol < 0:
return False
# The case in which the volume hasn't changed recently.
if raw_vol == last_raw_volume:
return True
compute_midi_msf = _compute_midi_msf
last_raw_volume = raw_vol
# This should get called after the music starts playing.
def _set_music_volume(vol):
# print "raw_vol", raw_vol
# The fraction that the midi mixer is at.
mixfrac = 1.0 * ( raw_vol & 0xffff ) / 0xffff
global master_music_volume
master_music_volume = vol
global midi_msf
midi_msf = mixfrac / master_music_volume
if playing_midi:
# print "Midi msf is now:", midi_msf
vol *= midi_msf
if vol > 1.0:
vol = 1.0
return True
try:
pygame.mixer.music.set_volume(vol)
except:
if renpy.config.debug_sound:
raise
global last_raw_volume
last_raw_volume = read_raw_volume()
set_music_volume = _set_music_volume
# Figure out the default msf, and set it up.
windows_magic = compute_midi_msf()
playing_midi = False
except Exception, e:
print "Exception when trying to init music:", str(e)
print "Falling back to Unix mode."
if not windows_magic:
def _compute_midi_msf():
return
compute_midi_msf = _compute_midi_msf
def _set_music_volume(vol):
if not mixer_works:
return
# This should get called after the music starts playing.
def set_music_volume(vol):
global master_music_volume
master_music_volume = vol
try:
pygame.mixer.music.set_volume(vol)
except:
if renpy.config.debug_sound:
raise
if playing_midi:
vol *= midi_msf
if vol > 1.0:
vol = 1.0
set_music_volume = _set_music_volume
pygame.mixer.music.set_volume(vol)
global last_raw_volume
last_raw_volume = read_raw_volume()
# Figure out the default msf, and set it up.
windows_magic = compute_midi_msf()
playing_midi = False
if mixer_works:
pygame.mixer.music.set_endevent(renpy.display.core.MUSICEND)
except Exception, e:
print "Exception when trying to init music:", str(e)
print "Falling back to Unix mode."
if not windows_magic:
def compute_midi_msf():
return
def set_music_volume(vol):
global master_music_volume
master_music_volume = vol
pygame.mixer.music.set_volume(vol)
playing_midi = False
# 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")
# 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.
"""
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.
"""
music_stop()
renpy.game.context().scene_lists.music = (filename, loops, startpos)
restore_music()
def music_stop():
"""
Stops the currently playing music track.
"""
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.
"""
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 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
@@ -194,14 +289,10 @@ def disable_mixer():
pygame mixer.
"""
if not mixer_works:
return
global mixer_enabled
if mixer_enabled:
try:
music_stop()
pygame.mixer.quit()
except:
if renpy.config.debug_sound:
@@ -215,9 +306,6 @@ def enable_mixer():
pygame mixer.
"""
if not mixer_works:
return
global mixer_enabled
if not mixer_enabled:
@@ -229,392 +317,3 @@ 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)
+297 -74
View File
@@ -101,16 +101,41 @@ class Keymap(renpy.display.layout.Null):
for name, action in self.keymap.iteritems():
if map_event(ev, name):
rv = action()
if rv is not None:
return rv
action()
raise renpy.display.core.IgnoreEvent()
# 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
@@ -139,11 +164,10 @@ class SayBehavior(renpy.display.layout.Null):
mouse button.
"""
focusable = True
def __init__(self, default=True, **properties):
super(SayBehavior, self).__init__(default=default, **properties)
def __init__(self):
super(SayBehavior, self).__init__()
def event(self, ev, x, y):
if ev.type == renpy.display.core.DISPLAYTIME and \
@@ -155,7 +179,8 @@ class SayBehavior(renpy.display.layout.Null):
elif renpy.game.context().seen_current(True):
return True
if map_event(ev, "dismiss") and self.is_focused():
if map_event(ev, "dismiss"):
return True
if map_event(ev, "rollforward"):
@@ -163,70 +188,211 @@ 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):
rv = super(Button, self).render(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_')
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.
if self.activated:
self.activated = False
self.activated = False
if self.focusable:
if self.is_focused():
self.set_style_prefix('hover_')
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
else:
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()
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',
@@ -235,6 +401,29 @@ 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):
"""
@@ -242,47 +431,34 @@ class Input(renpy.display.text.Text):
"""
def __init__(self, default, length=None,
style='input_text',
allow=None,
exclude=None,
**properties):
super(Input, self).__init__(default.replace("{", "{{") + "_", style=style, **properties)
style='input_text', **properties):
super(Input, self).__init__(default + "_", style=style, **properties)
self.content = unicode(default)
self.length = length
self.allow = allow
self.exclude = exclude
def event(self, ev, x, y):
if map_event(ev, "input_backspace"):
if self.content:
self.content = self.content[:-1]
self.set_text(self.content.replace("{", "{{") + "_")
self.set_text(self.content + "_")
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()
if self.allow and ev.unicode not in self.allow:
raise renpy.display.core.IgnoreEvent()
if self.exclude and ev.unicode in self.exclude:
raise renpy.display.core.IgnoreEvent()
return None
self.content += ev.unicode
self.set_text(self.content.replace("{", "{{") + "_")
self.set_text(self.content + "_")
renpy.display.render.redraw(self, 0)
raise renpy.display.core.IgnoreEvent()
@@ -294,7 +470,7 @@ class Bar(renpy.display.core.Displayable):
to clicks on that value.
"""
def __init__(self, width, height, range, value,
def __init__(self, width, height, range, value, clicked=None,
style='bar', **properties):
super(Bar, self).__init__()
@@ -306,14 +482,61 @@ 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
lgutter = 0
rgutter = 0
# 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
barwidth = width - lgutter - rgutter
@@ -346,7 +569,7 @@ class Conditional(renpy.display.layout.Container):
self.condition = condition
self.null = renpy.display.layout.Null()
self.state = eval(self.condition, vars(renpy.store))
self.state = eval(self.condition, renpy.game.store)
def render(self, width, height, st):
if self.state:
@@ -356,7 +579,7 @@ class Conditional(renpy.display.layout.Container):
def event(self, ev, x, y):
state = eval(self.condition, vars(renpy.store))
state = eval(self.condition, renpy.game.store)
if state != self.state:
renpy.display.render.redraw(self, 0)
+47 -138
View File
@@ -2,16 +2,15 @@
# 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
@@ -34,40 +33,9 @@ class Displayable(renpy.object.Object):
their fields.
"""
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 __init__(self):
self.style = None
self.style_prefix = None
def set_style_prefix(self, prefix):
"""
@@ -78,7 +46,9 @@ class Displayable(renpy.object.Object):
if prefix == self.style_prefix:
return
self.style.set_prefix(prefix)
if self.style:
self.style.set_prefix(prefix)
self.style_prefix = prefix
renpy.display.render.redraw(self, 0)
@@ -136,8 +106,8 @@ class Displayable(renpy.object.Object):
the images it may want to load.
"""
if self.style and self.style.background:
self.style.background.predict(callback)
return
def place(self, dest, x, y, width, height, surf):
"""
@@ -225,7 +195,6 @@ 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):
@@ -242,7 +211,6 @@ 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:
@@ -251,7 +219,6 @@ class SceneLists(object):
self.music = None
self.movie = None
self.sticky_positions = { }
self.focused = None
def rollback_copy(self):
"""
@@ -264,8 +231,6 @@ class SceneLists(object):
for i in renpy.config.overlay_layers:
rv.layers[i] = [ ]
rv.focused = None
# rv.master = self.master[:]
# rv.transient = self.transient[:]
@@ -380,14 +345,11 @@ class Display(object):
renpy.display.audio.pre_init()
pygame.init()
renpy.display.audio.init()
self.fullscreen = renpy.game.preferences.fullscreen
fsflag = 0
fullscreen = self.fullscreen and not os.environ.get('RENPY_DISABLE_FULLSCREEN', False)
if fullscreen:
if self.fullscreen:
fsflag = FULLSCREEN
# The window we display things in.
@@ -409,12 +371,12 @@ class Display(object):
pygame.display.set_caption(renpy.config.window_title)
if renpy.config.window_icon:
pygame.display.set_icon(renpy.display.im.load_image(renpy.config.window_icon))
pygame.display.set_icon(renpy.display.image.cache.load_image(renpy.config.window_icon))
# Load the mouse image, if any.
if renpy.config.mouse:
self.mouse = renpy.display.im.load_image(renpy.config.mouse)
self.mouse = renpy.display.image.cache.load_image(renpy.config.mouse)
pygame.mouse.set_visible(False)
else:
self.mouse = None
@@ -498,7 +460,6 @@ class Display(object):
self.suppress_mouse = suppress_blit
renpy.display.focus.take_focuses(surftree.focuses)
def save_screenshot(self, filename):
"""
@@ -552,9 +513,6 @@ class Interface(object):
@ivar restart_interaction: If True, the current interaction will
be restarted.
@ivar pushed_event: If not None, an event that was pushed back
onto the stack.
"""
def __init__(self):
@@ -567,7 +525,6 @@ class Interface(object):
self.quick_quit = False
self.force_redraw = False
self.restart_interaction = False
self.pushed_event = None
def take_screenshot(self, scale):
"""
@@ -621,41 +578,12 @@ class Interface(object):
self.transition[layer] = transition
def event_peek(self):
"""
This peeks the next event. It returns None if no event exists.
"""
if self.pushed_event:
return self.pushed_event
ev = pygame.event.poll()
if ev.type == NOEVENT:
return None
self.pushed_event = ev
return ev
def event_wait(self):
"""
This is in its own function so that we can track in the
profiler how much time is spent in interact.
"""
if self.pushed_event:
rv = self.pushed_event
self.pushed_event = None
return rv
# We load at most one image per wait.
if renpy.display.im.cache.needs_preload():
ev = pygame.event.poll()
if ev.type != NOEVENT:
return ev
renpy.display.im.cache.preload()
return pygame.event.wait()
@@ -671,7 +599,7 @@ class Interface(object):
root = renpy.display.layout.Fixed()
for layer in renpy.config.layers:
f = renpy.display.layout.Fixed(focus=layer)
f = renpy.display.layout.Fixed()
f.append_scene_list(scene_lists.layers[layer])
rv[layer] = f
@@ -688,23 +616,16 @@ class Interface(object):
keyword arguments are passed off to interact_core.
"""
# These things can be done once per interaction.
repeat = True
try:
repeat = True
while repeat:
repeat, rv = self.interact_core(**kwargs)
while repeat:
repeat, rv = self.interact_core(**kwargs)
return rv
finally:
# Clean out transient stuff at the end of an interaction.
scene_lists = renpy.game.context().scene_lists
scene_lists.replace_transient()
# 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,
@@ -742,14 +663,13 @@ 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()
## Expensive things we want to do before we pick the start_time.
# Tick time forward.
renpy.display.im.cache.tick()
renpy.display.image.cache.tick()
# Predict images.
renpy.game.context().predict(renpy.display.image.cache.preload_image)
# Set up key repeats.
# pygame.time.set_timer(KEYREPEATEVENT, renpy.config.skip_delay)
@@ -760,9 +680,12 @@ class Interface(object):
# Clear some events.
pygame.event.clear((MOUSEMOTION, DISPLAYTIME,
MOUSEBUTTONUP, MOUSEBUTTONDOWN))
# Figure out the scene list we want to show.
# Figure out the scene list we want to show.
start_time = time.time()
scene_lists = renpy.game.context().scene_lists
# Figure out what the overlay layer should look like.
for i in renpy.config.overlay_layers:
@@ -788,15 +711,7 @@ 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()
@@ -838,20 +753,13 @@ 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()))
# We only want to do prediction once, but we will defer it as
# long as possible.
did_prediction = False
pygame.event.post(pygame.event.Event(MOUSEMOTION,
pos=pygame.mouse.get_pos()))
rv = None
@@ -883,13 +791,13 @@ class Interface(object):
self.display.show(root_widget, suppress_blit)
renpy.config.frames += 1
# frames = frames + 1
# If profiling is enabled, report the profile time.
if renpy.config.profile:
new_time = time.time()
print "Profile: Redraw took %f seconds." % (new_time - draw_start)
print "Profile: %f seconds to complete event." % (new_time - self.profile_time)
print "Profile: %f seconds between event and display." % (new_time - self.profile_time)
# Draw the mouse, if it needs drawing.
if show_mouse:
@@ -900,23 +808,19 @@ class Interface(object):
# If we need to redraw again, do it if we don't have an
# event going on.
if needs_redraw and not self.event_peek():
if needs_redraw and not pygame.event.peek():
continue
# Predict images, if we haven't done so already.
if not did_prediction and not self.event_peek():
root_widget.predict(renpy.display.im.cache.preload_image)
renpy.game.context().predict(renpy.display.im.cache.preload_image)
did_prediction = True
# While we have nothing to do, preload images.
while renpy.display.image.cache.needs_preload() and \
not pygame.event.peek():
renpy.display.image.cache.preload()
try:
# ev = pygame.event.wait()
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]))
@@ -926,6 +830,12 @@ 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)
@@ -945,9 +855,8 @@ class Interface(object):
if len(evs):
ev = evs[-1]
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
@@ -1,355 +0,0 @@
# 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
@@ -1,576 +0,0 @@
# 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))
+396 -57
View File
@@ -1,14 +1,205 @@
# 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 *
Image = renpy.display.im.image
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))
return rv
im = pygame.image.load(renpy.loader.load(fn), fn)
if im.get_flags() & SRCALPHA:
im = im.convert_alpha()
else:
im = im.convert()
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)
class UncachedImage(renpy.display.core.Displayable):
"""
@@ -21,13 +212,10 @@ class UncachedImage(renpy.display.core.Displayable):
super(UncachedImage, self).__init__()
self.surf = pygame.image.load(file, hint)
self.surf = self.surf.convert_alpha()
if scale:
self.surf = pygame.transform.scale(self.surf, scale)
renpy.display.render.mutated_surface(self.surf)
self.style = renpy.style.Style(style, properties)
def get_placement(self):
@@ -42,7 +230,7 @@ class UncachedImage(renpy.display.core.Displayable):
# Should never be called, but what the hey?
def predict(self, callback):
return None
callback(self.filename)
class ImageReference(renpy.display.core.Displayable):
"""
@@ -70,7 +258,8 @@ 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)
@@ -100,6 +289,7 @@ class ImageReference(renpy.display.core.Displayable):
def render(self, width, height, st):
if not hasattr(self, 'target'):
self.find_target()
@@ -110,12 +300,6 @@ class ImageReference(renpy.display.core.Displayable):
self.find_target()
return self.target.get_placement()
def predict(self, callback):
if not hasattr(self, 'target'):
self.find_target()
self.target.predict(callback)
class Solid(renpy.display.core.Displayable):
@@ -127,8 +311,7 @@ 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__()
@@ -136,11 +319,10 @@ class Solid(renpy.display.core.Displayable):
def render(self, width, height, st):
si = renpy.display.im.SolidImage(self.color,
width,
height)
rv = renpy.display.render.Render(width, height)
rv.fill(self.color)
return render(si, width, height, st)
return rv
class Frame(renpy.display.core.Displayable):
"""
@@ -154,10 +336,11 @@ class Frame(renpy.display.core.Displayable):
the center of the image is scaled in both x and y directions.
"""
def __init__(self, image, xborder, yborder):
nosave = [ 'cache' ]
def __init__(self, filename, xborder, yborder):
"""
@param image: The image (which may be a filename or image
object) that will be scaled.
@param filename: The file that the original image will be read from.
@param xborder: The number of pixels in the x direction to use as
a border.
@@ -165,29 +348,108 @@ 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 share a dimension
For better performance, have the image file 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.image = Image(image)
self.filename = filename
self.xborder = xborder
self.yborder = yborder
def render(self, width, height, st):
fi = renpy.display.im.FrameImage(self.image,
self.xborder,
self.yborder,
width,
height)
return render(fi, width, height, st)
if hasattr(self, 'cache'):
if self.cache.get_size() == (width, height):
return self.cache
dest = renpy.display.render.Render(width, height)
dw, dh = width, height
def predict(self, callback):
self.image.predict(callback)
source = cache.load_image(self.filename)
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.
self.cache = dest
return dest
class Animation(renpy.display.core.Displayable):
"""
@@ -205,7 +467,7 @@ class Animation(renpy.display.core.Displayable):
animation will restart after the final delay time.
"""
super(Animation, self).__init__(style='image_placement')
super(Animation, self).__init__()
self.images = [ ]
self.delays = [ ]
@@ -213,7 +475,7 @@ class Animation(renpy.display.core.Displayable):
for i, arg in enumerate(args):
if i % 2 == 0:
self.images.append(Image(arg))
self.images.append(arg)
else:
self.delays.append(arg)
@@ -228,7 +490,7 @@ class Animation(renpy.display.core.Displayable):
if t < delay:
renpy.display.render.redraw(self, delay - t)
im = render(image, width, height, st)
im = cache.load_image(image)
width, height = im.get_size()
rv = renpy.display.render.Render(width, height)
rv.blit(im, (0, 0))
@@ -240,21 +502,102 @@ class Animation(renpy.display.core.Displayable):
def predict(self, callback):
for i in self.images:
i.predict(callback)
callback(i)
def get_placement(self):
return renpy.game.style.image_placement
class ImageButton(renpy.display.behavior.Button):
class ImageMap(renpy.display.core.Displayable):
"""
Used to implement the guts of an image button.
The displayable that implements renpy.imagemap.
"""
def __init__(self, idle_image, hover_image,
style='image_button',
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.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))
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):
def __init__(self, idle_image, hover_image, style='image_button',
image_style='image_button_image',
clicked=None, hovered=None, **properties):
clicked=None, hovered=None):
self.idle_image = Image(idle_image, style=image_style)
self.idle_image.style.set_prefix("idle_")
@@ -264,18 +607,14 @@ class ImageButton(renpy.display.behavior.Button):
super(ImageButton, self).__init__(self.idle_image,
style=style,
clicked=clicked,
hovered=hovered,
**properties)
def predict(self, callback):
self.idle_image.predict(callback)
self.hover_image.predict(callback)
hovered=hovered)
def focus(self, default=False):
self.child = self.hover_image
super(ImageButton, self).focus(default=default)
def unfocus(self):
self.child = self.idle_image
super(ImageButton, self).unfocus()
def set_hover(self, hover):
super(ImageButton, self).set_hover(hover)
if hover:
self.child = self.hover_image
else:
self.child = self.idle_image
+16 -24
View File
@@ -27,7 +27,9 @@ class Null(renpy.display.core.Displayable):
"""
def __init__(self, width=0, height=0, style='default', **properties):
super(Null, self).__init__(style=style, **properties)
super(Null, self).__init__()
self.style = renpy.style.Style(style, properties)
self.width = width
self.height = height
@@ -35,12 +37,7 @@ class Null(renpy.display.core.Displayable):
return self.style
def render(self, width, height, st):
rv = renpy.display.render.Render(self.width, self.height)
if self.focusable:
rv.add_focus(self, None, None, None, None, None)
return rv
return renpy.display.render.Render(self.width, self.height)
class Container(renpy.display.core.Displayable):
@@ -63,24 +60,16 @@ class Container(renpy.display.core.Displayable):
"""
def __init__(self, *args, **properties):
def __init__(self, *args):
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)
@@ -143,8 +132,6 @@ class Container(renpy.display.core.Displayable):
def predict(self, callback):
super(Container, self).predict(callback)
for i in self.children:
i.predict(callback)
@@ -163,7 +150,8 @@ class Fixed(Container):
"""
def __init__(self, style='default', **properties):
super(Fixed, self).__init__(style=style, **properties)
super(Fixed, self).__init__()
self.style = renpy.style.Style(style, properties)
self.times = [ ]
def add(self, widget, time=None):
@@ -226,7 +214,9 @@ class Position(Container):
child of this widget is placed.
"""
super(Position, self).__init__(style=style, **properties)
super(Position, self).__init__()
self.style = renpy.style.Style(style, properties)
self.add(child)
def render(self, width, height, st):
@@ -466,15 +456,17 @@ class Window(Container):
def __init__(self, child, style='window', **properties):
super(Window, self).__init__(style=style, **properties)
super(Window, self).__init__()
self.add(child)
self.style = renpy.style.Style(style, properties)
def get_placement(self):
return self.style
def render(self, width, height, st):
# save some typing.
# save typing and screen space.
style = self.style
xminimum = scale(style.xminimum, width)
+61 -202
View File
@@ -1,11 +1,6 @@
# Render lifespan.
# Rules for renders: Every widget creates its own Render, even if all
# it does is blit that render somewhere else.
# A render is alive when it is first created. It stays alive on subsequent
# styles if it is not killed and it is used. It can be killed either
# due to lack of use by the end of a cycle or because it was killed between
# cycles due to a timeout.
import sets
import time
import renpy
import pygame
@@ -28,14 +23,11 @@ class SolidCache(object):
self.color = color
if color[3] == 255:
surf = pygame.Surface(size, 0,
renpy.game.interface.display.window)
surf = pygame.Surface(size, 0, renpy.game.interface.display.window)
else:
surf = pygame.Surface(size, 0,
renpy.game.interface.display.sample_surface)
mutated_surface(surf)
surf.fill(color)
self.cached = surf
@@ -47,15 +39,18 @@ solid_cache = SolidCache()
## if any widget producing a render is redrawn, all instances of that
## render are killed, and so the entire thing is redrawn.
# Renders of a given widget.
widget_renders = { }
# Renders that have been used during the current rendering pass.
new_renders = { }
# Renders that were used on the old rendering pass.
old_renders = { }
# The set of surfaces that are mutated (that is, can change their
# The set of surfaces that are mutable (that is, can change their
# contents.)
mutated_surfaces = { }
mutable_surfaces = { }
def render(widget, width, height, st):
"""
@@ -65,20 +60,20 @@ def render(widget, width, height, st):
if (widget, width, height) in old_renders:
rv = old_renders[widget, width, height]
# assert (widget, width, height) in rv.render_of
# assert not rv.dead
assert widget in rv.widgets
rv.keep_alive()
rv.add_to_new_renders()
return rv
rv = widget.render(width, height, st)
rv.render_of.append((widget, width, height))
rv.req_width = width
rv.req_height = height
old_renders[widget, width, height] = rv
rv.widgets.append(widget)
new_renders[widget, width, height] = rv
widget_renders.setdefault(widget, []).append(rv)
return rv
@@ -94,7 +89,6 @@ def process_redraws():
redraw_queue.sort()
i = 0
dead_widgets = sets.Set()
now = time.time()
for when, widget in redraw_queue:
@@ -104,17 +98,14 @@ def process_redraws():
i += 1
dead_widgets.add(widget)
if widget in widget_renders:
for r in widget_renders[widget]:
r.remove_from_old_renders()
if not dead_widgets:
if i == 0:
return False
redraw_queue = redraw_queue[i:]
for (widget, width, height), render in old_renders.items():
if widget in dead_widgets:
render.kill()
return True
def redraw(widget, when):
@@ -127,36 +118,21 @@ def redraw(widget, when):
def render_screen(widget, width, height, st):
global widget_renders
global redraw_queue
global old_renders
global new_renders
global mutated_surfaces
global mutable_surfaces
mutated_surfaces = { }
new_renders = { }
widget_renders = { }
redraw_queue = [ ]
mutable_surfaces = { }
rv = render(widget, width, height, st)
# Renders that are in the old set but not the new one die here.
old_render_set = sets.Set(old_renders.itervalues())
new_render_set = sets.Set(new_renders.itervalues())
old_renders = new_renders
dead_render_set = old_render_set - new_render_set
for r in dead_render_set:
r.kill()
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 = [ ]
@@ -203,8 +179,8 @@ def compute_clip(source):
# If the two are the same.
if b0 == b1:
# Only add if the surface is mutated.
if b0[0] in mutated_surfaces:
# Only add if the surface is mutable.
if b0[0] in mutable_surfaces:
changes.append(b0)
i0 += 1
@@ -264,19 +240,17 @@ def screen_blit(source, full=False):
source.blit_to(screen, 0, 0)
screen.set_clip()
return cliprect
def mutated_surface(surf):
def mutable_surface(surf):
"""
Called to indicate that a pygame surface has been mutated. This also
should be called each time a new pygame surface is created.
Called to indicate that a pygame surface is mutable.
"""
mutated_surfaces[id(surf)] = True
mutable_surfaces[id(surf)] = True
class Render(object):
"""
@@ -296,23 +270,18 @@ class Render(object):
widget, then this is the widget it corresponds to.
"""
# Just for safety's sake.
self.dead = False
self.widgets = [ ]
# A list of widget, width, height, corresponding to the
# 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.req_width = width
self.req_height = height
self.parents = [ ]
self.blittables = [ ]
self.children = [ ]
self.depends = [ ]
# A pygame surface holding this Render, if one exists.
self.surface = None
@@ -320,98 +289,41 @@ 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 add_to_new_renders(self):
# def __del__(self):
# Render.renders -= 1
# print "Render del", Render.renders, Render.liverenders, self
def keep_alive(self):
# assert not self.dead
for widget, width, height in self.render_of:
new_renders[widget, width, height] = self
for w in self.widgets:
widget_renders.setdefault(w, []).append(self)
new_renders[w, self.req_width, self.req_height] = self
for i in self.children:
i.add_to_new_renders()
# assert self in i.parents
def remove_from_old_renders(self):
# assert not i.dead
for w in self.widgets:
# We need to check, since the same widget can appear twice.
if (w, self.req_width, self.req_height) in old_renders:
del old_renders[w, self.req_width, self.req_height]
i.keep_alive()
self.widgets = [ ]
def kill(self):
"""
Calling this marks the render and all of its parents dead. It also
unlinks it from the tree, readying it for reclamation.
"""
for p in self.parents:
p.remove_from_old_renders()
if self.dead:
return
self.parents = [ ]
self.dead = True
for widget, width, height in self.render_of:
del old_renders[widget, width, height]
if (widget, width, height) in new_renders:
del new_renders[widget, width, height]
parents = self.parents[:]
children = self.children[:]
depends = self.depends[:]
for p in parents:
p.kill()
for c in children:
while self in c.parents:
c.parents.remove(self)
for c in depends:
while self in c.parents:
c.parents.remove(self)
# assert not self.parents
# for p in parents:
# # assert p.dead
self.children = [ ]
self.depends = [ ]
# Removes cycles.
self.render_of = [ ]
self.focuses = [ ]
def blit(self, source, (xo, yo), focus=True):
def blit(self, source, (x, y)):
"""
Adds the source to the list of things that need to be blitted
to the screen. The source should be either a pygame.Surface,
or a Render.
"""
if isinstance(source, Render):
# assert not source.dead
source.parents.append(self)
self.children.append(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))
self.blittables.append((x, y, source))
def blit_to(self, dest, x, y):
@@ -480,11 +392,9 @@ class Render(object):
self.surface = rv
self.surface_alpha = alpha
mutated_surface(rv)
return rv
def subsurface(self, pos, focus=False):
def subsurface(self, pos):
"""
Returns a subsurface of this render.
"""
@@ -502,24 +412,6 @@ 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
@@ -544,25 +436,17 @@ class Render(object):
else:
oy = uly
sy = 0
if ox > width or oy > height:
continue
sw, sh = source.get_size()
sw = min(sw - sx, width - ox)
sh = min(sh - sy, height - oy)
sw = min(sw - sx, width)
sh = min(sh - sy, height)
if sw <= 0 or sh <= 0:
continue
subsurf = source.subsurface((sx, sy, sw, sh))
if isinstance(subsurf, pygame.Surface):
mutated_surface(subsurf)
rv.blit(subsurf, (ox, oy))
rv.blit(source.subsurface((sx, sy, sw, sh)),
(ox, oy))
self.subsurfaces[pos] = rv
@@ -570,36 +454,11 @@ class Render(object):
return rv
def depends_on(self, child):
def depends_on(self, render):
"""
Used to indicate that this render depends on another
render. Useful, for example, if we use pygame_surface to make
a surface, and then blit that surface into another render.
"""
# assert not child.dead
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))
render.parents.append(self)
+186
View File
@@ -0,0 +1,186 @@
# Function to allocate a surface.
import pygame
from pygame.constants import *
import renpy
# def Surface(width, height):
# """
# Allocate a surface. Flags and depth are ignored, for compatibility
# with pygame.Surface.
# """
# return pygame.Surface((width, height), 0,
# renpy.game.interface.display.sample_surface)
# class FilledSurface(object):
# def __init__(self, width, height, color):
# self.width = width
# self.height = height
# self.color = color
# def blit_to(self, dest, x, y):
# dest.set_alpha(0)
# dest.fill(self.color, [ x, y, self.width, self.height ])
# We only cache a single solid... but that should be enough to handle
# some important cases, like button and window backgrounds.
class SolidCache(object):
def __init__(self):
self.size = None
self.color = None
self.cached = None
def create(self, size, color):
if size == self.size and color == self.color:
return self.cached
self.size = size
self.color = color
surf = pygame.Surface(size, 0,
renpy.game.interface.display.sample_surface)
surf.fill(color)
self.cached = surf
return surf
solid_cache = SolidCache()
class Surface(object):
"""
This is our own surface object, which is a node in a tree in which
all of the leaves are PyGame surfaces. It ensures that things are
only blit to the screen once, hopefully giving a performance boost.
"""
def __init__(self, width, height):
self.width = width
self.height = height
self.blittables = [ ]
def blit(self, source, (x, y)):
"""
Adds the source surface to the list of things that need to be
blitted to the screen. The source surface is either a
pygame.Surface, or one of these Ren'Py Surfaces.
"""
self.blittables.append((x, y, source))
def blit_to(self, dest, x, y):
"""
This blits the children of this Surface to dest, which must be
a pygame.Surface. The x and y parameters are the location of
the upper-left hand corner of this surface, relative to the
destination surface.
"""
for xo, yo, source in self.blittables:
if isinstance(source, pygame.Surface):
dest.blit(source, (x + xo, y + yo))
else:
source.blit_to(dest, x + xo, y + yo)
def fill(self, color):
"""
Fake a pygame.Surface.fill()
"""
surf = solid_cache.create((self.width, self.height), color)
# surf = pygame.Surface((self.width, self.height), 0,
# renpy.game.interface.display.sample_surface)
# surf.fill(color)
# surf = FilledSurface(self.width, self.height, color)
self.blittables.append((0, 0, surf))
def get_size(self):
"""
Returns the size of this surface, a mostly ficticious value
that's taken from the inputs to the constructor. (As in, we
don't clip to this size.)
"""
return self.width, self.height
def pygame_surface(self, alpha=True):
"""
Returns a pygame surface constructed from self.
"""
if alpha:
sample = renpy.game.interface.display.sample_surface
else:
sample = renpy.game.interface.display.window
rv = pygame.Surface((self.width, self.height), 0, sample)
self.blit_to(rv, 0, 0)
return rv
def subsurface(self, (x, y, width, height)):
"""
Returns the subsurface of this surface, similar to
pygame.Surface.subsurface
"""
if x > self.width or y > self.height:
return Surface(0, 0)
width = min(self.width - x, width)
height = min(self.height - y, height)
rv = Surface(width, height)
for xo, yo, source in self.blittables:
# ulx, uly -- the coordinates of the upper-left hand corner of
# the image, relative to the subsurface.
ulx = xo - x
uly = yo - y
# ox, oy -- the offsets that the source will be blitted at.
# sx, sy -- the offset within the subsurface at which we begin.
if ulx < 0:
ox = 0
sx = -ulx
else:
ox = ulx
sx = 0
if uly < 0:
oy = 0
sy = -uly
else:
oy = uly
sy = 0
sw, sh = source.get_size()
if sw - ox <= 0:
continue
if sh - oy <= 0:
continue
sw = min(sw - sx - ox, width)
sh = min(sh - sy - oy, height)
rv.blit(source.subsurface((sx, sy, sw, sh)),
(ox, oy))
return rv
+105 -360
View File
@@ -1,114 +1,49 @@
import pygame
from pygame.constants import *
import re
import renpy
import sys
_font_cache = { }
# TODO: Something sane if the font file can't be found.
def get_font(fn, size, bold=False, italics=False, underline=False):
def get_font(fn, size):
from renpy.loader import transfn
if (fn, size, bold, italics, underline) in _font_cache:
return _font_cache[(fn, size, bold, italics, underline)]
if (fn, size) in _font_cache:
return _font_cache[(fn, size)]
try:
rv = pygame.font.Font(transfn(fn), size)
rv.set_bold(bold)
rv.set_italic(italics)
except:
rv = pygame.font.SysFont(fn, size, bold, italics)
rv = pygame.font.SysFont(fn, size)
rv.set_underline(underline)
_font_cache[(fn, size, bold, italics, underline)] = rv
_font_cache[(fn, size)] = rv
return rv
def color(s):
"""
This function converts a hexcode into a color/alpha tuple. Leading
# marks are ignored. Colors can be rgb or rgba, with each element having
either one or two digits. (So the strings can be 3, 4, 6, or 8 digits long,
not including the optional #.) A missing alpha is interpreted as 255,
fully opaque.
For example, color('#123a') returns (17, 34, 51, 170), while
color('c0c0c0') returns (192, 192, 192, 255).
"""
if s[0] == '#':
s = s[1:]
if len(s) == 6:
r = int(s[0]+s[1], 16)
g = int(s[2]+s[3], 16)
b = int(s[4]+s[5], 16)
a = 255
elif len(s) == 8:
r = int(s[0]+s[1], 16)
g = int(s[2]+s[3], 16)
b = int(s[4]+s[5], 16)
a = int(s[6]+s[7], 16)
elif len(s) == 3:
r = int(s[0], 16) * 0x11
g = int(s[1], 16) * 0x11
b = int(s[2], 16) * 0x11
a = 255
elif len(s) == 4:
r = int(s[0], 16) * 0x11
g = int(s[1], 16) * 0x11
b = int(s[2], 16) * 0x11
a = int(s[3], 16) * 0x11
else:
raise Exception("Argument to color() must be 3, 4, 6, or 8 hex digits long.")
return (r, g, b, a)
class TextStyle(object):
"""
This is used to represent the style of text that will be displayed
on the screen.
"""
def __init__(self, source=None):
if source is not None:
vars(self).update(vars(source))
def get_font(self):
return get_font(self.font, self.size, self.bold, self.italic, self.underline)
def get_ascent(self):
return self.get_font().get_ascent()
def sizes(self, text):
font = self.get_font()
# print font.get_ascent() - font.get_descent(), font.get_height(), font.get_linesize()
return font.size(text)[0], font.get_ascent() - font.get_descent()
def render(self, text, antialias, color, use_colors):
if use_colors and self.color:
color = self.color
font = self.get_font()
rv = font.render(text, antialias, color)
renpy.display.render.mutated_surface(rv)
return rv
class Text(renpy.display.core.Displayable):
"""
A displayable that can format and display text on the screen.
A Displayable that can display text on the screen.
"""
"""
@ivar style: The style that is used to display the text.
@ivar text: The text that is being displayed.
nosave = [ 'laidout', 'laidout_lineheights', 'laidout_width', 'laidout_height', 'width' ]
The following aren't serialized, but are reconstructed the first
time this is redrawn:
def after_setstate(self):
self.laidout = None
@ivar laidout: The text, split into a list of strings where each
string happens to be one line on the screen.
@ivar height: The height of the laid-out text.
@ivar width: The width of the laid-out text.
"""
def __init__(self, text, slow=False, style='default', **properties):
"""
@@ -121,14 +56,13 @@ class Text(renpy.display.core.Displayable):
@param properties: Additional properties that are applied to the text.
"""
super(Text, self).__init__()
self.text = text
self.style = renpy.style.Style(style, properties)
self.slow = slow
self.laidout = None
def get_placement(self):
return self.style
@@ -152,317 +86,130 @@ class Text(renpy.display.core.Displayable):
"""
This is called after this widget has been updated by
set_text or set_style.
"""
self.laidout = None
renpy.display.render.redraw(self, 0)
def event(self, ev, x, y):
"""
Space, Enter, or Click ends slow, if it's enabled.
"""
if not self.slow:
return None
if renpy.display.behavior.map_event(ev, "dismiss"):
self.slow = False
raise renpy.display.core.IgnoreEvent()
try:
del self.laidout
del self.width
del self.height
except AttributeError:
pass
def layout(self, width):
"""
This lays out the text of this widget. It sets self.laidout,
self.laidout_lineheights, self.laidout_width, and
self.laidout_height.
Called to split the text into a string with newline characters
at line endings where wrapping has occured.
"""
if self.laidout and self.width == width:
return
# Set this, so caching works.
self.width = width
def indent():
if lines:
return self.style.rest_indent
else:
return self.style.first_indent
tsl = [ TextStyle() ]
tsl[-1].font = self.style.font
tsl[-1].size = self.style.size
tsl[-1].bold = self.style.bold
tsl[-1].italic = self.style.italic
tsl[-1].underline = self.style.underline
tsl[-1].color = None
font = get_font(self.style.font, self.style.size)
lines = [ ]
line = [ ]
pars = self.text.split('\n')
# The height of the current line, in pixels, not including
# line_spacing.
lineheight = 0
lh = 0
# A list of same.
lineheights = [ ]
# The width of the current line.
linewidth = 0
# The maximum linewidth.
maxwidth = 0
# The current text.
cur = ""
# The indent of the current line.
indent = self.style.first_indent
# The width, in pixels, of cur.
curwidth = 0
for p in pars:
words = p.split(' ')
line = ""
# The remaining width of the line, not including the text in
# cur.
remwidth = width - indent()
for i in re.split(r'( |\{[^{]+\}|\{\{|\n)', self.text):
# Newline.
if i == "\n":
if cur:
line.append((TextStyle(tsl[-1]), cur))
maxwidth = max(maxwidth, linewidth + curwidth)
cur = ""
lines.append(line)
lineheights.append(lineheight)
line = [ ]
linewidth = 0
curwidth, lineheight = tsl[-1].sizes(" ")
remwidth = width - indent()
continue
elif i == "{{":
i = "{"
# We want to render this like a word, so no continue.
elif i.startswith("{"):
# Are we closing a tag?
if i.startswith("{/"):
if cur:
line.append((TextStyle(tsl[-1]), cur))
cur = ""
remwidth -= curwidth
linewidth += curwidth
curwidth = 0
tsl.pop()
if not tsl:
raise Exception("Closing tag %s does not match an open tag." % i)
for w in words:
# Each line must have at least one word on it.
if not line:
line = w
lw, lh = font.size(line)
maxwidth = max(maxwidth, lw)
continue
# Otherwise, we're opening a new tag.
# Mark up any text that uses an old style.
if cur:
line.append((TextStyle(tsl[-1]), cur))
cur = ""
remwidth -= curwidth
linewidth += curwidth
curwidth = 0
tsl.append(TextStyle(tsl[-1]))
if i == "{b}":
tsl[-1].bold = True
elif i == "{i}":
tsl[-1].italic = True
elif i == "{u}":
tsl[-1].underline = True
elif i == "{plain}":
tsl[-1].bold = False
tsl[-1].italic = False
tsl[-1].underline = False
elif i.startswith("{size"):
m = re.match(r'\{size=(\+|-|)(\d+)\}', i)
if not m:
raise Exception('Size tag %s could not be parsed.' % i)
if m.group(1) == '+':
tsl[-1].size += int(m.group(2))
elif m.group(1) == '-':
tsl[-1].size -= int(m.group(2))
else:
tsl[-1].size = int(m.group(2))
elif i.startswith("{color"):
m = re.match(r'\{color=(\#?[a-fA-F0-9]+)\}', i)
if not m:
raise Exception('Color tag %s could not be parsed.' % i)
tsl[-1].color = color(m.group(1))
lw, lh = font.size(line + " " + w)
if lw + indent < width:
line += " " + w
maxwidth = max(maxwidth, lw)
else:
raise Exception("Text tag %s was not recognized. Case and spacing matter here.")
lines.append(line)
line = w
indent = self.style.rest_indent
continue
elif i == ' ':
# Spaces always get appended to the end of a line. So they
# will never show up at the start of a line, unless they're
# after a newline or at the start of a string.
cur += i
curwidth, lh = tsl[-1].sizes(cur)
lineheight = max(lh, lineheight)
continue
# If we made it here, then we have normal text.
# We must have at least one word or something else in the
# line before we care about wrapping.
if not cur and not line:
cur = i
curwidth, lineheight = tsl[-1].sizes(cur)
continue
# Should we wrap?
curwidth, lh = tsl[-1].sizes(cur + i)
if curwidth > remwidth:
line.append((TextStyle(tsl[-1]), cur))
lines.append(line)
maxwidth = max(maxwidth, linewidth)
line = [ ]
lineheights.append(lineheight)
cur = i
curwidth, lineheight = tsl[-1].sizes(cur)
remwidth = width - indent()
linewidth = 0
else:
cur = cur + i
lineheight = max(lh, lineheight)
# We're done. Let's close up.
if len(tsl) != 1:
exception("A tag was left open at the end of the text.")
if cur:
line.append((tsl[-1], cur))
maxwidth = max(maxwidth, curwidth)
if line:
lines.append(line)
lineheights.append(lineheight)
self.laidout = lines
self.laidout_lineheights = lineheights
self.laidout_width = max(maxwidth, self.style.minwidth)
self.laidout_height = sum(lineheights) + len(lineheights) * self.style.line_spacing
self.laidout = "\n".join(lines)
self.height = len(lines) * (font.get_linesize() + self.style.line_height_fudge)
self.width = max(maxwidth, self.style.minwidth)
def render_pass(self, r, xo, yo, color, user_colors, length):
"""
Renders the text to r at xo, yo. Color is the base color,
and user_colors controls if the user can override those colors.
Returns True if all characters were rendered, or False if a
length restriction stopped some from being rendered.
"""
y = yo
indent = self.style.first_indent
rest_indent = self.style.rest_indent
antialias = self.style.antialias
line_spacing = self.style.line_spacing
for line, line_height in zip(self.laidout, self.laidout_lineheights):
x = xo + indent
indent = rest_indent
max_ascent = 0
for ts, text in line:
max_ascent = max(ts.get_ascent(), max_ascent)
for ts, text in line:
length -= len(text)
if length < 0:
text = text[:length]
surf = ts.render(text, antialias, color, user_colors)
sw, sh = surf.get_size()
r.blit(surf, (x, y + max_ascent - ts.get_ascent()))
x = x + sw
if length <= 0:
return False
y = y + line_height + line_spacing
return True
def render(self, width, height, st):
if self.slow and renpy.config.annoying_text_cps and not renpy.game.preferences.fast_text:
length = int(st * renpy.config.annoying_text_cps)
else:
length = sys.maxint
self.slow = False
if self.style.drop_shadow:
dsxo, dsyo = self.style.drop_shadow
else:
dsxo, dsyo = 0, 0
absxo = abs(dsxo)
absyo = abs(dsyo)
if not hasattr(self, "laidout"):
self.layout(width - dsxo)
width -= absxo
surf = renpy.display.render.Render(self.width + dsxo, self.height + dsyo)
font = get_font(self.style.font, self.style.size)
if dsxo < 0:
xo = -dsxo
dsxo = 0
laidout = self.laidout
# Annoying text hack.
if self.slow and renpy.config.annoying_text_cps and not renpy.game.preferences.fast_text:
chars = int(st * renpy.config.annoying_text_cps)
if chars < len(laidout):
laidout = laidout[:chars]
renpy.display.render.redraw(self, 0)
else:
xo = 0
self.slow = False
else:
self.slow = False
if dsyo < 0:
yo = -dsyo
dsyo = 0
else:
yo = 0
lines = laidout.split('\n')
first_indent = self.style.first_indent
rest_indent = self.style.rest_indent
# Common rendering code.
def render_lines(x, y, color):
y += self.style.text_y_fudge
indent = first_indent
for l in lines:
ls = font.render(l, self.style.antialias, color)
lw, lh = ls.get_size()
lw += indent
xo = int((self.width - lw) * self.style.textalign)
surf.blit(ls, (x + xo + indent, y + font.get_descent()))
y += font.get_linesize() + self.style.line_height_fudge
indent = rest_indent
self.layout(width - absxo)
rv = renpy.display.render.Render(self.laidout_width + absxo, self.laidout_height + absyo)
# Render drop-shadow.
if self.style.drop_shadow:
self.render_pass(rv, dsxo, dsyo, self.style.drop_shadow_color, False, length)
render_lines(dsxo, dsyo, self.style.drop_shadow_color)
self.slow = not self.render_pass(rv, xo, yo, self.style.color, True, length)
# Render foreground.
render_lines(0, 0, self.style.color)
if self.slow:
renpy.display.render.redraw(self, 0)
return rv
return surf
def event(self, ev, x, y):
"""
@@ -477,7 +224,6 @@ class Text(renpy.display.core.Displayable):
self.slow = False
raise renpy.display.core.IgnoreEvent()
class ParameterizedText(object):
"""
This can be used as an image. When used, this image is expected to
@@ -499,4 +245,3 @@ class ParameterizedText(object):
return Text(string, style=self.style, **self.properties)
+13 -48
View File
@@ -60,7 +60,6 @@ class Transition(renpy.display.core.Displayable):
"""
def __init__(self, delay):
super(Transition, self).__init__()
self.delay = delay
self.offsets = [ ]
self.events = True
@@ -71,9 +70,6 @@ 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
@@ -138,7 +134,7 @@ class Fade(Transition):
if widget:
surf = render(widget, width, height, st)
rv.blit(surf, (0, 0), focus=events)
rv.blit(surf, (0, 0))
self.events = events
@@ -234,58 +230,27 @@ class Dissolve(Transition):
self.new_widget = new_widget
self.events = False
self.old_bottom = None
self.old_top = None
self.old_alpha = 0
def render(self, width, height, st):
if st >= self.time:
self.events = True
return render(self.new_widget, width, height, st)
if st < self.time:
renpy.display.render.redraw(self, 0)
rv = render(self.old_widget, width, height, st)
alpha = min(255, int(255 * st / self.time))
rv = renpy.display.render.Render(width, height)
bottom = render(self.old_widget, width, height, st)
top = render(self.new_widget, width, height, st)
surf = top.pygame_surface(False)
renpy.display.render.mutated_surface(surf)
renpy.display.render.mutable_surface(surf)
rv.focuses.extend(top.focuses)
if id(top) == self.old_top and id(bottom) == self.old_bottom:
# Fast rendering path.
alpha = alpha / 255.0
change = ( alpha - self.old_alpha) / ( 1.0 - self.old_alpha)
change = int(change * 255.0)
surf.set_alpha(change, RLEACCEL)
rv.blit(surf, (0, 0))
change /= 255.0
self.old_alpha = self.old_alpha * ( 1 - change ) + change
else:
# Complete rendering path.
rv.blit(bottom, (0, 0), focus=False)
surf.set_alpha(alpha, RLEACCEL)
rv.blit(surf, (0, 0))
self.old_alpha = alpha / 255.0
alpha = min(255, int(255 * st / self.time))
self.old_top = id(top)
self.old_bottom = id(bottom)
surf.set_alpha(alpha, RLEACCEL)
rv.blit(surf, (0, 0))
if st < self.time:
renpy.display.render.redraw(self, 0)
return rv
@@ -358,7 +323,7 @@ class CropMove(Transition):
image. Otherwise, the top layer contains the old image.
"""
super(CropMove, self).__init__(time)
self.time = time
if mode == "wiperight":
@@ -509,11 +474,11 @@ class CropMove(Transition):
rv = renpy.display.render.Render(width, height)
rv.blit(render(self.bottom, width, height, st), (0, 0), focus=not self.topnew)
rv.blit(render(self.bottom, width, height, st), (0, 0))
top = render(self.top, width, height, st)
ss = top.subsurface(crop, focus=self.topnew)
rv.blit(ss, pos, focus=self.topnew)
ss = top.subsurface(crop)
rv.blit(ss, pos)
renpy.display.render.redraw(self, 0)
return rv
+2 -1
View File
@@ -160,8 +160,9 @@ 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)
renpy.display.render.mutable_surface(surface)
w, h = surface.get_size()
rv = renpy.display.render.Render(w, h)
+1 -7
View File
@@ -22,8 +22,6 @@ 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):
@@ -32,15 +30,12 @@ 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):
@@ -120,7 +115,6 @@ 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
+12 -61
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 ParameterizedText
from renpy.display.behavior import Keymap
# from renpy.display.image import *
from renpy.display.layout import *
from renpy.display.text import *
from renpy.display.behavior import *
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 load, save, saved_games
from renpy.loadsave import *
from renpy.python import py_eval as eval
from renpy.python import rng as random
@@ -112,7 +112,7 @@ def watch(expression, style='default', **properties):
renpy.config.overlay_functions.append(overlay_func)
def input(prompt, default='', allow=None, exclude='{}', length=None):
def input(prompt, default='', length=None):
"""
This pops up a window requesting that the user enter in some text.
It returns the entered text.
@@ -123,19 +123,13 @@ def input(prompt, default='', allow=None, exclude='{}', length=None):
@param length: If given, a limit to the amount of text that this
function will return.
@param allow: If not None, then if an input character is not in this
string, it is ignored.
@param exclude: If not None, then if an input character is in this
set, it is ignored.
"""
renpy.ui.window(style='input_window')
renpy.ui.vbox()
renpy.ui.text(prompt, style='input_prompt')
renpy.ui.input(default, length=length, style='input_text', allow=allow, exclude=exclude)
renpy.ui.input(default, length=length, style='input_text')
renpy.ui.close()
@@ -200,22 +194,6 @@ def display_menu(items, window_style='menu_window'):
return rv
class TagQuotingDict(object):
def __getitem__(self, key):
store = vars(renpy.store)
if key in store:
rv = store[key]
if isinstance(rv, (str, unicode)):
rv = rv.replace("{", "{{")
return rv
else:
raise Exception("During an interpolation, '%s' was not found as a variable." % key)
tag_quoting_dict = TagQuotingDict()
def say(who, what):
"""
@@ -225,13 +203,13 @@ def say(who, what):
"""
# Interpolate variables.
what = what % tag_quoting_dict
what = what % renpy.game.store
if who is None:
who = renpy.store.narrator
if isinstance(who, (str, unicode)):
renpy.store.say(who, what)
display_say(who, what, what_style='say_dialogue')
else:
who(what)
@@ -313,6 +291,8 @@ 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
@@ -478,14 +458,6 @@ def windows():
import sys
return hasattr(sys, 'winver')
def version():
"""
Returns a string containing the current version of Ren'Py, prefixed with the
string "Ren\'Py ".
"""
return renpy.version
def transition(trans, layer=None):
"""
Sets the transition that will be used for the next
@@ -525,19 +497,6 @@ 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
@@ -563,14 +522,6 @@ 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,6 +21,11 @@ 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.
+1 -1
View File
@@ -1,6 +1,6 @@
import renpy
import os.path
from pickle import loads
from cPickle import loads
from cStringIO import StringIO
archives = [ ]
+10 -23
View File
@@ -1,6 +1,6 @@
# This file contains functions that load and save the game state.
from pickle import dumps, loads, HIGHEST_PROTOCOL
from cPickle import dumps, loads, HIGHEST_PROTOCOL
import cStringIO
import zipfile
import time
@@ -15,12 +15,8 @@ savegame_suffix = renpy.savegame_suffix
def debug_dump(prefix, o, seen):
if isinstance(o, (int, str, float, bool)):
print prefix, o
return
if id(o) in seen:
print prefix, "@%x" % id(o)
print prefix, id(o)
return
seen[id(o)] = True
@@ -135,25 +131,16 @@ def saved_games():
for f in files:
try:
zf = zipfile.ZipFile(renpy.config.savedir + "/" + f, "r")
extra_info = zf.read("extra_info")
sio = cStringIO.StringIO(zf.read("screenshot.tga"))
zf.close()
screenshot = renpy.display.image.UncachedImage(sio, "screenshot.tga", False)
zf = zipfile.ZipFile(renpy.config.savedir + "/" + f, "r")
extra_info = zf.read("extra_info")
sio = cStringIO.StringIO(zf.read("screenshot.tga"))
zf.close()
f = f[:-len(savegame_suffix)]
screenshot = renpy.display.image.UncachedImage(sio, "screenshot.tga", False)
f = f[:-len(savegame_suffix)]
saveinfo[f] = screenshot, extra_info
except:
if renpy.config.debug:
raise Exception
if newest not in saveinfo:
newest = None
saveinfo[f] = screenshot, extra_info
return saveinfo, newest
+4 -9
View File
@@ -12,7 +12,7 @@
import renpy
import renpy.game as game
import os
from pickle import loads, dumps, HIGHEST_PROTOCOL
from cPickle import loads, dumps, HIGHEST_PROTOCOL
def run(restart=False):
"""
@@ -54,11 +54,6 @@ 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 = { }
@@ -71,11 +66,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
@@ -93,14 +88,14 @@ def run(restart=False):
renpy.game.exception_info = 'After initialization, but before game start.'
# Rebuild the various style caches.
renpy.style.build_styles()
game.style._build_style_caches()
# 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 = vars(renpy.store).copy()
game.clean_store = game.store.copy()
# Re-Initialize the log.
game.log = renpy.python.RollbackLog()
+228
View File
@@ -0,0 +1,228 @@
# This module contains code that handles the playing of music.
import pygame
import renpy
import sys # to detect windows.
# The Windows Volume Management Strategy (tm).
# We keep a master music volume, which is the volume we use directly
# when playing music as mp3, ogg, etc. When we start up, we compute
# a midi music scaling factor. This midi music scaling factor is
# computed from the current master music volume such that when we are not
# fading, if we pygame.mixer.music.set_volume() to the mmv * mmsv, we get
# read the same value from midiOutGetVolume() as we did before we tried
# doing that.
playing_midi = True
fading = False
master_music_volume = 1.0
windows_magic = False
if hasattr(sys, 'winver'):
midi_msf = 0.0
last_raw_volume = -1
def read_raw_volume():
res = c_uint()
for i in range(0, winmm.midiOutGetNumDevs()):
rv = winmm.midiOutGetVolume(i, byref(res))
if not rv:
return res.value
else:
print "Couldn't read raw midi volume."
return -1
try:
from ctypes import windll, c_uint, byref
winmm = windll.winmm
def compute_midi_msf():
"""
Computes the Midi MSF. Returns True if successful, False if otherwise.
"""
# Don't update the MSF when fading is going on, or when not
# playing a midi. (Except before playing any music whatsoever.)
if fading or not playing_midi:
return False
global last_raw_volume
raw_vol = read_raw_volume()
if raw_vol < 0:
return False
# The case in which the volume hasn't changed recently.
if raw_vol == last_raw_volume:
return True
last_raw_volume = raw_vol
# print "raw_vol", raw_vol
# The fraction that the midi mixer is at.
mixfrac = 1.0 * ( raw_vol & 0xffff ) / 0xffff
global midi_msf
midi_msf = mixfrac / master_music_volume
# print "Midi msf is now:", midi_msf
return True
# This should get called after the music starts playing.
def set_music_volume(vol):
global master_music_volume
master_music_volume = vol
if playing_midi:
vol *= midi_msf
if vol > 1.0:
vol = 1.0
pygame.mixer.music.set_volume(vol)
global last_raw_volume
last_raw_volume = read_raw_volume()
# Figure out the default msf, and set it up.
windows_magic = compute_midi_msf()
playing_midi = False
except Exception, e:
print "Exception when trying to init music:", str(e)
print "Falling back to Unix mode."
if not windows_magic:
def compute_midi_msf():
return
def set_music_volume(vol):
global master_music_volume
master_music_volume = vol
pygame.mixer.music.set_volume(vol)
playing_midi = False
# 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")
# 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.
"""
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.
"""
music_stop()
renpy.game.context().scene_lists.music = (filename, loops, startpos)
restore()
def music_stop():
"""
Stops the currently playing music track.
"""
renpy.game.context().scene_lists.music = None
restore()
def restore():
"""
This makes sure that the current music matches the music found in
the context.
"""
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
# 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)
+7 -13
View File
@@ -5,7 +5,6 @@ import codecs
import re
import os
import renpy
import renpy.ast as ast
class ParseError(Exception):
@@ -143,7 +142,7 @@ def list_logical_lines(filename):
if line != "":
raise ParseError(filename, start_number, "is not terminated with a newline (check quotes and parenthesis).")
raise ParseError(filename, number, "is not terminated with a newline.")
return rv
@@ -172,11 +171,13 @@ def group_logical_lines(lines):
if l[index] == '\t':
index += 1
depth = depth + 8 - (depth % 8)
depth = depth + 8 - (16 % 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
@@ -465,17 +466,12 @@ class Lexer(object):
if c == 'u':
self.pos += 1
if self.pos == len(self.text):
self.pos -= 1
if self.eol():
return False
c = self.text[self.pos]
if c not in ('"', "'"):
self.pos -= 1
return False
elif c not in ('"', "'"):
if c not in ('"', "'"):
return False
delim = c
@@ -517,7 +513,7 @@ class Lexer(object):
while self.match(r'\.'):
n = self.name()
if not n:
self.error('expecting name.')
self.parse_error('expecting name.')
rv += "." + n
@@ -1188,8 +1184,6 @@ def parse(fn):
statements that were found at the top level of the file.
"""
renpy.game.exception_info = 'While parsing ' + fn + '.'
version = os.stat(fn).st_mtime
lines = list_logical_lines(fn)
nested = group_logical_lines(lines)
+19 -28
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
or from the context info objects as reachable.
as reachable.
@param store: A map from variable name to variable value.
@param reachable: A dictionary mapping reached object ids to
@@ -72,9 +72,7 @@ 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.
@@ -358,10 +356,10 @@ class Rollback(renpy.object.Object):
for t in self.store:
if len(t) == 2:
k, v = t
vars(renpy.store)[k] = v
renpy.game.store[k] = v
else:
k, = t
del vars(renpy.store)[k]
del renpy.game.store[k]
renpy.game.contexts = [ self.context ]
rng.pushback(self.random)
@@ -430,7 +428,7 @@ class RollbackLog(renpy.object.Object):
self.log.append(self.current)
self.mutated = { }
self.old_store = vars(renpy.store).copy()
self.old_store = renpy.game.store.copy()
def complete(self):
"""
@@ -441,7 +439,7 @@ class RollbackLog(renpy.object.Object):
occurs.
"""
new_store = vars(renpy.store)
new_store = renpy.game.store
store = [ ]
@@ -486,11 +484,9 @@ class RollbackLog(renpy.object.Object):
rv = { }
store = vars(renpy.store)
for k in self.ever_been_changed.keys():
if k in store:
rv[k] = store[k]
if k in renpy.game.store:
rv[k] = renpy.game.store[k]
return rv
@@ -621,15 +617,14 @@ class RollbackLog(renpy.object.Object):
renpy.game.log = self
# Restore the store.
store = vars(renpy.store)
store.clear()
store.update(renpy.game.clean_store)
renpy.game.store.clear()
renpy.game.store.update(renpy.game.clean_store)
for k in self.ever_been_changed:
if k in store:
del store[k]
if k in renpy.game.store:
del renpy.game.store[k]
store.update(self.frozen_roots)
renpy.game.store.update(self.frozen_roots)
self.frozen_roots = None
# Now, rollback to an acceptable point.
@@ -639,35 +634,31 @@ class RollbackLog(renpy.object.Object):
def py_exec_bytecode(bytecode, hide=False):
store = vars(renpy.store)
if hide:
locals = { }
else:
locals = store
locals = renpy.game.store
exec marshal.loads(bytecode) in store, locals
exec marshal.loads(bytecode) in renpy.game.store, locals
def py_exec(source, hide=False):
store = vars(renpy.store)
if hide:
locals = { }
else:
locals = store
locals = renpy.game.store
exec py_compile(source, 'exec') in store, locals
exec py_compile(source, 'exec') in renpy.game.store, locals
def py_eval_bytecode(bytecode):
return eval(marshal.loads(bytecode), vars(renpy.store))
return eval(marshal.loads(bytecode), renpy.game.store)
def py_eval(source):
source = source.strip()
return eval(py_compile(source, 'eval'),
vars(renpy.store))
renpy.game.store)
+1 -1
View File
@@ -6,7 +6,7 @@ import renpy
import os.path
import os
from pickle import loads, dumps
from cPickle import loads, dumps
# The version of the dumped script.
script_version = renpy.script_version
+53
View File
@@ -0,0 +1,53 @@
# Plays sounds.
import renpy
import pygame
from pygame.constants import *
def init():
try:
bufsize = 4096
import os
if 'RENPY_SOUND_BUFSIZE' in os.environ:
bufsize = int(os.environ('RENPY_SOUND_BUFSIZE'))
pygame.mixer.pre_init(renpy.config.sound_sample_rate, -16, 2, bufsize)
except:
try:
pygame.mixer.pre_init()
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 (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 fn:
return
if not renpy.game.preferences.sound:
return
try:
sound = pygame.mixer.Sound(renpy.loader.load(fn))
sound.play()
except:
if renpy.config.debug_sound:
raise
+39 -10
View File
@@ -6,8 +6,6 @@
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__
@@ -150,22 +148,53 @@ class DynamicCharacter(object):
interact=interact,
**self.properties)
# The color function. (Moved, since text needs it, too.)
color = renpy.display.text.color
# Conveniently get rid of all the packages we had imported before.
import renpy.exports as renpy
# The default narrator.
def narrator(what, interact=True):
renpy.display_say(None, what, what_style='say_thought', interact=interact)
# The default menu function.
menu = renpy.display_menu
# The function that is called when anonymous text is said.
def say(who, what):
renpy.display_say(who, what)
def color(s):
"""
This function converts a hexcode into a color/alpha tuple. Leading
# marks are ignored. Colors can be rgb or rgba, with each element having
either one or two digits. (So the strings can be 3, 4, 6, or 8 digits long,
not including the optional #.) A missing alpha is interpreted as 255,
fully opaque.
For example, color('#123a') returns (17, 34, 51, 170), while
color('c0c0c0') returns (192, 192, 192, 255).
"""
if s[0] == '#':
s = s[1:]
if len(s) == 6:
r = int(s[0]+s[1], 16)
g = int(s[2]+s[3], 16)
b = int(s[4]+s[5], 16)
a = 255
elif len(s) == 8:
r = int(s[0]+s[1], 16)
g = int(s[2]+s[3], 16)
b = int(s[4]+s[5], 16)
a = int(s[6]+s[7], 16)
elif len(s) == 3:
r = int(s[0], 16) * 0x11
g = int(s[1], 16) * 0x11
b = int(s[2], 16) * 0x11
a = 255
elif len(s) == 4:
r = int(s[0], 16) * 0x11
g = int(s[1], 16) * 0x11
b = int(s[2], 16) * 0x11
a = int(s[3], 16) * 0x11
else:
raise Exception("Argument to color() must be 3, 4, 6, or 8 hex digits long.")
return (r, g, b, a)
# The default transition.
default_transition = None
+121 -155
View File
@@ -1,192 +1,158 @@
import renpy
# A list of style prefixes we care about, including no prefix.
prefixes = [ 'hover_', 'idle_', 'activate_' , 'insensitive_' ]
def startswith_prefix(s):
for i in prefixes:
if s.startswith(i):
return True
else:
return False
prefixes = [ 'hover_', 'idle_', 'activate_', '' ]
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
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.
"""
def __getattr__(self, name):
try:
return style_map[name]
except:
raise Exception('The style %s does not exist.' % name)
def __init__(self):
self._style_list = [ ]
def create(self, name, parent, description=None):
style_map[name] = Style(parent, { })
def create(self, name, parent='default', description=''):
"""
Creates a new style with the given parent and description, and
adds it to the StyleManager.
"""
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)
if parent and not hasattr(self, parent):
raise Exception("Style '%s' has non-existent parent '%s'." % (name, parent))
style.cache.update(parent.cache)
s = Style(parent, defer=True)
s.name = name
s.parent = parent
s.description = description
style.cache.update(style.properties)
setattr(self, name, s)
# This builds all pending styles, recursing to ensure that they are built
# in the right order.
def build_styles():
self._style_list.append(s)
global styles_pending
global styles_built
def _build_style_caches(self):
for s in styles_pending:
build_style(s, True)
for i in self._style_list:
i.build_cache()
styles_pending = None
styles_built = 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()
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(prefix = self.prefix,
parent = self.parent,
cache = { },
properties = self.properties)
return dict(properties=self.properties,
prefix=self.prefix,
parent=self.parent)
def __setstate__(self, state):
vars(self).update(state)
build_style(self)
self.__dict__.update(state)
def __init__(self, parent, properties):
# This should always work, as only one layer of these styles will
# be serialized.
fields = dict(
prefix = 'insensitive_',
parent = parent,
cache = { },
properties = { },
)
self.build_cache()
vars(self).update(fields)
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)
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"] = { }
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()
if not defer:
self.build_cache()
+12 -56
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 widgets are added to this grid unil ui.close() is called.
grid. New widges are added to this vbox 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,6 +194,8 @@ 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
@@ -242,12 +244,9 @@ 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
return add(renpy.display.behavior.KeymouseBehavior())
def saybehavior():
"""
@@ -281,12 +280,7 @@ def pausebehavior(delay, result=False):
return add(renpy.display.behavior.PauseBehavior(delay, result))
def menu(menuitems,
style = 'menu',
caption_style='menu_caption',
choice_style='menu_choice',
choice_button_style='menu_choice_button',
**properties):
def menu(menuitems, **properties):
"""
This creates a new menu widget. Unlike the menu statement or
renpy.menu function, this menu widget is not enclosed in any sort
@@ -299,24 +293,9 @@ def menu(menuitems,
if this item is a non-selectable caption.
"""
# menu is now a conglomeration of other widgets. And bully for it.
return add(renpy.display.behavior.Menu(menuitems, **properties))
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):
def input(default, length=None, **properties):
"""
This creats a new input widget. This widget accepts textual input
until the user hits enter, and then returns that text.
@@ -325,15 +304,9 @@ def input(default, length=None, allow=None, exclude='{}', **properties):
@param length: If set, the maximum number of characters that will be
returned by this input.
@param allow: If not None, then if an input character is not in this
string, it is ignored.
@param exclude: If not None, then if an input character is in this
set, it is ignored.
"""
return add(renpy.display.behavior.Input(default, length=length, allow=allow, exclude=exclude, **properties))
return add(renpy.display.behavior.Input(default, length=length, **properties))
def image(filename, **properties):
"""
@@ -344,33 +317,16 @@ 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 called to create imagemaps. Parameters are
This is the widget that implements imagemaps. Parameters are
roughtly the same as renpy.imagemap. The value of the hotspot is
returned when ui.interact() returns.
"""
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
return add(renpy.display.image.ImageMap(ground, selected,
hotspots, unselected,
**properties))
def button(clicked=None, **properties):
+9 -53
View File
@@ -2,8 +2,7 @@
import os.path
# Enable psyco. Warning: Check for memory leaks!
# Go psyco! (Compile where we can.)
try:
if not os.path.exists("nopsyco"):
import psyco
@@ -21,21 +20,18 @@ import sys
# Extra things used for distribution.
import encodings.utf_8
import encodings.zlib_codec
import encodings.unicode_escape
import encodings.string_escape
import encodings.raw_unicode_escape
# Load up all of Ren'Py, in the right order.
import renpy
def main():
if __name__ == "__main__":
# Stdout should be a utf-8 stream, so print works nicely.
# utf8writer = codecs.getwriter("utf-8")
# sys.stdout = utf8writer(sys.stdout)
name = os.path.basename(sys.argv[0])
dirname = os.path.dirname(sys.argv[0])
if dirname:
os.chdir(dirname)
if name.find(".") != -1:
name = name[:name.find(".")]
@@ -55,9 +51,6 @@ def main():
op.add_option('--python', dest='python', default=None,
help='Run the argument in the python interpreter.')
op.add_option('--leak', dest='leak', action='store_true', default=False,
help='When the game exits, dumps a profile of memory usage.')
options, args = op.parse_args()
if options.python:
@@ -113,45 +106,8 @@ def main():
os.startfile('traceback.txt')
except:
pass
if options.leak:
memory_profile()
sys.exit(0)
def memory_profile():
print "Memory Profile"
print
print "Showing all objects in memory at program termination."
print
import gc
gc.collect()
objs = gc.get_objects()
c = { } # count
dead_renders = 0
for i in objs:
t = type(i)
c[t] = c.get(t, 0) + 1
if isinstance(i, renpy.display.render.Render):
if i.dead:
dead_renders += 1
results = [ (count, ty) for ty, count in c.iteritems() ]
results.sort()
for count, ty in results:
print count, str(ty)
if dead_renders:
print
print "*** found", dead_renders, "dead Renders. ***"
if __name__ == "__main__":
main()
-1
View File
@@ -1 +0,0 @@
run_game.py
+1 -1
View File
@@ -8,7 +8,7 @@ def got_node(n):
if isinstance(n, renpy.ast.Say):
print n.what
renpy.config.searchpath = [ 'game' ]
renpy.config.searchpath = [ 'moonlight' ]
renpy.script.Script(got_node)
+6 -10
View File
@@ -1,6 +1,6 @@
init:
$ config.window_title = "Test Game"
$ config.profile = False
$ config.profile = True
$ config.debug_sound = True
@@ -22,11 +22,11 @@ init:
# Styles.
$ style.window.background = Frame("frame.png", 120, 25)
$ style.menu_choice_button.hover_sound = "chev1.wav"
$ style.menu_choice_button.activate_sound = "chev2.wav"
$ style.menu.hover_sound = "chev1.wav"
$ style.menu.activate_sound = "chev2.wav"
$ style.imagemap_button.hover_sound = "chev3.wav"
$ style.imagemap_button.activate_sound = "chev4.wav"
$ style.imagemap.hover_sound = "chev3.wav"
$ style.imagemap.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", 60.0)
"9a_concerned.png")
image movie = Movie()
@@ -82,10 +82,6 @@ label main_menu:
# Stuff that happens before the real main menu.
scene black
show eileen anim
$ renpy.pause()
show text "American Bishoujo\nPresents" \