Compare commits

...

141 Commits

Author SHA1 Message Date
tom 5eb0185d03 Tagging 5.3.0.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/tags/renpy-5.3.0@341 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-12-14 01:43:26 +00:00
tom df4154c854 A new variable, config.auto_choice_delay, has been added. If not None,
this variable gives a number of seconds that Ren'Py will pause at an
in-game menu before picking a random choice from that menu. We'd
expect this variable to always be set to None in released games, but
setting it to a number will allow for automated demonstrations of
games without much human interaction.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@270 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-11-04 15:07:06 +00:00
tom cbe66f8353 There is now a new function, LiveComposite, that can composite
together arbitrary displayables, rather than just images. It has the
same API as im.Composite, but should be used when one of the things
you wish to composite is an animation.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@269 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-11-03 03:47:59 +00:00
tom 143db03196 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@268 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-11-02 17:10:54 +00:00
tom e879687a16 Character objects now can take properties prefixed with what_ or
window_. Properties prefixed with what_ are applied to the text being
spoken, while those prefixed with window_ are applied to the window
containing everything. Unprefixed properties are still applied to the
label giving the name of the character speaking.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@267 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-11-01 01:20:57 +00:00
tom fcb3286df9 Implemented a new text tag, {fast}. This tag causes the slow text
display to start from a given point in the text string. This allows
one to show some text, run some command, and show the same text with
additions, and have the slow text display occur correctly.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@266 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-30 01:29:08 +00:00
tom ba23df21ab Fixed Motion (the class that implements Move and Pan) to allow motions
to be used as transitions, in conjunction with the say and menu
statements. Previously, they only worked with the with statement.




git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@265 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-29 21:33:08 +00:00
tom 5f6f3d8e70 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@261 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-10-26 03:08:45 +00:00
tom 9d7cd97fcc Added a new extra, quick_save.rpy, that adds a quick save feature to
Ren'Py. This feature adds a single save slot that can be quickly
accessed without going through the game menu. It also adds a quick
load button to the main menu, which causes a load from this slot. The
button is disabled if no quick save game exists.

To support this, changed the format of library.main_menu. Now, instead
of being a list of pairs, library.main_menu is a list of triples. The
new third component is an expression, which must evaluate to true for
the menu button to be enabled.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@260 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-26 02:26:53 +00:00
tom a90491b678 Adding raw strings.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@258 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-19 21:03:53 +00:00
tom ae8fc4b859 Adding raw strings.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@257 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-19 21:03:29 +00:00
tom c4605e915d Fixed regression in lint caused by imspec change.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@255 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-19 14:22:06 +00:00
tom e809b24663 Fixed regression affecting nested ctcs.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@251 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-15 02:46:37 +00:00
tom ebf669e8af Fixed changelog.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@250 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-15 00:32:54 +00:00
tom 3b7b40b85a Increased the default rollback length to back 100, now that we have a
readback extra people can use if they want to. Updated readback so it
works a little better.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@249 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-15 00:32:39 +00:00
tom 4225f92095 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@248 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-10-13 04:14:34 +00:00
tom 31241ca637 Implemented two new text tags, {w} and {p}. {w} pauses the display of
dialogue, waiting for the user to click to continue. {p} does the same
thing, but also breaks the line where it occurs.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@246 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-13 03:17:39 +00:00
tom 6e06e155dd Added a framerate limiter. When enabled by setting config.framerate,
Ren'Py limits the number of frames that are displayed per second to
the supplied values. This tends to smooth out the framerate, and hence
the percieved smoothness of the program. It will lower the overall
framerate, but make it more regular.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@245 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-10-09 04:47:12 +00:00
tom c7059bc3cb The scene, show, and hide statements now take an onlayer clause, which
gives the layer that the operations occur on. So if we declare high to
be another layer (by setting config.layers to something like 
[ 'master', 'high', 'transient', 'overlay' ]), then one can use a
statement like "show eileen happy onlayer high" to show the given
image on the high layer. This image will not be removed when scene
statements clear other layers, such as the default master layer.

To support the above, the parameters to renpy.show() changed in an
incompatible way. If your code uses renpy.show(), it will need to be
upgraded. Instead of considering the second and later parameters to be
at expressions, it now takes as its second parameter a list of all at
expressions. (This lets us use the third parameter to accept the layer
name.) 

The label 'after_load' is now called after a game is loaded, if it
exists on disk. This label can be used to upgrade save files, restore
music, and the like.

It's now possible to pick the screen the game menu enters when it is
invoked, by setting the _game_menu_screen variable.

Simplified the demo game script by moving the style stuff out of that
first init block, and down to the bottom of the script where it's out
of the way of a first viewing. Hopefully, this will reduce the shock
of someone first encountering Ren'Py.

Documented how to modify and replace the game and main menus, and how
to add in one's own preferences.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@242 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-09-24 23:45:20 +00:00
tom f6c4f7fda1 The label 'after_load' is now called after a game is loaded, if it
exists on disk. This label can be used to upgrade save files, restore
music, and the like. 



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@241 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-09-16 04:04:19 +00:00
tom a6f04a6741 5.1.2
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@238 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-09-15 03:27:21 +00:00
tom 12e4028979 Ready for build...
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@237 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-09-15 02:29:57 +00:00
tom f06e4ab954 Custom mouse support has been totaly rewritten. It now supports
animated mice, and changing the mouse cursor based on the kind of
interaction that is ongoing. Rather than describe the changes here,
read the documentation of config.mouse to understand the new
behavior. Please note that config.mouse has changed, and so if you use
it in your game (and AFAIK noone does), it will need to be modified.

Custom mouse support has also had significant performance
improvements.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@236 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-09-14 16:51:07 +00:00
tom be98f91907 Mouse perf improvements.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@235 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-09-14 04:33:08 +00:00
tom ada05d2eb2 A new widget, anim.Blink, blinks a child widget in and out of
existence by varying its alpha channel. [doc]

Character now allows for the use of a click-to-continue widget when
the widget finishes showing text to the user. While many widgets can
be used, this feature is especially useful with Images, Animations,
and anim.Blink. The click-to-continue widget can be nestled in with
the end of the text, or placed at a fixed location on the screen.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@234 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-09-13 01:57:49 +00:00
tom 1d355ceb0b Ren'Py now supports embedding images inside text. Please read the
documentation of ui.text for some important caveats about how this
works, but basically, the input of ui.text can be a string or a list
containing strings and widgets. In the latter case, the widgets are
rendered inside the text. One can also use a text tag like
{image=filename.png} to include an image anywhere a text tag
works. One thing to realize is that images so included cannot be any
bigger then the text they are included with.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@233 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-09-12 05:03:47 +00:00
tom e26d93a3fa git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@231 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-09-08 03:22:12 +00:00
tom f622a4f7b6 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@225 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-25 00:14:45 +00:00
tom 7cb63a3db9 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@224 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-24 02:11:25 +00:00
tom 42b11c2e74 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@223 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-23 01:40:00 +00:00
tom f4d5ef1425 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@222 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-21 14:06:37 +00:00
tom 8109a8dbfe git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@221 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-21 13:34:54 +00:00
tom c8c1b93894 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@219 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-14 19:21:40 +00:00
tom 597be879e4 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@218 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-12 02:17:31 +00:00
tom 3d1a2e4d96 Ready to test, almost.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@217 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-08-12 01:56:36 +00:00
tom 00401fd0e9 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@216 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-11 23:39:00 +00:00
tom 6eef431161 Fixed a bug that prevented rollback of the info object.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@215 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-08-10 03:10:42 +00:00
tom f3b31c8025 More work on renpy.audio
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@214 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-08-09 02:26:38 +00:00
tom df4fa900e9 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@213 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-08 02:53:38 +00:00
tom fbd92b66b0 Working on sound.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@212 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-08-08 01:54:40 +00:00
tom a29fd3aa8b git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@211 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-08-08 01:16:06 +00:00
tom c9af317092 Menu text now supports percent-substitutions, like say statements
always did.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@210 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-07-29 12:24:52 +00:00
tom 91fdd68ec1 Fixed object rollback bug.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@209 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-07-28 14:14:40 +00:00
tom c8b90b04e2 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@207 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-07-27 03:27:00 +00:00
tom 8c85390aaf Finalizing 5.0.1.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@203 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-07-26 02:46:01 +00:00
tom b77f295921 Finalizing 5.0.1.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@202 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-07-26 02:35:30 +00:00
tom c0e3dfc99b Finalizing 5.0.1.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@201 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-07-26 02:34:17 +00:00
tom f5c6ea530d The hovered callback on buttons was no longer working. This is now
fixed. Also added a complementary unhovered callback.

A number of changes have been made to Ren'Py, that shrink the zip file
by over ... k.

The math module is now included.

The presplash screen now displays without a title-bar.




git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@200 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-07-21 04:08:26 +00:00
tom ce7ccc2f87 Updated demo game. Ready for 5.0.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@194 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-28 12:37:59 +00:00
tom a4d1670378 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@190 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-06-24 00:47:03 +00:00
tom c143c0c479 Fixed demo.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@189 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-23 03:14:21 +00:00
tom f7cb4eae9c Added config.font_replacement_map, which is used to map from a regular
font to a version of that font that is specialized as italic, bold, or
both.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@188 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-23 03:04:28 +00:00
tom bb35ff5658 Readying 4.8.9.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@185 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-18 00:56:30 +00:00
tom b479fd4a06 Fixed a bug that was preventing RevertableObjects from supporting
properties.


git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@184 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-17 02:04:18 +00:00
tom c9c0ac5809 Removing DSE from RenPY VC.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@183 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-16 16:52:57 +00:00
tom 04cbf7620c There is now a preference that controls skipping after choices.
The new function renpy.choice_for_skipping should be called by user
code to indicate that a choice is occuring, and therefore we might
want to end skipping.




git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@182 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-14 01:24:21 +00:00
tom 6770b268fc 4.4.8
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@178 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-12 15:24:59 +00:00
tom 60f988dc6c git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@177 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-06-12 14:31:29 +00:00
tom a8e8dc4f49 Unicode support has been extended to names in the script. Now names in
the script can contain unicode characters, as well as ascii
characters. This includes labels and image names. Right now, we
consider all unicode characters to be alphabetic, so be sure to use
ASCII space where spaces are required.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@176 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-12 14:26:46 +00:00
tom 622db83c50 Auto-forward-mode has been implemented. It is controlled by its own
preference, which gives the number of seconds for which a given number
of characters is shown to the user. The number of characters is
configured by config.afm_characters, which defaults to 250. The number
of characters is determined by the length of the string shown to the
user with a number of bonus characters added to it. The number of
bonus characters is configurable in config.afm_bonus, which defaults
to 25. Auto-forward-mode can be hidden from the user by setting 
library.had_afm to False.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@175 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-10 03:53:09 +00:00
tom d17b68a17c Added old_day_planner.
Now, the day planners will not let the user continue unless every
period has been assigned a valid value.




git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@173 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-09 17:06:00 +00:00
tom 8d0465e72e 4.8.7
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@170 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-09 03:37:41 +00:00
tom 3dc117ed96 Getting ready for 4.8.7.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@169 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-09 03:10:56 +00:00
tom 111b7e440d DSE.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@168 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-08 20:54:53 +00:00
tom 3e80c3acbb A new im.Tile image operator has been introduced. This operator tiles
a source image until it reaches a specified size, or the size of the
screen if no size is specified.

Ren'Py now supports a presplash screen. If the file presplash.png is
present in the game directory, it is shown to the user on
startup. This occurs before any loading or initialization occurs, so
the size of the window it is shown in is determined by the size of the
presplash.png image itself. 



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@166 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-07 21:35:17 +00:00
tom 56b6135b8e Implemented ImageDissolve, which is a transition that uses images to
control how the dissolve occurs.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@165 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-07 04:54:24 +00:00
tom 24ec412f1e The definitions found in common/definitions.rpy are now documented in
the reference. This gives a default set of transitions and positions,
as well as an image (black).



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@164 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-06 03:47:52 +00:00
tom cd987bdb6d The definitions found in common/definitions.rpy are now documented in
the reference. This gives a default set of transitions and positions,
as well as an image (black).



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@163 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-05 23:29:12 +00:00
tom d6f3b7aab0 Fade has been reimplemented in terms of Dissolve. This means that
fades will be as fast as dissolves. Fade now also can take a widget,
instead of a color, as an argument. If this widget is an Image, it
means that we will dissolve from the old screen to the image, hold at
the image for a given amount of time, and then dissolve from the image
to the new screen.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@162 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-04 15:10:39 +00:00
tom f2f9db46e9 As per user request, we have made programmatic equivalents of the
image, scene, show, and hide statements available. These functions are
named renpy.image, renpy.scene, renpy.show, and renpy.hide,
respectively.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@161 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-04 13:28:00 +00:00
tom 57c38e0363 Ren'Py now supports a new classification of layer, top layers. Top
layers are layers that are displayed above any transition. This is
useful if you want to have an overlay that is shown even when a
transition is in progress. To have the overlays be displayed as a 
top_layer, remove the string 'overlay' from config.layers and append
it to config.top_layers.

A new config variable, config.enable_fast_dissolve, was
introduced.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@160 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-04 12:35:44 +00:00
tom 6d055e775a Fixed bugs that caused blinking in overlay.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@159 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-03 14:32:00 +00:00
tom 48033b3b1e Ren'Py now supports a presplash screen. If the file presplash.png is
present in the game directory, it is shown to the user on
startup. This occurs before any loading or initialization occurs, so
the size of the window it is shown in is determined by the size of the
presplash.png image itself. 



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@158 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-02 18:05:07 +00:00
tom 9d57e0dad1 Separated out display, font, and mixer initialization code.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@157 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-02 16:46:03 +00:00
tom bed546e6e6 Added a RENPY_DISABLE_SOUND environment variable, which if set
disables support for sound in Ren'Py. (For use on platforms where
sound doesn't quite work.)

Fixed a bug in lint.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@156 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-06-01 21:20:39 +00:00
tom 94a9422ddc 4.8.6 made
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@150 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-29 16:11:30 +00:00
tom 8f7bd1be77 DynamicCharacter and Character have been unified into a single class,
such that now calling DynamicCharacter is equivalent to calling
Character with dynamic=True.

Character (and DynamicCharacter, and renpy.display_say) now take a new
argument, image. If this argument is set to True, then the name of the
character is interpreted as an image filename. This image is used
where the character's name would go, in place of a textual name.




git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@149 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-29 15:33:32 +00:00
tom e863d4a645 Ready for 4.8.6.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@148 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-29 02:57:14 +00:00
tom 8d1cf70edc Module version up.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@147 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-29 02:23:26 +00:00
tom eadb1c6385 Add document on how to release renpy games.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@146 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-29 02:17:38 +00:00
tom ad51d6a674 Ren'Py now takes a new option, --lint. (Or for those of you under
Windows, run lint.bat, editing it if necessary.) This option, when
supplied, causes Ren'Py to run checks for dozens of errors in the
script. Many of these checks test for problems that will only occur
at runtime, on platforms other than Windows, or when loading a
save-game after a change in script. This is the subtle stuff that my
script-review service was for... now it's automated, so you don't need
me anymore. 

Lint should be run before you release any game with Ren'Py, from now
on. In general, you should not release a game if lint gives you any
errors, but instead you should fix those errors first.

Lint also produces a count of the number of say statments and menus in
a program.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@145 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-29 00:44:56 +00:00
tom 6880da07b7 Fixed a bug in the _renpy module, in which we accidentally used the
height, rather than the width, of an image.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@144 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-28 20:02:43 +00:00
tom 35408d81ea git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@141 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-05-26 00:32:09 +00:00
tom d75eb13658 Ready for release, I think.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@140 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-25 23:06:51 +00:00
tom dc3a01aae4 Changed the look of the frame.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@139 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-25 21:41:55 +00:00
tom 1792f7a7af There is now a new config variable, config.text_tokenizer. This
variable is used to give a function that breaks a line of text up into
words, spaces, and other things. Providing a custom function here
allows for control of where line breaking is allowed, useful for
languages that have specific rules about that.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@138 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-25 19:47:50 +00:00
tom eb82c47109 Fixed bugs in image color mapping.
Got script ready for release.


git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@137 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-25 04:15:02 +00:00
tom 229748a68e Updated the Ren'Py module to 4.8.5.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@136 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-24 21:24:22 +00:00
tom d5d0d12697 There are now two new image operators. im.Map can map the pixels
values in an image to different values. Using this, one can adjust the
colors of an image. The im.Alpha function is built on top of this, and
allows one to alter the alpha of an image. (It even adjusts a
pre-existing alpha channel.)



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@135 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-24 20:53:59 +00:00
tom 6bce3b0e9a The SMAnimation framework can also be used to describe very
complicated, even nondeterministic, motions. This is done by declaring
a SMAnimation with None for the images, and then passing it in to the
at clause.

All motions (Motion, Pan, Move, and anim.SMAnimation when used as a
motion) can now be used as transitions, in which case the motion is
applied to the screen (or layer, as appropriate). This let us put
together two new transitions, hpunch and vpunch, which shake the
screen horizontally and vertically, as appropriate. Useful for when
the POV character gets punched by a particularly feisty girl.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@134 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-24 19:53:35 +00:00
tom 349f2479d3 Removed xblur32 code.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@133 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-24 05:05:41 +00:00
tom 75d2f81973 Before removing blur code.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@132 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-24 04:58:07 +00:00
tom eadbdebcb0 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@131 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-05-24 01:41:47 +00:00
tom 196e3701e9 Added a new, state-machine based animation function,
anim.SMAnimation. This function creates animations by applying images
and transitions specified by the states and edges of a nondeterminstic
state machine. The result is that it's possible to specify
complicated random patterns of behavior. This can allow a character to
blink randomly, allow random backgrounds to be shown, and even allow
things to randomly move around the screen. 



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@130 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-24 01:39:18 +00:00
tom a2904a7618 MoveTransition works with pairs of displayables.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@129 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-23 18:51:20 +00:00
tom 3a2fc5498a Fixed the gallery.rpy extra, so that it now works properly with the
main menu being in a menu context. 



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@128 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-22 03:23:31 +00:00
tom c3757fdea9 4.8.4
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@124 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-14 02:57:47 +00:00
tom 6ee37d4933 Readying 4.8.4.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@122 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-14 00:54:51 +00:00
tom 4ded3eba45 The xanchor and yanchor arguments can now take numbers between 0 and 1
as well as names. Functions returned by Motion now can either return
an xpos, ypos tuple or a xpos, ypos, xanchor, yanchor tuple. Move has
been upgraded so that it can take similar tuples as the start and
end arguments, letting us interpolate the location of the anchors
while moving an image around.

The above modification allowed us to implement a new move
transition. This transition attempts to find images that are present
in both the old and new scenes, but have changed location between the
two scenes. Such images are moved from the old location to the new
location over the duration of the move transition. This makes it easy
to smoothly move characters from one location to another on the
screen.

The demo script was updated and reorganized, to demonstrate more
features.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@120 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-12 19:15:55 +00:00
tom 277c7b4d88 Removed some unnecessary conversion from pixellate, improving its
performance when running on a computer with 32 bits per pixel
graphics. 

Implemented a basic map function, but haven't tested it yet.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@119 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-12 03:46:30 +00:00
tom 403bb65826 The format of the compiled script (.rpyc) files has changed. The new
format is more efficent, both in terms of space and memory. 
Unfortunately, the format change is not backwards compatible,
and the automatic upgrade function probably won't work. So you'll need
to delete the rpyc files from the game directory when you copy it
over.




git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@117 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-09 20:10:34 +00:00
tom 0d1946f3bc git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@116 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-05-07 18:29:23 +00:00
tom 61381a2f12 Voice support.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@115 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-07 14:56:52 +00:00
tom 168729e9d9 Speedup loadable.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@114 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-07 06:45:04 +00:00
tom 42a2d2c8c0 A new Motion function allows the user to give a function that controls
how a displayable moves on the screen, allowing for customizable
motions. Pan and Move have been reimplemented in terms of Motion. All
three functions now take repeat and bounce parameters. Repeat causes
the motion to repeat after a single period is over, while bounce
causes the motion to bounce from extreme to extreme.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@113 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-07 05:44:02 +00:00
tom 42c8f4339c git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@111 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-05-07 04:00:03 +00:00
tom 8c7ca92e54 Fixed bugs in windows midi support.
Removed config.music_callback, replaced with interact_callbacks.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@109 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-07 03:35:54 +00:00
tom 0919f556b4 Removed psyco.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@108 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-06 06:07:22 +00:00
tom 50bb65973b Updates for 4.8.2a.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@105 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-04 00:00:30 +00:00
tom 660fa3114e 4.8 Release
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@102 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-03 03:40:17 +00:00
tom 9ebad42f5a Wrote a readme for the module.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@99 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-03 01:19:11 +00:00
tom cd8acdcd8a Moved the main menu into the same context as the game menu.
A number of changes to the menus.

The ability to run things in their own contexts.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@98 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-03 00:56:37 +00:00
tom 9bbecc250d Two-window-say extra, conditional dialogue.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@97 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-02 18:31:26 +00:00
tom 518a10cae0 Implemented pixellate transition, changed version of the Ren'Py module.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@96 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-02 16:31:06 +00:00
tom c1206e7fbd More work on the renpy module.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@95 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-02 04:36:53 +00:00
tom daaba9f294 Initial checkin of RenPy module.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@94 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-05-01 21:36:19 +00:00
tom 2e864c971d Unicode support in python blocks now works. This allows unicode
strings to be used with Ren'Py. This is especially useful when naming
characters and translating the various menus into your native
language. Please note that the only unicode allowed is inside unicode
strings, which begin with a u before the first quote. Unicode outside
of unicode strings is an error.

The special labels intended for main menu use, _continue and
_preferences, now call a user main menu after they finish executing,
rather than the library main menu. This simplifies the implementation
of user-defined main menus.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@93 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-29 03:44:46 +00:00
tom 5b9494ebcc PyLint done.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@91 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-26 14:49:05 +00:00
tom d228af6ffd Version up!
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@90 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-26 04:19:17 +00:00
tom 454f103818 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@89 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-04-26 03:28:00 +00:00
tom e26989c288 An empty string is now rendered as if it was a string consisting of a
single space. This fixes a bug in which an empty string was rendered
with zero width and height.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@88 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-26 03:08:20 +00:00
tom 5c06b96c18 Update documentation.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@87 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-26 02:57:47 +00:00
tom d3bfda1a11 Character objects are no longer limited to displaying through
renpy.display_say, but can now be supplied a function argument, which
is a function that is called instead. This will make it easier to
radically change how dialogue is displayed.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@86 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-26 02:49:43 +00:00
tom 0308d48953 There is a new spinner preference type, which allows a value to be
incremented or decremented. This preference is now used to adjust the
speed at which text is shown to the user. So now this is controlled by
the user, rather than the config.annoying_text_cps parameter. If you
don't want to give the user this control, you can set library.has_cps
to False. A number of new styles define the spinner. [ document ]




git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@85 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-26 02:15:38 +00:00
tom 9525db2212 The way in which preferences are displayed has been made more
configurable. Now, there is a new variable, library.preferences, which
is a map from a style to a list of preferences to be displayed in a
vbox with that style. There are now two new styles (prefs_left and
prefs_right) which control the placement of the default preference 
columns. [ document ]



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@84 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-26 01:14:34 +00:00
tom 7afca9d5d5 There are two new style properties, xmaximum and ymaximum. These two
properties limit the size of the widgets that they are applied to, as
if that widget was placed inside a ui.sizer(). (In fact, ui.sizer() is
now implemented in terms of these properties.) [ document ]

Now, we only restore or rollback to places where all of the transient
layers are empty. This prevents a bug in which additional dialogue
will be lost when a restore or rollback occurs. 



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@83 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-26 00:48:17 +00:00
tom ccca92c88a The ui.grid object now has a new argument, transpose, which allows
widgets to be added down the columns first. It also is smarter about
allocating space to subwidgets when xfill or yfill are set.

The file entries in the load and save screens are now arraigned in a
grid, which has the style file_picker_grid. This means that we should
now be smarter in the presence of games of varying size, and we should
wrap text if it gets too long. As part of this,
library.file_picker_length has now been broken out into two variables,
library.file_picker_rows and library.file_picker_cols. [ document ]

renpy.music_stop now supports a fadeout parameter, for orthagonality
with the new renpy.music_start. 

Now, python blocks have the correct filename and line numbers
annotated onto them. This means that tracebacks generated in python
blocks will show a .rpy file and the appropriate line, rather than
showing "<none>" and a random line as they did before.




git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@82 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-25 16:25:52 +00:00
tom aa3ff2f012 Changelog fixes.
Added transition support to the gallery extra.


git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@80 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-22 14:25:50 +00:00
tom 46e8fdd092 Made the 'h' key now hide the windows, so there's a way to do that on
Mac OS X.

Many pylint inspired bug fixes.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@79 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-22 14:03:57 +00:00
tom f2963d33c3 Added focus.py
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@78 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-22 02:42:44 +00:00
tom 0c96d52b54 Added gallery script.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@77 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-22 02:37:05 +00:00
tom 74678d5cef Bug fixing.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@76 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-22 02:36:45 +00:00
tom 3dd6959fe1 Documentation update.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@75 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-22 02:26:59 +00:00
tom 7b6ec9db5b Rewrote styles so they're not buggy.
Fixed bugs with refresh.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@74 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-22 00:24:27 +00:00
tom 404936dae6 Replaced disabled buttons with the insensitive_ prefix.
Added support for the RENPY_DISABLE_FULLSCREEN environment variable.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@73 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-21 19:22:58 +00:00
tom f7de1b0eb0 Implemented new music and sound code.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@72 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-21 14:08:01 +00:00
tom 8614e6406d Fixed a bug that prevented character names from beginning with 'u'.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@71 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-20 15:09:27 +00:00
tom 0c6e1893e2 Implemented new image code, which now makes it possible and easy to
perform operations on images. This code includes a new size-based
image cache.

Rewrote imagemap in terms of cropped images and imagebuttons.

Added code to load images before peforming a transition.

Added code to store seen images in a dictionary.



git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@70 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-20 02:03:10 +00:00
tom f1bec25291 git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@69 ef037c1b-3fee-0310-a5a7-a1260de54e99 2005-04-11 03:00:50 +00:00
tom 429b34b80f New focus system.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@68 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-11 02:26:14 +00:00
tom 5a28ecd5f5 Eliminate game.store.
git-svn-id: file:///home/tom/magnetic/ab/cvs-and-svn/svn-repos/renpy/trunk@67 ef037c1b-3fee-0310-a5a7-a1260de54e99
2005-04-06 01:02:44 +00:00
127 changed files with 23076 additions and 5482 deletions
+956
View File
@@ -1,9 +1,965 @@
Changes potentially requiring updates to scripts are flagged with +++.
Major bug fixes that could affect many systems are indicated with !!!.
New in Ren'Py 5.3.0
-------------------
This changelog has been updated to flag changes requiring (or
suggesting) script updates, or providing major bug fixes.
!!!
Script compatibility has been broken, but for a good reason. The
previous version of Ren'Py stored python blocks in the script as bits
of python bytecode in the .rpyc file. This has a problem. Python
bytecode is not guaranteed to be forward or backwards
compatible. Without some sort of versioning, an upgrade of python
could cause these bytecodes to be silently interpreted incorrectly,
which could lead to games failing to work properly. To fix this,
several things have changed. The .rpyc files now store the python
script, rather than just the bytecode. The bytecode has been moved
into a bytecode.rpyb file, which is automatically created when a
script changes. This file contains versioned bytecode, and is
automatically regenerated if either the source code changes or the
version of python used to run Ren'Py changes. It is always generated
into the game directory. While it is possible to include bytecode.rpyb
into a .rpya file (under the same conditions as .rpyc files can be
added to .rpya files), realize that it may spring into existence in
the game directory automatically if the user tries to run Ren'Py with
a newer or older version of python. One should ship bytecode.rpyb as
part of a completed Ren'Py game.
There are now a few changes to Ren'Py to allow it to better support
scripts that are not games. Some of the internal messages have been
changed to remove the word "game". While "game" has still been left in
the most prominent places (the main and game menus), this is because
these places are the most obvious to the user, and hence the most
likely to be translated using library.translations.
For the same reason, Ren'Py will also search for the game in a data/
directory if the game/ directory is not found, and it will then look
for the game scripts in data.rpa along with game.rpa.
ImageDissolve now supports a number of different kinds of alpha
ramps. Along with the default linear, we now support cubic,
double-cubic, and mirrored-double-cubic ramp types. These types of
ramps allow for smoother or sharper ImageDissolve transtions. Thanks
go to shaja for contributing the code that implements this.
+++
Alpha channels are now supported on text colors. When rendering
text with a color or drop_shadow_color having an alpha that is not
255, that alpha is then used to scale the alpha channel of the drawn
text. This basically means that alpha is now respected in text, wheras
previously it was ignored. The default drop_shadow_color is now
(0, 0, 0, 255), for compatibility with the way drop shadows were
rendered previously. If user code renders text with an alpha that is
not 255, the alpha should be set to 255 to ensure the text is rendered
the same way as before.
The DSE has been updated. There are now new event.only() and
event.choose_one() condition functions, and a basic guide to writing
events in the DSE can be found in extras/dse/EVENTS.txt.
Now, user code can import python modules from the game directory
and .rpa archives. A guide to using Python to extened Ren'Py can be
found in doc/EXTENDING.txt. For now, it's just about how to use
pygame code with Ren'Py.
Renamed extras/chinese.rpy to extras/east_asian.rpy, and changed the
line breaking algorithm to one that can be used with both Chinese and
Japanese text. It may also be usable with Korean, but I'm not sure
about that.
Made the zero-width-space character break lines if necessary, so it
can be used to have auto-breaking with Thai.
All defined preferences are now found in library.all_preferences,
which provides a level of indirection when customizing the preferences
screens.
Now, we force the presplash to be shown using windib. This can fix
potential directx incompatibility bugs with Windows XP. This only is
an issue if a game has a presplash, the bug was fixed for regular code
in 5.1.1. The presplash is also shown faster now, before the rest of
Ren'Py loads.
Fixed a bug that affected voice. This required splitting
config.interact_callbacks into config.interact_callbacks and the new
config.start_interact_callbacks. The latter is called exactly once per
interaction, while the former is called whenever an interaction starts
or restarts.
Fixed a bug with non-ascii text and the new log feature.
Now, invoking Ren'Py with --profile on the command line does the same
thing as setting config.profile in the script, without requiring the
user to actually change the script.
Moved most of the code that used to be in run_game.py into
renpy/bootstrap.py (the renpy.bootstrap module.) This shouldn't really
affect anything, but it does make run_game.py a bit cleaner.
New in Ren'Py 5.2.1
-------------------
Added a command that allows the user to skip to the next
choice. Primarily intended for developers, this feature needs to be
enabled by setting config.fast_skipping to True before it can be
used. Once enabled, fast skipping can be activated by typing '>'. It
jumps the user to the next interaction that isn't a say, a transition,
or a call to renpy.pause()
Setting config.log to a filename will now cause a log of the user's
actions to be written to that filename.
Added (optional, and discouraged) monkeypatch protection through the
--lock command-line option.
Fixed a regression, introduced in 5.1.6, that prevented what_prefix
and what_suffix from working.
New in Ren'Py 5.2.0
-------------------
Restructured the distribution. All of the tools are now in the tools/
directory, while the dse has been moved to extras/dse, with a script
to launch it found in tools/.
Implemented a new warp feature. By supplying a filename and line
number, this feature tries to jump you to the statement preceding that
line. It isn't perfect, but for some games, this lets you have a live
preview of the game. Please see the reference manual for instructions on
how to invoke this, and a number of caveats.
Also implemented an editor feature. By pressing E (that's shift-e),
one can launch an editor at the location of the current statement in
the Ren'Py program. To enable this, one needs to set config.editor to
the command line for your favorite text editor. Again, see the
reference manual (for config.editor) for more details.
Implemented a new function, renpy.get_filename_line(), which returns
the filename and line number of the currently executing
statement. This can be used in conjunction with renpy.watch() to
constantly display the line number of the current statement.
Fixed a bug in the text widget that caused it to be sized incorrectly
in certain cases. Fixed an incorrect reference to a wav that became an
ogg in the demo. Modified renpy.watch() so that it coerces the result
of evaluating its argument to unicode, meaning it's no longer
necessary to manually do so.
New in Ren'Py 5.1.6
-------------------
Fixed Motion (the class that implements Move and Pan) to allow motions
to be used as transitions, in conjunction with the say and menu
statements. Previously, they only worked with the with statement.
Added a new variable, config.with_callback, that can be used to
specify a function that is called when a with statement runs. This can
be used, for example, to put up a window when a transition occurs,
which has the effect of preventing the window from disappearing.
Implemented a new text tag, {fast}. This tag causes the slow text
display to start from a given point in the text string. This allows
one to show some text, run some command, and show the same text with
additions, and have the slow text display occur correctly.
Character objects now can take properties prefixed with what_ or
window_. Properties prefixed with what_ are applied to the text being
spoken, while those prefixed with window_ are applied to the window
containing everything. Unprefixed properties are still applied to the
label giving the name of the character speaking.
There is now a new function, LiveComposite, that can composite
together arbitrary displayables, rather than just images. It has the
same API as im.Composite, but should be used when one of the things
you wish to composite is an animation.
A new variable, config.auto_choice_delay, has been added. If not None,
this variable gives a number of seconds that Ren'Py will pause at an
in-game menu before picking a random choice from that menu. We'd
expect this variable to always be set to None in released games, but
setting it to a number will allow for automated demonstrations of
games without much human interaction.
The kanamode.rpy extra has been updated to support some of the newer
features, like slow text and auto-forward mode.
+++
Archives now combine the index and the data into a single file. Now,
instead of needing .rpa and .rpi files, only a .rpa file is needed
(and is created). The code and the archiver have been updated to
support this new format. The code still supports reading the old
format, for compatibility purposes. When archives are re-created, the
.rpi file can be deleted with no ill effects.
There is now a new function, renpy.block_rollback(), that prevents
Ren'Py from rolling back through the point at which it was called.
Renamed config.overlay_during_wait to config.overlay_during_with,
since the former name was a mistake.
New in Ren'Py 5.1.5
-------------------
There are now two new variables, library.main_menu_positions and
library.game_menu_positions. If not None, then we expect these
variables to be maps between main or game menu navigation button
labels, and properties that will be supplied to those buttons. This is
intended to allow the user to manually position the various
buttons. This shuts off the default positioning code, so if you
position one button in a menu, you'll need to position them all.
Added a new extra, menu_positions.rpy, that demonstrates the new
positioning code.
Added a new extra, quick_save.rpy, that adds a quick save feature to
Ren'Py. This feature adds a single save slot that can be quickly
accessed without going through the game menu. It also adds a quick
load button to the main menu, which causes a load from this slot. The
button is disabled if no quick save game exists.
+++
To support this, changed the format of library.main_menu. Now, instead
of being a list of pairs, library.main_menu is a list of triples. The
new third component is an expression, which must evaluate to true for
the menu button to be enabled.
Fixed a bug in lint that was caused by the introduction of the onlayer
clause in Ren'Py 5.1.3. This bug caused lint to fail entirely, so not
it works again.
Added raw string literals, although they're not a documented feature.
New in Ren'Py 5.1.4
-------------------
Implemented two new text tags, {w} and {p}. {w} pauses the display of
dialogue, waiting for the user to click to continue. {p} does the same
thing, but also breaks the line where it occurs.
Added a framerate limiter. When enabled by setting config.framerate,
Ren'Py limits the number of frames that are displayed per second to
the supplied values. This tends to smooth out the framerate, and hence
the percieved smoothness of the program. It will lower the overall
framerate, but make it more regular.
Some improvements in the auto-forward mode. AFM will no longer dismiss
text when it is still being drawn onto the screen. (Because the text
speed is not infinite.) There is now also an AFM callback that can be
used to control if an auto-forward occurs. This is intended to be used
with the new renpy.sound.is_playing call, to prevent auto-forwarding
from occuring if when a voice is being played. I'm not bothering to
actually implement much of the voice functionality until someone
seriously proposes a voiced game, but I do want to add in the
infrastructure.
Increased the default rollback length back to 100, now that we have a
readback extra people can use if they want to. Updated readback so it
works a little better.
Updated LICENSE.txt, to point to a mirror site that contains the
source code for the LGPL libraries used in the various binary
distributions of Ren'Py.
New in Ren'Py 5.1.3
-------------------
The scene, show, and hide statements now take an onlayer clause, which
gives the layer that the operations occur on. So if we declare high to
be another layer (by setting config.layers to something like
[ 'master', 'high', 'transient', 'overlay' ]), then one can use a
statement like "show eileen happy onlayer high" to show the given
image on the high layer. This image will not be removed when scene
statements clear other layers, such as the default master layer.
+++
To support the above, the parameters to renpy.show() changed in an
incompatible way. If your code uses renpy.show(), it will need to be
upgraded. Instead of considering the second and later parameters to be
at expressions, it now takes as its second parameter a list of all at
expressions. (This lets us use the third parameter to accept the layer
name.)
The label 'after_load' is now called after a game is loaded, if it
exists on disk. This label can be used to upgrade save files, restore
music, and the like.
It's now possible to pick the screen the game menu enters when it is
invoked, by setting the _game_menu_screen variable.
Simplified the demo game script by moving the style stuff out of that
first init block, and down to the bottom of the script where it's out
of the way of a first viewing. Hopefully, this will reduce the shock
of someone first encountering Ren'Py.
Documented how to modify and replace the game and main menus, and how
to add in one's own preferences.
New in Ren'Py 5.1.2
-------------------
Ren'Py now supports embedding images inside text. Please read the
documentation of ui.text for some important caveats about how this
works, but basically, the input of ui.text can be a string or a list
containing strings and widgets. In the latter case, the widgets are
rendered inside the text. One can also use a text tag like
{image=filename.png} to include an image anywhere a text tag
works. One thing to realize is that images so included cannot be any
bigger then the text they are included with.
A new widget, anim.Blink, blinks a child widget in and out of
existence by varying its alpha channel.
Character now allows for the use of a click-to-continue widget when
the widget finishes showing text to the user. While many widgets can
be used, this feature is especially useful with Images, Animations,
and anim.Blink. The click-to-continue widget can be nestled in with
the end of the text, or placed at a fixed location on the screen.
Custom mouse support has been totaly rewritten. It now supports
animated mice, and changing the mouse cursor based on the kind of
interaction that is ongoing. Rather than describe the changes here,
read the documentation of config.mouse to understand the new
behavior. Please note that config.mouse has changed, and so if you use
it in your game (and AFAIK noone does), it will need to be modified.
Custom mouse support has also had significant performance
improvements.
Ren'Py can now read the script files out of an archive file. The
archive used can either be game.rpa and game.rpi, or name.rpa and
name.rpi, where name is the name of the file that is used to start
Ren'Py, with the extension and any prefix (before an including an
underscore) stripped off.
New in Ren'Py 5.1.1
-------------------
Fixed a bug that prevented the various day-planners from working, in
both the demo game and the DSE. (This was a bug in the game more so
then a bug in Ren'Py proper.)
!!!
Fixed a bug on Windows that manifested with DirectX under some
circumstances. We now use SDL's windib driver, which seems just as
fast for the software surface stuff we're doing.
Updated native midi support on the Mac, eliminating a non-obvious
race/locking problem. A new version of the Ren'Py launcher for
Macintosh comes with this release.
Removed the .rpyl files that were added in 5.1.0, as they wound up not
being needed after all.
New in Ren'Py 5.1.0
-------------------
+++
There is now a new sound system, supporting playing up to 8 mp3, ogg
vorbis, ogg speex, mod, or wav files at once. Midi can also be played,
but is limited to one midi file at a time. Audio files can be read out
of archives. Music and Sound now have their own mixers, which control
the system mixer. The low-level audio api has been ripped out and
replaced with new renpy.sound and renpy.music apis, which are somewhat
higher-level, and more flexible.
In order to support this, we've temporarily dropped Mac support. In a
few weeks, we'll be releasing a tool that restores and improves
Ren'Py's compatability with the Macintosh platform. To help support
this tool, this release includes .rpyl files for the demo game and
DSE.
The bar widget has been extended. It can now be adjusted by the user,
and it can show a thumb in the middle. Various volume and speed
preferences have been adjusted to take advantage of the new bar
widget.
All of the backgrounds in the demo-game script and in the reference
now are of the form "bg name". This lets one run a command
"show bg newbg" to replace the background without replacing the
current screen. The demo game also has been prefixed with a UTF-8 BOM,
so editors should (hopefully) load and save it as UTF-8.
There is now a new font text tag. By writing {font=Font1,Font2}, one
can change the font in the middle of a block of text. Use this
wisely. Font files with extensions are now also searched for in the
system font directories, although we do not recommend this unless you
are having encoding problems.
Menu labels and choices support percent-substitution, like say
statements always have.
Now, all of the im image constructors take properties as
arguments. This lets placement properties be applied to images, as
with regular widgets.
A new extra, chinese.rpy, implements word-wrapping for the Chinese
language. Or at least tries to.
Fixed a bug which made the images created with im.Rotozoom
incompatible with im.Alpha.
Fixed a weird focus bug that manifested upon rollback when resized
frames were on the screen.
New in Ren'Py 5.0.1
-------------------
The hovered callback on buttons was no longer working. This is now
fixed. Also added a complementary unhovered callback. Documented them
both on ui.button().
A number of changes have been made to Ren'Py, the cumulative effect of
which is to shrink the package size by a bit.
The python math module is now included with Ren'Py.
The presplash screen now displays without a titlebar.
Ren'Py now always runs internally in 24 or 32 bpp mode. It will still
run on a 16 bpp machine, but there will be a conversion from 32 -> 16
bits when it is displayed. This fixes a number of transitions that did
not work in 16 bit mode.
Fixed a bug with im.Size in which 800x600 was used as the default
target size, instead of the current screen size.
Fixed a bug that prevented drop_shadow = None from working.
New in Ren'Py 5.0
-----------------
Blessed 4.8.10 as the 5.0 stable release.
Minor changes to images in the demo game.
New in Ren'Py 4.8.10
--------------------
Added config.font_replacement_map, which is used to map from a regular
font to a version of that font that is specialized as italic, bold, or
both.
New in Ren'Py 4.8.9
-------------------
There is now a preference that controls skipping after choices.
The new function renpy.choice_for_skipping should be called by user
code to indicate that a choice is occuring, and therefore we might
want to end skipping.
Fixed a bug that was preventing RevertableObjects from supporting
properties.
New in Ren'Py 4.8.8
-------------------
Auto-forward-mode has been implemented. It is controlled by its own
preference, which gives the number of seconds for which a given number
of characters is shown to the user. The number of characters is
configured by config.afm_characters, which defaults to 250. The number
of characters is determined by the length of the string shown to the
user with a number of bonus characters added to it. The number of
bonus characters is configurable in config.afm_bonus, which defaults
to 25. Auto-forward-mode can be hidden from the user by setting
library.has_afm to False.
+++
The preferences screen has been redesigned, to have three
columns. Code that manipulates library.preferences may need to be adjusted.
Unicode support has been extended to names in the script. Now names in
the script can contain unicode characters, as well as ascii
characters. This includes labels and image names. Right now, we
consider all unicode characters to be alphabetic, so be sure to use
ASCII space where spaces are required.
New in Ren'Py 4.8.7
-------------------
The new DSE dating-sim engine code is now present in the dse/
directory. It's not documented, apart from the comments in the source
code, but it can be run by running the new run_dse program.
The image statement now passes its argument to the Image function,
in loose mode. This means that strings and tuples are turned into
single and composited images, respectively. So it's now possible to
write:
init:
image control room = "control_room.jpg"
instead of having to write:
init:
image control room = Image("control_room.jpg")
Of course, the old way still works.
There is now a new transition type, ImageDissolve. It lets us creat
dissolve transitions in which pixels are dissolved at different times,
which can be used for a range of effects. Two such effects are
'squares' and 'blinds'. A number of other types are defined in the
source of the demo game, in their own section.
A new im.Tile image operator has been introduced. This operator tiles
a source image until it reaches a specified size, or the size of the
screen if no size is specified.
Ren'Py now supports a presplash screen. If the file presplash.png is
present in the game directory, it is shown to the user on
startup. This occurs before any loading or initialization occurs, so
the size of the window it is shown in is determined by the size of the
presplash.png image itself.
As per user request, we have made programmatic equivalents of the
image, scene, show, and hide statements available. These functions are
named renpy.image, renpy.scene, renpy.show, and renpy.hide,
respectively.
Ren'Py now supports a new classification of layer, top layers. Top
layers are layers that are displayed above any transition. This is
useful if you want to have an overlay that is shown even when a
transition is in progress. To have the overlays be displayed as a
top_layer, remove the string 'overlay' from config.layers and append
it to config.top_layers.
A new config variable, config.enable_fast_dissolve, was
introduced. Setting this to False can fix a potential bug in the
dissolve transition when used with an overlay layer that has an alpha
channel that is not fully transparent or opaque, at the cost of 25% of
the performance of dissolve. It usually can be kept at True with no
ill effects.
Fade has been reimplemented in terms of Dissolve. This means that
fades will be as fast as dissolves. Fade now also can take a widget,
instead of a color, as an argument. If this widget is an Image, it
means that we will dissolve from the old screen to the image, hold at
the image for a given amount of time, and then dissolve from the image
to the new screen.
The ImageReference class is now documented and present in the game's
namespace. This lets us refer to an image by name from user code.
Added a RENPY_DISABLE_SOUND environment variable, which if set
disables support for sound in Ren'Py. (For use on platforms where
sound doesn't quite work.)
Fixed a bug in lint.
Fixed bugs that caused overlays to jump in and out during transitions,
restoring the pre-4.8 behavior.
The definitions found in common/definitions.rpy are now documented in
the reference. This gives a default set of transitions and positions,
as well as an image (black).
New in 4.8.6
------------
Ren'Py now takes a new option, --lint. (Or for those of you under
Windows, run lint.bat, editing it if necessary.) This option, when
supplied, causes Ren'Py to run checks for dozens of errors in the
script. Many of these checks test for problems that will only occur
at runtime, on platforms other than Windows, or when loading a
save-game after a change in script. This is the subtle stuff that my
script-review service was for... now it's automated, so you don't need
me anymore.
Lint should be run before you release any game with Ren'Py, from now
on. In general, you should not release a game if lint gives you any
errors, but instead you should fix those errors first.
Lint also produces a count of the number of say statments and menus in
a program.
DynamicCharacter and Character have been unified into a single class,
such that now calling DynamicCharacter is equivalent to calling
Character with dynamic=True.
Character (and DynamicCharacter, and renpy.display_say) now take a new
argument, image. If this argument is set to True, then the name of the
character is interpreted as an image filename. This image is used
where the character's name would go, in place of a textual name.
Fixed a bug that prevented newlines from being recognized in text
widgets. (Introduced when I rewrote tokenizing in the previous
release.)
Fixed a bug in the _renpy module, in which we accidentally used the
height, rather than the width, of an image.
New in 4.8.5
------------
Added a new state-machine based animation function, anim.SMAnimation.
This function creates animations by applying images and transitions
specified by the states and edges of a nondeterminstic state
machine. The result is that it's possible to specify complicated
random patterns of behavior. This can allow a character to blink
randomly, allow random backgrounds to be shown, and even allow things
to randomly move around the screen.
The SMAnimation framework can also be used to describe very
complicated, even nondeterministic, motions. This is done by declaring
a SMAnimation with None for the images, and then passing it in to the
at clause.
I should point out that despite the complexity of the interface, the S
and M in SMAnimation stand for state machine, and not anything
else. Honest.
All motions (Motion, Pan, Move, and anim.SMAnimation when used as a
motion) can now be used as transitions, in which case the motion is
applied to the screen (or layer, as appropriate). This let us put
together two new transitions, hpunch and vpunch, which shake the
screen horizontally and vertically, as appropriate. Useful for when
the POV character gets punched by a particularly feisty girl.
There are now two new image operators. im.Map can map the pixels
values in an image to different values. Using this, one can adjust the
colors of an image. The im.Alpha function is built on top of this, and
allows one to alter the alpha of an image. (It even adjusts a
pre-existing alpha channel.)
The definitions of transitions and placements, which were once in
script.rpy, have been moved into common/definitions.rpy.
There is now a new config variable, config.text_tokenizer. This
variable is used to give a function that breaks a line of text up into
words, spaces, and other things. Providing a custom function here
allows for control of where line breaking is allowed, useful for
languages that have different rules about that.
Fixed the gallery.rpy extra, so that it now works properly with the
main menu being in a menu context.
The demo game was updated to demonstrate the new feature, as well as
to have a new, drop-shadowed window frame.
New in 4.8.4
------------
The format of the compiled script (.rpyc) files has changed. The new
format is more efficent, both in terms of space and memory.
Unfortunately, the format change is not backwards compatible,
and the automatic upgrade function probably won't work. So you'll need
to delete the rpyc files from the game directory when you copy it
over.
Removed some unnecessary conversion from pixellate, improving its
performance when running on a computer with 32 bits per pixel
graphics.
The xanchor and yanchor arguments can now take numbers between 0 and 1
as well as names. Functions returned by Motion now can either return
an xpos, ypos tuple or a xpos, ypos, xanchor, yanchor tuple. Move has
been upgraded so that it can take similar tuples as the start and
end arguments, letting us interpolate the location of the anchors
while moving an image around.
The above modification allowed us to implement a new move
transition. This transition attempts to find images that are present
in both the old and new scenes, but have changed location between the
two scenes. Such images are moved from the old location to the new
location over the duration of the move transition. This makes it easy
to smoothly move characters from one location to another on the
screen.
The demo script was updated and reorganized, to demonstrate more
features. Now, all transitions are demonstrated, as well as things
like renpy.input, DynamicCharacter, Move, and Pan.
A slightly updated version of the script for Moonlight Walks is now
in the scripts/ directory. It's there as an example for people to
learn from. This script has been updated to work with Ren'Py 4.8, and
to fix a tense error.
!!!
Fixed another segfault in windows native midi support.
New in 4.8.3
------------
!!!
Ren'Py no longer uses the psyco specializing compiler. The relatively
small (about 2%) speed increase it gave wasn't worth the hassle
involved in dealing with memory leaks and other semantic changes. This
change significantly lowers Ren'Py's memory usage, and fixes a major
memory leak that was introduced in the 4.8 series.
!!!
A number of race conditions were fixed in the windows native midi
code. The upshot of this is that we can now play midi on windows
without there being a small chance of crashing each time we start a
new track.
+++
The config.music_interact variable is gone. It's been replaced by
config.interact_callbacks, which is a list of functions to call before
each interaction. (This will help us support voice.)
A new Motion function allows the user to give a function that controls
how a displayable moves on the screen, allowing for customizable
motions. Pan and Move have been reimplemented in terms of Motion. All
three functions now take repeat and bounce parameters. Repeat causes
the motion to repeat after a single period is over, while bounce
causes the motion to bounce from extreme to extreme.
Finally, a new voice extra has been added as extras/voice.rpy. This is
a simplified version of what will eventually be Ren'Py's voice
system. Right now, the only feature missing is that voices for
characters cannot be enabled or disabled individually, but may
instead only be manipulated in the aggregate.
New in 4.8.2a
-------------
!!!
Clicking preferences on the main menu jumpled to an undefined
label. This is now fixed.
A new extra has been added. The commentary.rpy extra adds an optional
commentary window, which is shown only when the commentary preference
it adds is enabled.
To support the above, statements are only marked as seen if they have
actually been shown to the user (by ui.interact being called during
the statement.)
New in 4.8.2
------------
Unicode support in python blocks now works. This allows unicode
strings to be used with Ren'Py. This is especially useful when naming
characters and translating the various menus into your native
language. Please note that the only unicode allowed is inside unicode
strings, which begin with a u before the first quote. Unicode outside
of unicode strings is an error.
We apologize to the non-english-speaking world for not noticing this
one sooner.
+++
The main menu was moved out of the context that the main body of the
game code executes in, into its own context. The only effect this
should have on user code is that some of the menu options changed from
simple label names into more complicated jumps. But it does allow us
to share the main menu with the game menu, which is something I've
had requests for.
A number of minor changes were made to the menu at this time.
The README_RENPY.txt file was updated, to make it more current and
useful.
The Character and DynamicCharacter objects were updated to take a
condition argument. If this argument does not evaluate to true when a
line of dialogue is executed, the line is not displayed to the user.
A new extra was added (in two_window_say.rpy) that places the name of
a speaking character, and what they are saying, in two windows
that can be styled separately.
The biggest change in Ren'Py was the addition of the _renpy
module. This is a C module that enhances the functionality of pygame,
allowing us to provide transitions and apply effects that simply
weren't possible before. A compiled version ships with the windows
binaries, installers are available for windows and mac from the Ren'PY
website, and source is included in the module/ directory to compile it
under Linux and Unix.
The presence of the _renpy module allows for two new features. The
first is that the thumbnails of save images are now smoothly scaled
down, which improves their look quite a bit.
The second new feature is the new pixellate transition, which
pixellates the old screen, swaps pixellated screens, and then
depixellates the new screen. One can see this feature in action by
right clicking to access the game menu... for this release, we've set
the game menu transition to be pixellate.
New in 4.8.1
------------
Character objects are no longer limited to displaying through
renpy.display_say, but can now be supplied a function argument, which
is a function that is called instead. This will make it easier to
radically change how dialogue is displayed.
The ui.grid object now has a new argument, transpose, which allows
widgets to be added down the columns first. It also is smarter about
allocating space to subwidgets when xfill or yfill are set.
+++
The file entries in the load and save screens are now arrainged in a
grid, which has the style file_picker_grid. This means that we should
now be smarter in the presence of games of varying size, and we should
wrap text if it gets too long. As part of this,
library.file_page_length has now been broken out into two variables,
library.file_page_rows and library.file_page_cols.
renpy.music_stop now supports a fadeout parameter, for orthogonality
with the new renpy.music_start.
Now, python blocks have the correct filename and line numbers
annotated onto them. This means that tracebacks generated in python
blocks will show a .rpy file and the appropriate line, rather than
showing "<none>" and a random line as they did before.
There are two new style properties, xmaximum and ymaximum. These two
properties limit the size of the widgets that they are applied to, as
if that widget was placed inside a ui.sizer(). (In fact, ui.sizer() is
now implemented in terms of these properties.)
!!!
Now, we only restore or rollback to places where all of the transient
layers are empty. This prevents a bug in which additional dialogue
will be lost when a restore or rollback occurs.
+++
The way in which preferences are displayed has been made more
configurable. Now, there is a new variable, library.preferences, which
is a map from a style to a list of preferences to be displayed in a
vbox with that style. There are now two new styles (prefs_left and
prefs_right) which control the placement of the default preference
columns.
There is a new spinner preference type, which allows a value to be
incremented or decremented. This preference is now used to adjust the
speed at which text is shown to the user. So now this is controlled by
the user, rather than the config.annoying_text_cps parameter. If you
don't want to give the user this control, you can set library.has_cps
to False. A number of new styles define the spinner.
An empty string is now rendered as if it was a string consisting of a
single space. This fixes a bug in which an empty string was rendered
with zero width and height.
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
+16 -3
View File
@@ -19,14 +19,24 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
The character images in the demo game (game/9a_*.png) are under their
own copyright. I allow them to be used as part of the demo game, but
not as part of other games.
---
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:
For the purposes of complicance with the LGPL, we've made available
the source code to all of the LGPL libraries used by Ren'Py at
http://www.bishoujo.us/renpy/dl/lgpl/.
The following libraries are used in binary packages of Ren'Py:
Python (Python License)
http://www.python.org
@@ -37,8 +47,11 @@ http://www.pygame.org
SDL (GNU LGPL)
http://www.libsdl.org
SDL_mixer (GNU LGPL)
http://www.libsdl.org/projects/SDL_mixer
SDL_image (GNU LGPL)
http://www.libsdl.org/projects/SDL_image/
SDL_sound (GNU LGPL)
http://www.icculus.org/SDL_sound/
SDL_ttf (GNU LGPL)
http://www.libsdl.org/projects/SDL_ttf
+45 -32
View File
@@ -1,42 +1,55 @@
Greetings!
This file contains boilerplate information about running Ren'Py that
can be included into the README for your game. It may be necessary to
change some of the filenames in this document, to make it specific to
your game.
You've downloaded a game that was written using Ren'Py, a python based
engine for visual novel style games. If you're interested in making
your own games, you may want to consider downloading Ren'Py from:
Feel free to use this in your projects.
http://www.bishoujo.us/renpy/
Ren'Py games can be played using the keyboard or the mouse.
When dialogue or transitions are displayed, they can be dismissed
by clicking the left mouse button, or pressing space or enter on the
keyboard. When the control key is held down, dialogue and transitions
are rapidly dismissed, provided that the user has seen them
already. Tab toggles skipping mode.
Running the Game
================
Choices on menus can be made by clicking on the appopriate choice with
the mouse, or by picking the choice using the up and down arrow and
hitting enter.
(Windows)
Hitting escape or clicking the right mouse button brings up the game
menu. This game menu lets you save and load games, quit the game, or
return to the main menu. It also lets you set preferences that control
the behavior of the game. These preferences are:
If this game was installed on Windows using an installer, then you can
run it by choosing the shortcut left by the installer. If this game
was installed on Windows using the cross-platform zip file, then it
can be run by executing the run_game.exe file.
Display --- Controls if the game displays in a window or
fullscreen.
Music --- Controls if music is played or silenced.
TAB and CTRL Skip --- Chooses if CTRL skips all messages or only messages
that have ever been seen on this computer.
Transitions --- Controls the amount of transitions that are shown.
(Macintosh)
Finally, Ren'Py supports a rollback feature, which lets you, with some
limits, play the game backwards. For example, you can rollback to a
menu, and save or make a different choice. It also lets you see
dialogue that you missed. You can rollback by pressing the 'page up'
key, or by scrolling your mouse wheel up. Pressing 'page down' or
scrolling your mouse wheel down lets you skip dialogue that's been
seen in this session, which is almost the opposite of rollback.
For information about running Ren'Py games on Mac OS 10.3 or higher,
please go to http://www.bishoujo.us/renpy/mac.html .
Thank you for choosing to play a Ren'Py powered game.
(Linux/Unix)
For information about running Ren'Py games under Linux and Unix,
please go to http://www.bishoujo.us/renpy/linux.html . At the very
least, you will need to compile the _renpy module. Read
the file module/README.txt for details.
Playing the Game
================
By default, the game starts running in full screen mode. On some
computers, especially some virtual machines, this can lead to mouse
problems. To fix this, press 'f'.
Some of the more interesting game actions can be performed as follows:
- The left mouse button is used to advance to the next line of
dialogue, or to pick menu options.
- The right mouse button brings you into a menu screen where you can
save the game, load the game, change preferences, return to the main
menu, or quit entirely.
- Scrolling the mouse wheel up or pushing page up returns you to the
previous screen.
- Holding down the CTRL key skips dialogue. By default, it only
skips read dialogue, but this can be changed by a preference. Tab
toggles skip mode.
+1 -1
View File
@@ -16,7 +16,7 @@ from cPickle import loads, dumps, HIGHEST_PROTOCOL
padding_every = 10240
# The amount of padding we will add.
padding_max = 32
padding_max = 4
def randpadding():
+6 -3
View File
@@ -22,10 +22,13 @@ programs = [
program(sys.argv[1]),
]
sys.argv[1:] = [ 'py2exe' ]
sys.argv[1:] = [ 'py2exe', '-x', '-a', '--dll-excludes', 'w9xpopen.exe' ]
setup(name="RenPy",
windows=programs,
console=[ "archiver.py", "add_from.py", "console.py", "dump_text.py" ],
zipfile='lib/renpy.zip',
console=[ "console.py" ],
zipfile='lib/renpy.code',
options={ 'py2exe' : { 'excludes' : [ 'doctest', ],
'optimize' : 2,
} },
)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/python
import glob
import os
files = glob.glob("common/*.rpy")
files.sort()
styles = [ ]
for fn in files:
f = file(fn)
fi = iter(f)
for l in iter(fi):
l = l.strip()
if l.startswith("### "):
prefix, style, parent = l.split()
desc = ""
for l in fi:
l = l.strip()
if not l.startswith("# "):
break
desc = desc + " " + l[2:]
styles.append(' style.create("%s", "%s", "%s")' % (style, parent, desc[1:]))
f.close()
f = file("common/style.rpy")
of = file("common/style.rpy.new", "w")
fi = iter(f)
for l in fi:
of.write(l)
if l.startswith("# AUTOMATICALLY GENERATED"):
for l in styles:
of.write(l + "\n")
for l in fi:
if l.startswith("# END AUTOMATICALLY GENERATED"):
break
of.write(l)
f.close()
of.close()
os.rename("common/style.rpy", "common/style.rpy.bak")
os.rename("common/style.rpy.new", "common/style.rpy")
Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

+61
View File
@@ -0,0 +1,61 @@
# This file contains a number of definitions of standard
# locations and transitions. We've moved them into the common
# directory so that it's easy for an updated version of all of these
# definitions.
init -1:
# Positions ##############################################################
# These are positions that can be used inside at clauses. We set
# them up here so that they can be used throughout the program.
$ left = Position(xpos=0.0, xanchor='left')
$ center = Position()
$ right = Position(xpos=1.0, xanchor='right')
# Offscreen positions for use with the move transition. Images at
# these positions are still shown (and consume
# resources)... remember to hide the image after the transition.
$ offscreenleft = Position(xpos=0.0, xanchor='right')
$ offscreenright = Position(xpos=1.0, xanchor='left')
# Transitions ############################################################
# Simple transitions.
$ fade = Fade(.5, 0, .5) # Fade to black and back.
$ dissolve = Dissolve(0.5)
$ pixellate = Pixellate(1.0, 5)
# Various uses of CropMove.
$ wiperight = CropMove(1.0, "wiperight")
$ wipeleft = CropMove(1.0, "wipeleft")
$ wipeup = CropMove(1.0, "wipeup")
$ wipedown = CropMove(1.0, "wipedown")
$ slideright = CropMove(1.0, "slideright")
$ slideleft = CropMove(1.0, "slideleft")
$ slideup = CropMove(1.0, "slideup")
$ slidedown = CropMove(1.0, "slidedown")
$ slideawayright = CropMove(1.0, "slideawayright")
$ slideawayleft = CropMove(1.0, "slideawayleft")
$ slideawayup = CropMove(1.0, "slideawayup")
$ slideawaydown = CropMove(1.0, "slideawaydown")
$ irisout = CropMove(1.0, "irisout")
$ irisin = CropMove(1.0, "irisin")
# This moves changed images to their new locations
$ move = MoveTransition(0.5)
# These shake the screen up and down for a quarter second.
# The delay needs to be an integer multiple of the period.
$ vpunch = Move((0, 10), (0, -10), .10, bounce=True, repeat=True, delay=.275)
$ hpunch = Move((15, 0), (-15, 0), .10, bounce=True, repeat=True, delay=.275)
# These use the ImageDissolve to do some nifty effects.
$ blinds = ImageDissolve(im.Tile("blindstile.png"), 1.0, 8)
$ squares = ImageDissolve(im.Tile("squarestile.png"), 1.0, 256)
image black = Solid((0, 0, 0, 255))
+590
View File
@@ -0,0 +1,590 @@
# This file contains code for the game menu, and associated
# functionality that looks an awful lot like it.
init -499:
python:
######################################################################
# First up, we define a bunch of configuration variable, which the
# user can change.
# The contents of the game menu choices.
library.game_menu = [
( "return", "Return", ui.jumps("_return"), 'True'),
( "prefs", "Preferences", ui.jumps("_prefs_screen"), 'True' ),
( "save", "Save Game", ui.jumps("_save_screen"), 'not renpy.context().main_menu' ),
( "load", "Load Game", ui.jumps("_load_screen"), 'True'),
( "mainmenu", "Main Menu", lambda : _mainmenu_prompt(), 'not renpy.context().main_menu' ),
( "quit", "Quit", lambda : _quit_prompt("quit"), 'True' ),
]
# If not None, a map from the names of the game menu
# navigation buttons to new fixed positions for them on
# the screen.
library.game_menu_positions = None
# The number of columns of files to show at once.
library.file_page_cols = 2
# The number of rows of files to show at once.
library.file_page_rows = 5
# The number of pages to add quick access buttons for.
library.file_quick_access_pages = 5
# The width of a thumbnail.
library.thumbnail_width = 66
# The height of a thumbnail.
library.thumbnail_height = 50
# Sound played when entering the library without clicking a
# button.
library.enter_sound = None
# Sound played when leaving the library without clicking a
# button.
library.exit_sound = None
# Transition that occurs when entering the game menu.
library.enter_transition = None
# Transition that occurs when leaving the game menu.
library.exit_transition = None
# This lets us disable the file pager. (So we only have one
# page of files.)
library.disable_file_pager = False
# This lets us disable the thumbnails in the file pager.
library.disable_thumbnails = False
# If True, we will be prompted before loading a game. (This can
# be changed from inside the game code, so that one can load from
# the first few screens but not after that.)
_load_prompt = True
# The screen that we go to when entering the game menu.
_game_menu_screen = "_save_screen"
######################################################################
# Next, support code.
# This is used to store scratch data that's used by the
# library, but shouldn't be saved out as part of the savegame.
_scratch = object()
# This returns a window containing the game menu navigation
# buttons, set up to jump to the appropriate screen sections.
#
# This can be overridden by user code. It's called with the
# name of the selected screen... one of "mainmenu", "prefs",
# "save", "load", or "quit", at least for the default game
# menu. If None, then it's an indication that none of the
# nav buttons should be shown.
def _game_nav(screen, buttons=True):
ui.add(renpy.Keymap(toggle_fullscreen = renpy.toggle_fullscreen))
ui.add(renpy.Keymap(game_menu=ui.jumps("_noisy_return")))
### gm_root_window default
# (window) The style used for the root window of the game
# menu. This is primarily used to change the background of
# the game menu.
ui.window(style='gm_root_window')
ui.null()
if not screen:
return
### gm_nav_window default
# (window) The style used by a window containing
# buttons that allow the user to navigate through the
# different screens of the game menu.
### gm_nav_vbox thin_vbox
# (box) The style that is used by the box inside the
# gm_nav_window
if library.game_menu_positions:
ui.fixed()
else:
ui.window(style='gm_nav_window')
ui.vbox(focus='gm_nav', style='gm_nav_vbox')
for key, label, clicked, enabled in library.game_menu:
disabled = False
if not eval(enabled):
disabled = True
clicked = None
### gm_nav_button button
# (window, hover) The style of an unselected game menu
# navigation button.
### gm_nav_button_text button_text
# (text, hover) The style of the text of an unselected game
# menu navigation button.
### gm_nav_selected_button selected_button
# (window, hover) The style of a selected game menu
# navigation button.
### gm_nav_selected_button_text selected_button_text
# (text, hover) The style of the text of a selected game
# menu navigation button.
if library.game_menu_positions:
kwargs = library.game_menu_positions.get(label, { })
else:
kwargs = { }
_button_factory(label, "gm_nav",
selected=(key==screen),
disabled=disabled,
clicked=clicked, **kwargs)
ui.close()
# This is called from the game menu to interact with the
# user. It suppresses all of the underlays and overlays.
def _game_interact():
return ui.interact(suppress_underlay=True,
suppress_overlay=True,
mouse="gamemenu")
# This renders a slot with a file in it, in the file picker.
def _render_savefile(name, info, newest):
image, extra = info
### file_picker_entry button
# (window, hover) The style that is used for each of the
# slots in the file picker.
ui.button(style='file_picker_entry',
clicked=ui.returns(("return", (name, True))))
### file_picker_entry_box thin_hbox
# (box) The style that is used for the hbox inside of a
# file picker entry.
ui.hbox(style='file_picker_entry_box')
if not library.disable_thumbnails:
ui.add(image)
### file_picker_text default
# (text) A base style for all text that is displayed in the
# file picker.
### file_picker_new file_picker_text
# (text) The style that is applied to the number of the new slot in
# the file picker.
### file_picker_old file_picker_text
# (text) The style that is applied to the number of the old slot in
# the file picker.
if name == newest:
ui.text(name + ". ", style='file_picker_new')
else:
ui.text(name + ". ", style='file_picker_old')
### file_picker_extra_info file_picker_text
# (text) The style that is applied to extra info in the file
# picker. The extra info is the save time, and the save_name
# if one exists.
ui.text(extra, style='file_picker_extra_info')
ui.close()
# This renders an empty slot in the file picker.
def _render_new_slot(name, save):
if save:
clicked=ui.returns(("return", (name, False)))
enable_hover = True
else:
clicked = None
enable_hover = True
ui.button(style='file_picker_entry',
clicked=clicked,
enable_hover=enable_hover)
ui.hbox(style='file_picker_entry_box')
if not library.disable_thumbnails:
ui.null(width=library.thumbnail_width,
height=library.thumbnail_height)
ui.text(name + ". ", style='file_picker_old')
### file_picker_empty_slot file_picker_text
# (text) The style that is used for the empty slot indicator
# in the file picker.
ui.text(_("Empty Slot."), style='file_picker_empty_slot')
ui.close()
_scratch.file_picker_index = None
# This displays a file picker that can chose a save file from
# the list of save files.
def _file_picker(selected, save):
# The number of slots in a page.
file_page_length = library.file_page_cols * library.file_page_rows
saves, newest = renpy.saved_games(regexp=r'[0-9]+')
# The index of the first entry in the page.
fpi = _scratch.file_picker_index
if fpi is None:
fpi = 0
if newest:
fpi = (int(newest) - 1) // file_page_length * file_page_length
if fpi < 0:
fpi = 0
while True:
if fpi < 0:
fpi = 0
_scratch.file_picker_index = fpi
# Show Navigation
_game_nav(selected)
### file_picker_window default
# (window) A window containing the file picker
# that is used to choose slots for loading and saving.
### file_picker_window_vbox thin_vbox
# (box) The vbox containing both the nav and the grid in
# the file picker.
ui.window(style='file_picker_window')
ui.vbox(style='file_picker_window_vbox') # whole thing.
if not library.disable_file_pager:
### file_picker_navbox thick_hbox
# (box) The box containing the naviation (next/previous)
# buttons in the file picker.
### file_picker_nav_button button
# (window, hover) The style that is used for enabled file
# picker navigation buttons.
### file_picker_nav_button_text button_text
# (text) The style that is used for the label of enabled
# file picker navigation buttons.
# Draw the navigation.
ui.hbox(style='file_picker_navbox') # nav buttons.
def tb(cond, label, clicked):
_button_factory(label, "file_picker_nav", disabled=not cond, clicked=clicked)
# Previous
tb(fpi > 0, _('Previous'), ui.returns(("fpidelta", -1)))
# Quick Access
for i in range(0, library.file_quick_access_pages):
target = i * file_page_length
tb(fpi != target, str(i + 1), ui.returns(("fpiset", target)))
# Next
tb(True, _('Next'), ui.returns(("fpidelta", +1)))
# Done with nav buttons.
ui.close()
# This draws a single slot.
def entry(offset):
i = fpi + offset
name = str(i + 1)
if name not in saves:
_render_new_slot(name, save)
else:
_render_savefile(name, saves[name], newest)
### file_picker_grid default
# The style of the grid containing the file
# picker entries.
# Actually draw a slot.
ui.grid(library.file_page_cols,
library.file_page_rows,
style='file_picker_grid',
transpose=True) # slots
for i in range(0, file_page_length):
entry(i)
ui.close() # slots
ui.close() # whole thing
result = _game_interact()
type, value = result
if type == "return":
return value
if type == "fpidelta":
fpi += value * file_page_length
if type == "fpiset":
fpi = value
# This renders a yes/no prompt, as part of the game menu. If
# screen is None, then it omits the game menu navigation.
def _yesno_prompt(screen, message):
"""
@param screen: The screen button that should be highlighted when this prompt is shown. If None, then no game menu navigation is shown.
@param message: The message that is shown to the user to prompt them to answer yes or no.
This function returns True if the user clicks Yes, or False if the user clicks No.
"""
_game_nav(screen)
### yesno_window default
# (window) The style of a window containing a yes/no prompt.
### yesno_window_vbox thick_vbox
# (box) The style of a box containing the widgets in a
# yes/no prompt.
ui.window(style='yesno_window')
ui.vbox(style='yesno_window_vbox')
### yesno_label default
# (text) The style used for the prompt in a yes/no
# dialog.
### yesno_button button
# (window, hover) The style of yes/no buttons.
### yesno_button_text button_text
# (window, hover) The style of yes/no button text.
_label_factory(message, "yesno", xpos=0.5, xanchor='center')
ui.grid(5, 1, xfill=True)
# The extra nulls are because we want equal whitespace surrounding
# the two buttons. It should work as long as we have xfill=True
ui.null()
_button_factory("Yes", 'yesno', clicked=ui.returns(True), xpos=0.5, xanchor='center')
ui.null()
_button_factory("No", 'yesno', clicked=ui.returns(False), xpos=0.5, xanchor='center')
ui.null()
ui.close()
ui.close()
return _game_interact()
def _show_exception(title, message):
### error_window default
# (window) The style of the window containing internal error
# messages.
### error_title default
# (text) The style of the text containing the title of an
# error message.
### error_body default
# (text) The style of the body of an error message.
ui.window(style='error_window')
ui.vbox()
ui.text(title, style='error_title')
ui.text("")
ui.text(message, style='error_body')
ui.text("")
ui.text(_("Please click to continue."), style='error_body')
ui.close()
ui.saybehavior()
ui.interact()
library.old_names["Are you sure you want to quit?"] = "Are you sure you want to quit the game?"
def _quit_prompt(screen="quit"):
def prompt():
return _yesno_prompt(screen, "Are you sure you want to quit?")
if renpy.invoke_in_new_context(prompt):
renpy.quit()
else:
return
library.old_names["Are you sure you want to return to the main menu?\nThis will lose unsaved progress."] = "Are you sure you want to return to the main menu?\nThis will end your game."
def _mainmenu_prompt(screen="mainmenu"):
def prompt():
return _yesno_prompt(screen, "Are you sure you want to return to the main menu?\nThis will lose unsaved progress.")
if renpy.invoke_in_new_context(prompt):
renpy.full_restart()
else:
return
##############################################################################
# The actual menus begin around here.
# First up, we have the code that is common to all of the entry points into
# the game.
# This is the code that executes when entering a menu context, except
# for the scene statement.
label _enter_menu_without_scene:
$ renpy.movie_stop()
$ renpy.take_screenshot((library.thumbnail_width, library.thumbnail_height))
# This may be changed, if we are already in the main menu.
$ renpy.context().main_menu = False
return
label _enter_menu:
call _enter_menu_without_scene from _call_enter_menu_without_scene_1
scene
return
# Factored this all into one place, to make our lives a bit easier.
label _enter_game_menu:
call _enter_menu from _call__enter_menu_2
if library.enter_transition:
$ renpy.transition(library.enter_transition)
if renpy.has_label("enter_game_menu"):
call expression "enter_game_menu" from _call_enter_game_menu_1
return
##############################################################################
# Now, we have the actual entry points into the various menu screens.
# Entry points from the game into menu-space.
label _game_menu:
call _enter_game_menu from _call__enter_game_menu_0
jump expression _game_menu_screen
label _game_menu_save:
call _enter_game_menu from _call__enter_game_menu_1
jump _save_screen
label _game_menu_load:
call _enter_game_menu from _call__enter_game_menu_2
jump _load_screen
label _game_menu_preferences:
call _enter_game_menu from _call__enter_game_menu_3
jump _prefs_screen
label _confirm_quit:
call _enter_menu from _call__enter_menu_3
$ _quit_prompt(None)
return
##############################################################################
# Finally, we have the actual menu screens that are shown to the user.
init -499:
$ library.old_names["Loading will lose unsaved progress.\nAre you sure you want to do this?"] = "Loading a new game will end your current game.\nAre you sure you want to do this?"
label _load_screen:
python hide:
fn, exists = _file_picker("load", False )
if not renpy.context().main_menu and _load_prompt:
if _yesno_prompt("load", "Loading will lose unsaved progress.\nAre you sure you want to do this?"):
renpy.load(fn)
else:
renpy.load(fn)
jump _load_screen
label _save_screen:
$ _fn, _exists = _file_picker("save", True)
if not _exists or _yesno_prompt("save", "Are you sure you want to overwrite your save?"):
python hide:
if save_name:
full_save_name = "\n" + save_name
else:
full_save_name = ""
try:
renpy.save(_fn, renpy.time.strftime("%b %d, %H:%M") +
full_save_name)
except Exception, e:
if config.debug:
raise
message = ( _("The error message was:\n\n") +
e.__class__.__name__ + ": " + unicode(e) + "\n\n" +
_("You may want to try saving in a different slot, or playing for a while and trying again later."))
_show_exception(_("Save Failed."), message)
jump _save_screen
label _quit:
$ renpy.quit()
# Make some noise, then return.
label _noisy_return:
$ renpy.play(library.exit_sound)
# Return to the game.
label _return:
if renpy.context().main_menu:
jump _main_menu
if library.exit_transition:
$ renpy.transition(library.exit_transition)
return
+48 -436
View File
@@ -20,55 +20,20 @@ init -500:
# Used to store library settings.
library = object()
# The number of files to show at once.
library.file_page_length = 10
# The minimum version of the module we work with. Don't change
# this unless you know what you're doing.
library.module_version = 4008002
# The number of pages to add quick access buttons for.
library.file_quick_access_pages = 5
# A small amount of padding.
library.padding = 2
# The width of a thumbnail.
library.thumbnail_width = 66
# The height of a thumbnail.
library.thumbnail_height = 50
# The contents of the main menu.
library.main_menu = [
( "Start Game", "start" ),
( "Continue Game", "_continue" ),
( "Preferences", "_preferences" ),
( "Quit Game", "_quit" ),
]
# The contents of the game menu choices.
library.game_menu = [
( "return", "Return", "_return", 'True'),
( "prefs", "Preferences", "_prefs_screen", 'True' ),
( "save", "Save Game", "_save_screen", '_can_save' ),
( "load", "Load Game", "_load_screen", 'True'),
( "mainmenu", "Main Menu", "_full_restart", 'not _at_main_menu' ),
( "quit", "Quit", "_quit_screen", 'True' ),
]
# Should we warn the user if the module is missing or has a bad
# version?
library.module_warning = False
# Used to translate strings in the library.
library.translations = { }
# Sound played when entering the library without clicking a
# button.
library.enter_sound = None
# Sound played when leaving the library without clicking a
# button.
library.exit_sound = None
# Transition that occurs when entering the game menu.
library.enter_transition = None
# Transition that occurs when leaving the game menu.
library.exit_transition = None
# Used internally to maintain compatiblity with old
# translations of strings.
library.old_names = { }
# True if the skip indicator should be shown.
library.skip_indicator = True
@@ -77,12 +42,6 @@ init -500:
# taking place.
save_name = ''
# True if we're at the main menu, false otherwise.
_at_main_menu = False
# True if we can save, false otherwise.
_can_save = True
def _button_factory(label,
type=None,
selected=None,
@@ -113,11 +72,11 @@ init -500:
style = type
if selected:
if selected and not disabled:
style += "_selected"
if disabled:
style += "_disabled"
clicked = None
style = style + "_button"
text_style = style + "_text"
@@ -146,8 +105,11 @@ init -500:
if s in library.translations:
return library.translations[s]
else:
return s
if s in library.old_names and library.old_names[s] in library.translations:
return library.translations[library.old_names[s]]
return s
# Are the windows currently hidden?
_windows_hidden = False
@@ -164,7 +126,16 @@ init -500:
renpy.call_in_new_context('_game_menu')
def toggle_skipping():
config.skipping = not config.skipping
if config.skipping is None:
config.skipping = "slow"
else:
config.skipping = None
renpy.restart_interaction()
def fast_skip():
if config.fast_skipping or config.developer:
config.skipping = "fast"
# The default keymap.
km = renpy.Keymap(
@@ -173,8 +144,10 @@ init -500:
toggle_fullscreen = renpy.toggle_fullscreen,
toggle_music = renpy.toggle_music,
toggle_skip = toggle_skipping,
fast_skip = fast_skip,
game_menu = invoke_game_menu,
hide_windows = renpy.curried_call_in_new_context("_hide_windows")
hide_windows = renpy.curried_call_in_new_context("_hide_windows"),
launch_editor = renpy.launch_editor,
)
config.underlay = [ km ]
@@ -185,11 +158,15 @@ init -500:
def skip_indicator():
if config.allow_skipping and library.skip_indicator:
### skip_indicator default
# (text) The style and placement of the skip indicator.
ui.conditional("config.skipping")
if config.skipping == "slow" and library.skip_indicator:
ui.text(_("Skip Mode"), style='skip_indicator')
if config.skipping == "fast" and library.skip_indicator:
ui.text(_("Fast Skip Mode"), style='skip_indicator')
config.overlay_functions.append(skip_indicator)
return
@@ -208,399 +185,34 @@ label _hide_windows:
return
# This is the true starting point of the program. Sssh... Don't
# tell anyone.
label _start:
if renpy.has_label("splashscreen") and not _restart:
call splashscreen
jump _main_menu
# This shows the main menu to the user.
label _main_menu:
# Let the user completely override the main menu.
if renpy.has_label("main_menu"):
jump main_menu
label _library_main_menu:
scene
python hide:
ui.add(renpy.Keymap(toggle_fullscreen = renpy.toggle_fullscreen))
ui.keymousebehavior()
ui.window(style='mm_root_window')
ui.fixed()
ui.window(style='mm_menu_window')
ui.vbox()
for text, label in library.main_menu:
_button_factory(text, "mm", clicked=ui.returns(label))
ui.close()
ui.close()
store._result = ui.interact(suppress_overlay = True,
suppress_underlay = True)
# Computed jump to the appropriate label.
$ renpy.jump(_result)
return
# Used to call the game menu.
label _continue:
$ _can_save = False
$ _at_main_menu = True
$ renpy.call_in_new_context("_game_menu_load")
$ _can_save = True
$ _at_main_menu = False
jump _library_main_menu
# Used to call the game menu.
label _preferences:
$ _can_save = False
$ _at_main_menu = True
$ renpy.call_in_new_context("_game_menu_preferences")
$ _can_save = True
$ _at_main_menu = False
jump _library_main_menu
##############################################################################
#
# Code for the game menu.
init -500:
python:
# This is used to store scratch data that's used by the
# library, but shouldn't be saved out as part of the savegame.
_scratch = object()
# This returns a window containing the game menu navigation
# buttons, set up to jump to the appropriate screen sections.
def _game_nav(selected):
ui.keymousebehavior()
ui.add(renpy.Keymap(game_menu=ui.jumps("_noisy_return")))
ui.window(style='gm_root_window')
ui.fixed()
ui.window(style='gm_nav_window')
ui.vbox()
for key, label, target, enabled in library.game_menu:
clicked = ui.jumps(target)
disabled = False
if not eval(enabled):
disabled = True
clicked = ui.returns(None)
_button_factory(label, "gm_nav", selected=(key==selected),
disabled=disabled, clicked=clicked)
ui.close()
ui.close()
def _game_interact():
return ui.interact(suppress_underlay=True,
suppress_overlay=True)
def _render_new_slot(name, save):
if save:
clicked=ui.returns(("return", (name, False)))
enable_hover = True
else:
clicked=None
enable_hover = False
# This code here handles check for the correct version of the Ren'Py module.
ui.button(style='file_picker_entry',
clicked=clicked,
enable_hover=enable_hover)
ui.hbox(padding=library.padding)
ui.null(width=library.thumbnail_width,
height=library.thumbnail_height)
ui.text(name + ". ", style='file_picker_old')
ui.text(_("Empty Slot."), style='file_picker_empty_slot')
ui.close()
def _render_savefile(name, info, newest):
label _check_module:
image, extra = info
if not library.module_warning:
return
ui.button(style='file_picker_entry',
clicked=ui.returns(("return", (name, True))))
ui.hbox(padding=library.padding)
ui.add(image)
if name == newest:
ui.text(name + ". ", style='file_picker_new')
else:
ui.text(name + ". ", style='file_picker_old')
python hide:
module_info = _("While Ren'Py games are playable without the _renpy module, some features may be disabled. For more information, read the module/README.txt file or go to http://www.bishoujo.us/renpy/.")
ui.text(extra, style='file_picker_extra_info')
ui.close()
_scratch.file_picker_index = None
# This displays a file picker that can chose a save file from
# the list of save files.
def _file_picker(selected, save):
saves, newest = renpy.saved_games()
# The index of the first entry in the page.
fpi = _scratch.file_picker_index
if fpi is None:
fpi = 0
if newest:
fpi = (int(newest) - 1) // library.file_page_length * library.file_page_length
if fpi < 0:
fpi = 0
# The length of a half-page of files.
hfpl = library.file_page_length // 2
while True:
if fpi < 0:
fpi = 0
_scratch.file_picker_index = fpi
# Show Navigation
_game_nav(selected)
ui.window(style='file_picker_window')
ui.vbox() # whole thing.
# Draw the navigation.
ui.hbox(padding=library.padding * 10, style='file_picker_navbox') # nav buttons.
def tb(cond, label, clicked):
_button_factory(label, "file_picker_nav", disabled=not cond, clicked=clicked)
# Previous
tb(fpi > 0, _('Previous'), ui.returns(("fpidelta", -1)))
# Quick Access
for i in range(0, library.file_quick_access_pages):
target = i * library.file_page_length
tb(fpi != target, str(i + 1), ui.returns(("fpiset", target)))
# Next
tb(True, _('Next'), ui.returns(("fpidelta", +1)))
# Done with nav buttons.
ui.close()
# This draws a single slot.
def entry(offset):
i = fpi + offset
name = str(i + 1)
if name not in saves:
_render_new_slot(name, save)
else:
_render_savefile(name, saves[name], newest)
# Actually draw a slot.
ui.hbox() # slots
ui.vbox()
for i in range(0, hfpl):
entry(i)
ui.close()
ui.vbox()
for i in range(hfpl, hfpl * 2):
entry(i)
ui.close()
ui.close() # slots
ui.close() # whole thing
result = _game_interact()
type, value = result
if type == "return":
return value
if type == "fpidelta":
fpi += value * library.file_page_length
if type == "fpiset":
fpi = value
def _yesno_prompt(screen, message):
_game_nav(screen)
ui.window(style='yesno_window')
ui.vbox(library.padding * 10, xpos=0.5, xanchor='center', ypos=0.5, yanchor='center')
_label_factory(message, "yesno", xpos=0.5, xanchor='center')
ui.grid(5, 1, xfill=True)
# The extra nulls are because we want equal whitespace surrounding
# the two buttons. It should work as long as we have xfill=True
ui.null()
_button_factory("Yes", 'yesno', clicked=ui.returns(True), xpos=0.5, xanchor='center')
ui.null()
_button_factory("No", 'yesno', clicked=ui.returns(False), xpos=0.5, xanchor='center')
ui.null()
ui.close()
ui.close()
return _game_interact()
def _show_exception(title, message):
ui.add(Solid((0, 0, 0, 255)))
ui.vbox()
ui.text(title, color=(255, 128, 128, 255))
ui.text("")
ui.text(message)
ui.text("")
ui.text("Please click to continue.")
ui.close()
ui.saybehavior()
ui.interact()
# Factored this all into one place, to make our lives a bit easier.
label _enter_game_menu:
scene
$ renpy.movie_stop()
$ renpy.take_screenshot((library.thumbnail_width, library.thumbnail_height))
if library.enter_transition:
$ renpy.transition(library.enter_transition)
if renpy.module_version() == 0:
_show_exception(_("_renpy module not found."),
_("The _renpy module could not be loaded on your system.") + "\n\n" + module_info)
elif renpy.module_version() < library.module_version:
_show_exception(_("Old _renpy module found."),
_("An old version (%d) of the Ren'Py module was found on your system, while this game requires version %d.") % (renpy.module_version(), library.module_version) + "\n\n" + module_info)
return
# Entry points from the game into menu-space.
label _game_menu:
label _game_menu_save:
call _enter_game_menu
jump _save_screen
label _game_menu_load:
call _enter_game_menu
jump _load_screen
label _game_menu_preferences:
call _enter_game_menu
jump _prefs_screen
label _confirm_quit:
call _enter_game_menu
jump _quit_screen
# Menu screens.
label _load_screen:
python:
_fn, _exists = _file_picker("load", False )
python:
renpy.load(_fn)
jump _load_screen
label _save_screen:
$ _fn, _exists = _file_picker("save", True)
if not _exists or _yesno_prompt("save", _("Are you sure you want to overwrite your save?")):
python hide:
if save_name:
full_save_name = "\n" + save_name
else:
full_save_name = ""
try:
renpy.save(_fn, renpy.time.strftime("%b %d, %H:%M") +
full_save_name)
except Exception, e:
if config.debug:
raise
message = ( "The error message was:\n\n" +
e.__class__.__name__ + ": " + unicode(e) + "\n\n" +
"You may want to try saving in a different slot, or playing for a while and trying again later.")
_show_exception(_("Save Failed."), message)
jump _save_screen
# Asks the user if he wants to quit.
label _quit_screen:
if _yesno_prompt("quit", _("Are you sure you want to end the game?")):
jump _quit
else:
jump _return
label _quit:
$ renpy.quit()
label _full_restart:
$ renpy.full_restart()
# Make some noise, then return.
label _noisy_return:
$ renpy.play(library.exit_sound)
# Return to the game.
label _return:
if library.exit_transition:
$ renpy.transition(library.exit_transition)
return
# Random nice things to have.
init:
+122
View File
@@ -0,0 +1,122 @@
# This file contains code fo the main menu, and anything else that
# happens upon initial execution of a Ren'Py program.
init -498:
python hide:
library.old_names['Quit'] = 'Quit Game'
# The contents of the main menu.
library.main_menu = [
( "Start Game", "start", 'True'),
( "Continue Game", ui.jumps("_load_screen"), 'True' ),
( "Preferences", ui.jumps("_prefs_screen"), 'True' ),
( "Quit", ui.jumps("_quit"), 'True' ),
]
# If not None, this is used to fix the positions of the
# things in the main menu.
library.main_menu_positions = None
# This is the true starting point of the program. Sssh... Don't
# tell anyone.
label _start:
call _check_module from _call__check_module_1
if renpy.has_label("splashscreen") and not _restart:
call expression "splashscreen" from _call_splashscreen_1
# Clean out any residual scene from the splashscreen.
scene
$ renpy.call_in_new_context("_enter_main_menu")
# Should never happen... but might as well do something
jump start
# At this point, we've been switched into a new context. So we
# initialize it.
label _enter_main_menu:
call _enter_menu from _call__enter_menu_1
$ renpy.context().main_menu = True
# This is called to show the main menu to the user.
label _main_menu:
# Let the user completely override the main menu. (But please note
# it still lives in the menu context, rather than the game context.)
if renpy.has_label("main_menu"):
jump expression "main_menu"
# This is the default main menu, which we get if the user hasn't
# defined his own, or if that function calls this explicitly.
label _library_main_menu:
scene
python hide:
ui.add(renpy.Keymap(toggle_fullscreen = renpy.toggle_fullscreen))
ui.keymousebehavior()
### mm_root_window default
# (window) The style used for the root window of the main
# menu. This is primarily used to set a background for the
# main menu.
ui.window(style='mm_root_window')
ui.fixed()
### mm_menu_window default
# (window) A window that contains the choices in
# the main menu. Change this to change the placement of
# these choices on the main menu screen.
### mm_menu_window_vbox thin_vbox
# (box) The vbox containing the main menu choices.
if library.main_menu_positions:
ui.fixed()
else:
ui.window(style='mm_menu_window')
ui.vbox(style='mm_menu_window_vbox')
for text, clicked, enabled in library.main_menu:
if isinstance(clicked, basestring):
clicked = ui.jumpsoutofcontext(clicked)
### mm_button button
# (window, hover) The style that is used on buttons that are
# part of the main menu.
### mm_button_text button_text
# (text, hover) The style that is used for the labels of
# buttons that are part of the main menu.
if library.main_menu_positions:
kwargs = library.main_menu_positions.get(text, { })
else:
kwargs = { }
if not eval(enabled):
clicked = None
disabled = True
else:
disabled = False
_button_factory(text, "mm", clicked=clicked, disabled=disabled, **kwargs)
ui.close()
ui.close()
store._result = ui.interact(suppress_overlay = True,
suppress_underlay = True,
mouse="mainmenu")
# Computed jump to the appropriate label.
jump _main_menu
+435 -59
View File
@@ -4,23 +4,54 @@
init -450:
python:
# Used to collect the various preferences the system knows
# about.
library.left_preferences = [ ]
library.right_preferences = [ ]
# This is a map from the name of the style that is applied to
# a list of preferences that should be placed into a vbox
# with that style.
library.preferences = { }
# This is a map from preference name to that preference
# object, that can be used in rearranging preferences.
library.all_preferences = { }
# If true, the preference choices will be arranged in an
# hbox.
library.hbox_pref_choices = False
def _prefs_screen_run(prefs_map):
_game_nav("prefs")
### prefs_window default
# (window) A window containing all preferences.
ui.window(style='prefs_window')
ui.fixed()
for style, prefs in prefs_map.iteritems():
ui.vbox(style=style)
for i in prefs:
i.render_preference()
ui.close()
ui.close()
_game_interact()
class _Preference(object):
"""
This is a class that's used to represent a preference that
may be shown to the user.
This is a class that's used to represent a multiple-choice
preference.
"""
def __init__(self, name, field, values):
def __init__(self, name, field, values, base=_preferences):
"""
@param name: The name of this preference. It will be
displayed to the user.
@param variable: The field on the _preferences object
@param variable: The field on the base object
that will be assigned the selected value. This field
must exist.
@@ -33,11 +64,18 @@ init -450:
conditions are true, this preference will not be
displayed to the user. A condition of None is always
considered to be True.
@param base: The base object on which the variable is
read from and set. This defaults to _preferences,
the user preferences object.
"""
self.name = name
self.field = field
self.values = values
self.base = base
library.all_preferences[name] = self
def render_preference(self):
values = [ (name, val) for name, val, cond in self.values
@@ -46,24 +84,297 @@ init -450:
if not values:
return
### prefs_pref default
# (window) A window containing an individual
# preference.
### prefs_pref_vbox thin_vbox
# (box) The style of the vbox containing a preference.
### prefs_label default
# (text) The style that is applied to the label of
# a block of preferences.
ui.window(style='prefs_pref')
ui.vbox(style='prefs_pref')
ui.vbox(style='prefs_pref_vbox')
_label_factory(self.name, "prefs")
cur = getattr(_preferences, self.field)
cur = getattr(self.base, self.field)
### prefs_hbox default
# If library.hbox_pref_choices is True, the style
# of the hbox containing the choices.
if library.hbox_pref_choices:
ui.hbox(style='prefs_hbox')
for name, value in values:
def clicked(value=value):
setattr(_preferences, self.field, value)
return True
### prefs_button button
# (window, hover) The style of an unselected preferences
# button.
### prefs_button_text button_text
# (text, hover) The style of the text of an unselected
# preferences button.
### prefs_selected_button selected_button
# (window, hover) The style of a selected preferences
# button.
### prefs_selected_button_text selected_button_text
# (text, hover) The style of the text of a selected
# preferences button.
def clicked(value=value):
setattr(self.base, self.field, value)
return True
_button_factory(name, "prefs",
selected=cur==value,
clicked=clicked)
if library.hbox_pref_choices:
ui.close()
ui.close()
class _VolumePreference(object):
"""
This represents a preference that controls one of the
volumes in the system. It is represented as a slider bar,
and a button that can be pushed to play a sample sound on
a channel.
"""
def __init__(self, name, mixer, enable='True', sound='None', channel=0):
"""
@param name: The name of this preference, as shown to the user.
@param mixer: The mixer this preference controls.
@param enable: A string giving a python expression. If
the expression is evaluates to false, this preference
is not shown.
@param sound: A string that is evaluated to yield
another string. The yielded string is expected to give
a sound file, which is played as the sample sound. (We
apologize for the convolution of this.)
@param channel: The number of the channel the sample
sound is played on.
"""
self.name = name
self.mixer = mixer
self.enable = enable
self.sound = sound
self.channel = channel
library.all_preferences[name] = self
def render_preference(self):
if not eval(self.enable):
return
sound = eval(self.sound)
ui.window(style='prefs_pref')
ui.vbox(style='prefs_pref_vbox')
_label_factory(self.name, "prefs")
def changed(v):
if v == 0:
_preferences.mute[self.mixer] = True
else:
_preferences.mute[self.mixer] = False
v /= 128.0
_preferences.volumes[self.mixer] = v
### prefs_volume_slider prefs_slider
# (bar) The style that is applied to volume
# sliders.
ui.bar(128,
int(_preferences.volumes[self.mixer] * 128),
changed=changed,
style='prefs_volume_slider')
if sound:
def clicked():
renpy.sound.play(sound, channel=self.channel)
_button_factory("Test", "prefs", clicked=clicked)
ui.close()
class _SliderPreference(object):
"""
A class that represents a preference that is controlled by a
slider.
"""
def __init__(self, name, range, get, set, enable='True'):
"""
@param set: The name of this preference, that is shown to the user.
@param range: An integer giving the maximum value of
this slider. The slider goes from 0 to range.
@param get: A function that's called to get the
initial value of the slider. It's called with no
arguments, and should return an integet between 0 and
range, inclusive.
@param set: A function that's called when the value of
the slider is set by the user. It is called with a
single integer, in the range 0 to range, inclusive.
@param enable: A string giving a python expression. If
the expression is evaluates to false, this preference
is not shown.
"""
self.name = name
self.range = range
self.get = get
self.set = set
self.enable = enable
library.all_preferences[name] = self
def render_preference(self):
if not eval(self.enable):
return
ui.window(style='prefs_pref')
ui.vbox(style='prefs_pref_vbox')
_label_factory(self.name, "prefs")
def changed(v):
self.set(v)
### prefs_slider bar
# (bar) The style that is applied to preference
# sliders.
ui.bar(self.range,
self.get(),
changed=changed,
style='prefs_slider')
ui.close()
class _PreferenceSpinner(object):
"""
This is a class that's used to represent a preference
spinner, which is a preference that can be incremented
and decremented, when shown to the user.
"""
def __init__(self, name, field, minimum, maximum, delta,
cond = "True", render = lambda x : str(x),
base=_preferences):
"""
@param name: The name of this preference, that is presented
to the user.
@param field: The name of the field on the base object
that is updated by this spinner.
@param minimum: The minimum value that this spinner can set
the value to.
@param maximum: The maximum value that this spinner can set
the value to.
@param delta: The delta by which this spinner is
incremented or decremented.
@param cond: If this condition is not true, this spinner is
not shown.
@param render: This function is called with the value of
the field, and is expected to render that value to a
string.
@param base: The base object that this spinner updates
the field on. It defaults to _preferences, the preferences
object.
"""
self.name = name
self.field = field
self.minimum = minimum
self.maximum = maximum
self.delta = delta
self.cond = cond
self.render = render
self.base = base
library.all_preferences[name] = self
def render_preference(self):
### prefs_spinner default
# The position of the prefs spinner.
### prefs_spinner_label prefs_label
# (text) This is the style that displays the value of a
# preference spinner.
### prefs_spinner_button prefs_button
# (window, hover) The style of the + or - buttons in a
# preference spinner.
### prefs_spinner_button_text prefs_button_text
# (text, hover) The style of the text of the + and - buttons
# in a preference spinner.
if not renpy.eval(self.cond):
return
ui.window(style='prefs_pref')
ui.vbox(style='prefs_pref_vbox')
_label_factory(self.name, "prefs")
cur = getattr(self.base, self.field)
def minus_clicked():
value = cur - self.delta
value = max(self.minimum, value)
setattr(self.base, self.field, value)
return True
def plus_clicked():
value = cur + self.delta
value = min(self.maximum, value)
setattr(self.base, self.field, value)
return True
ui.hbox(style='prefs_spinner')
_button_factory("-", "prefs_spinner", clicked=minus_clicked)
_label_factory(self.render(cur), "prefs_spinner")
_button_factory("+", "prefs_spinner", clicked=plus_clicked)
ui.close()
ui.close()
python hide:
@@ -71,71 +382,136 @@ init -450:
# Enablers for some preferences.
library.has_music = True
library.has_sound = True
library.sample_sound = None
library.has_transitions = True
library.has_cps = True
library.has_afm = True
library.has_skipping = True
library.has_skip_after_choice = True
# Left
p1 = _Preference('Display', 'fullscreen', [
pl1 = _Preference('Display', 'fullscreen', [
('Window', False, None),
('Fullscreen', True, None),
])
p2 = _Preference('Music', 'music', [
('Enabled', True, 'library.has_music'),
('Disabled', False, 'library.has_music'),
])
p3 = _Preference('Sound Effects', 'sound', [
('Enabled', True, 'library.has_sound'),
('Disabled', False, 'library.has_sound'),
])
library.left_preferences = [ p1, p2, p3 ]
p4 = _Preference('TAB and CTRL Skip', 'skip_unseen', [
('Seen Messages', False, 'config.allow_skipping'),
('All Messages', True, 'config.allow_skipping'),
])
p5 = _Preference('Transitions', 'transitions', [
pl2 = _Preference('Transitions', 'transitions', [
('All', 2, 'library.has_transitions'),
('Some', 1, 'library.has_transitions and default_transition'),
('None', 0, 'library.has_transitions'),
])
p6 = _Preference('Text Display', 'fast_text', [
('Fast', True, 'config.annoying_text_cps'),
('Slow', False, 'config.annoying_text_cps'),
])
library.right_preferences = [ p4, p5, p6 ]
# Center
pc1 = _Preference('TAB and CTRL Skip', 'skip_unseen', [
('Seen Messages', False, 'config.allow_skipping and library.has_skipping'),
('All Messages', True, 'config.allow_skipping and library.has_skipping'),
])
pc2= _Preference('After Choices', 'skip_after_choices', [
('Stop Skipping', False, 'config.allow_skipping and library.has_skip_after_choice'),
('Continue Skipping', True, 'config.allow_skipping and library.has_skip_after_choice'),
])
def cps_render(n):
if n == 0:
return "Infinite"
else:
return str(n)
def cps_get():
cps = _preferences.text_cps
if cps == 0:
cps = 100
else:
cps -= 1
return cps
def cps_set(cps):
cps += 1
if cps == 101:
cps = 0
_preferences.text_cps = cps
pc3 = _SliderPreference('Text Speed', 100, cps_get, cps_set,
'library.has_cps')
def afm_get():
afm = _preferences.afm_time
if afm == 0:
afm = 40
else:
afm -= 1
return afm
def afm_set(afm):
afm += 1
if afm == 41:
afm = 0
_preferences.afm_time = afm
pc4 = _SliderPreference('Auto-Forward Time', 40, afm_get, afm_set,
'library.has_afm')
# Right
pr1 = _VolumePreference("Music Volume", 'music', 'library.has_music')
pr2 = _VolumePreference("Sound Volume", 'sfx', 'library.has_sound', 'library.sample_sound')
# pr1 = _Preference('Music', 'music', [
# ('Enabled', True, 'library.has_music'),
# ('Disabled', False, 'library.has_music'),
# ])
# pr2 = _Preference('Sound Effects', 'sound', [
# ('Enabled', True, 'library.has_sound'),
# ('Disabled', False, 'library.has_sound'),
# ])
### prefs_column default
# The style of a vbox containing a column of preferences.
### prefs_left prefs_column
# The position of the left column of preferences.
### prefs_center prefs_column
# The position of the center column of preferences.
### prefs_right prefs_column
# The position of the right column of preferences.
library.preferences['prefs_left'] = [
library.all_preferences['Display'],
library.all_preferences['Transitions'],
]
library.preferences['prefs_center'] = [
library.all_preferences['TAB and CTRL Skip'],
library.all_preferences['After Choices'],
library.all_preferences['Text Speed'],
library.all_preferences['Auto-Forward Time'],
]
library.preferences['prefs_right'] = [
library.all_preferences['Music Volume'],
library.all_preferences['Sound Volume'],
]
label _prefs_screen:
python hide:
_game_nav("prefs")
ui.window(style='prefs_window')
ui.grid(2, 1, xfill=True)
ui.vbox(library.padding * 3, xpos=0.5, xanchor='center')
for i in library.left_preferences:
i.render_preference()
ui.close()
ui.vbox(library.padding * 3, xpos=0.5, xanchor='center')
for i in library.right_preferences:
i.render_preference()
ui.close()
ui.close()
_game_interact()
$ _prefs_screen_run(library.preferences)
jump _prefs_screen
Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

+224 -218
View File
@@ -17,9 +17,122 @@
init -1000:
python hide:
# Style Declarations #################################################
style.create('default', None,
'The default style that all styles inherit from.')
style.create('hbox', 'default', '(box) The base style for hboxen.')
style.create('vbox', 'default', '(box) The base style for vboxen.')
style.create('window', 'default', '(window) The base style for the windows that contain dialogue, thoughts, and menus.')
style.create('image_placement', 'default', 'This style is used to control the default placement of images on the screen.')
# say
style.create('say_label', 'default', '(text) The style that is used by default for the label of dialogue. The label is used to indicate who is saying something.')
style.create('say_dialogue', 'default', "(text) The style that is used by default for the text of dialogue.")
style.create('say_thought', 'default', "(text) The label that is used by default for the text of thoughts or narration, when no speaker is given.""")
style.create('say_window', 'window', '(window) The default style for windows containing dialogue and thoughts.')
# menu
style.create('menu', 'default', "(position) The style that is used for the vbox containing a menu.")
style.create('menu_caption', 'default', "(text) The style that is used to render a menu caption.")
style.create('menu_choice', 'default', "(text, hover) The style that is used to render the text of a menu choice.""")
style.create('menu_choice_button', 'default', "(window, hover, sound) The style that is used to render the button containing a menu choice.")
style.create('menu_window', 'window', '(window) The default style for windows containing a menu.')
# input
style.create('input_text', 'default', '(text) The style used for the text of an input box.')
style.create('input_prompt', 'default', '(text) The style used for the prompt of an input box.')
style.create('input_window', 'window', '(window) The style used for the window of an input box.')
# centered
style.create('centered_window', 'default', '(window) The style that is used for a "window" containing centered text.')
style.create('centered_text', 'default', '(text) The style used for centered text.')
# imagemap
style.create('imagemap', 'image_placement', 'The style that is used for imagemaps.')
style.create('imagemap_button', 'default', '(window, sound, hover) The style that is used for buttons inside imagemaps.')
# imagebutton
style.create('image_button', 'default', '(window, sound, hover) The default style used for image buttons.')
style.create('image_button_image', 'default', 'The default style used for images inside image buttons.')
# button
style.create('button', 'default', '(window, sound, hover) The default style used for buttons in the main and game menus.')
style.create('button_text', 'default', '(text, hover) The default style used for the label of a button.')
style.create('selected_button', 'button', '(window, hover) The style that is used for a selected button (for example, the active screen or a chosen preference).')
style.create('selected_button_text', 'button_text', '(text, hover) The style that is used for the label of a selected button.')
# bar
style.create('bar', 'default', '(bar) The style that is used by default for bars.')
# boxen used by the various menus.
style.create('thin_hbox', 'hbox', '(box) A hbox with a small amount of spacing.')
style.create('thick_hbox', 'hbox', '(box) A hbox with a large amount of spacing.')
style.create('thin_vbox', 'vbox', '(box) A vbox with a small amount of spacing.')
style.create('thick_vbox', 'vbox', '(box) A vbox with a large amount of spacing.')
# AUTOMATICALLY GENERATED
style.create("gm_root_window", "default", "(window) The style used for the root window of the game menu. This is primarily used to change the background of the game menu.")
style.create("gm_nav_window", "default", "(window) The style used by a window containing buttons that allow the user to navigate through the different screens of the game menu.")
style.create("gm_nav_vbox", "thin_vbox", "(box) The style that is used by the box inside the gm_nav_window")
style.create("gm_nav_button", "button", "(window, hover) The style of an unselected game menu navigation button.")
style.create("gm_nav_button_text", "button_text", "(text, hover) The style of the text of an unselected game menu navigation button.")
style.create("gm_nav_selected_button", "selected_button", "(window, hover) The style of a selected game menu navigation button.")
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("file_picker_entry", "button", "(window, hover) The style that is used for each of the slots in the file picker.")
style.create("file_picker_entry_box", "thin_hbox", "(box) The style that is used for the hbox inside of a file picker entry.")
style.create("file_picker_text", "default", "(text) A base style for all text that is displayed in the file picker.")
style.create("file_picker_new", "file_picker_text", "(text) The style that is applied to the number of the new slot in the file picker.")
style.create("file_picker_old", "file_picker_text", "(text) The style that is applied to the number of the old slot in the file picker.")
style.create("file_picker_extra_info", "file_picker_text", "(text) The style that is applied to extra info in the file picker. The extra info is the save time, and the save_name if one exists.")
style.create("file_picker_empty_slot", "file_picker_text", "(text) The style that is used for the empty slot indicator in the file picker.")
style.create("file_picker_window", "default", "(window) A window containing the file picker that is used to choose slots for loading and saving.")
style.create("file_picker_window_vbox", "thin_vbox", "(box) The vbox containing both the nav and the grid in the file picker.")
style.create("file_picker_navbox", "thick_hbox", "(box) The box containing the naviation (next/previous) buttons in the file picker.")
style.create("file_picker_nav_button", "button", "(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_grid", "default", "The style of the grid containing the file picker entries.")
style.create("yesno_window", "default", "(window) The style of a window containing a yes/no prompt.")
style.create("yesno_window_vbox", "thick_vbox", "(box) The style of a box containing the widgets in a yes/no prompt.")
style.create("yesno_label", "default", "(text) The style used for the prompt in a yes/no dialog.")
style.create("yesno_button", "button", "(window, hover) The style of yes/no buttons.")
style.create("yesno_button_text", "button_text", "(window, hover) The style of yes/no button text.")
style.create("error_window", "default", "(window) The style of the window containing internal error messages.")
style.create("error_title", "default", "(text) The style of the text containing the title of an error message.")
style.create("error_body", "default", "(text) The style of the body of an error message.")
style.create("skip_indicator", "default", "(text) The style and placement of the skip indicator.")
style.create("mm_root_window", "default", "(window) The style used for the root window of the main menu. This is primarily used to set a background for the main menu.")
style.create("mm_menu_window", "default", "(window) A window that contains the choices in the main menu. Change this to change the placement of these choices on the main menu screen.")
style.create("mm_menu_window_vbox", "thin_vbox", "(box) The vbox containing the main menu choices.")
style.create("mm_button", "button", "(window, hover) The style that is used on buttons that are part of the main menu.")
style.create("mm_button_text", "button_text", "(text, hover) The style that is used for the labels of buttons that are part of the main menu.")
style.create("prefs_pref", "default", "(window) A window containing an individual preference.")
style.create("prefs_pref_vbox", "thin_vbox", "(box) The style of the vbox containing a preference.")
style.create("prefs_label", "default", "(text) The style that is applied to the label of a block of preferences.")
style.create("prefs_hbox", "default", "If library.hbox_pref_choices is True, the style of the hbox containing the choices.")
style.create("prefs_button", "button", "(window, hover) The style of an unselected preferences button.")
style.create("prefs_button_text", "button_text", "(text, hover) The style of the text of an unselected preferences button.")
style.create("prefs_selected_button", "selected_button", "(window, hover) The style of a selected preferences button.")
style.create("prefs_selected_button_text", "selected_button_text", "(text, hover) The style of the text of a selected preferences button.")
style.create("prefs_volume_slider", "prefs_slider", "(bar) The style that is applied to volume sliders.")
style.create("prefs_slider", "bar", "(bar) The style that is applied to preference sliders.")
style.create("prefs_spinner", "default", "The position of the prefs spinner.")
style.create("prefs_spinner_label", "prefs_label", "(text) This is the style that displays the value of a preference spinner.")
style.create("prefs_spinner_button", "prefs_button", "(window, hover) The style of the + or - buttons in a preference spinner.")
style.create("prefs_spinner_button_text", "prefs_button_text", "(text, hover) The style of the text of the + and - buttons in a preference spinner.")
style.create("prefs_column", "default", "The style of a vbox containing a column of preferences.")
style.create("prefs_left", "prefs_column", "The position of the left column of preferences.")
style.create("prefs_center", "prefs_column", "The position of the center column of preferences.")
style.create("prefs_right", "prefs_column", "The position of the right column of preferences.")
style.create("prefs_window", "default", "(window) A window containing all preferences.")
# END AUTOMATICALLY GENERATED
# Colors #############################################################
dark_cyan = (0, 192, 255, 255)
bright_cyan = (0, 255, 255, 255)
@@ -28,6 +141,8 @@ init -1000:
green = (0, 128, 0, 255)
# The Default Style ###################################################
# Magic.
style.default.enable_hover = True
@@ -40,7 +155,7 @@ init -1000:
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.drop_shadow_color = (0, 0, 0, 255)
style.default.minwidth = 0
style.default.textalign = 0
style.default.text_y_fudge = 0
@@ -64,14 +179,29 @@ init -1000:
style.default.ypos = 0
style.default.xanchor = 'left'
style.default.yanchor = 'top'
style.default.xmaximum = None
style.default.ymaximum = None
# Sound properties.
style.default.hover_sound = None
style.default.activate_sound = None
style.default.sound = None
# The base style for the large windows.
style.create('window', 'default',
'(window, placement) The base style for the windows that contain dialogue, thoughts, and menus.')
# Box properties.
style.default.box_spacing = 0
style.default.box_layout = None
######################################################################
# The style of various boxes.
style.hbox.box_layout = 'horizontal'
style.vbox.box_layout = 'vertical'
style.thin_hbox.box_spacing = 3
style.thick_hbox.box_spacing = 30
style.thin_vbox.box_spacing = 0
style.thick_vbox.box_spacing = 30
######################################################################
# Windows.
style.window.background = Solid((0, 0, 128, 128))
style.window.xpadding = 10
@@ -88,83 +218,33 @@ init -1000:
style.window.xanchor = 'center'
style.window.yanchor = 'bottom'
# This style controls the default placement of images on the screen.
style.create('image_placement', 'default',
'This style is used to control the default placement of images on the screen.')
######################################################################
# Image placement.
style.image_placement.xpos = 0.5
style.image_placement.ypos = 1.0
style.image_placement.xanchor = 'center'
style.image_placement.yanchor = 'bottom'
# Styles that are used for dialogue.
style.create('say_label', 'default',
"""(text) The style that is used by default for
the label of dialogue. The label is used to
indicate who is saying something.""")
######################################################################
# Dialogue
style.say_label.bold = True
style.create('say_dialogue', 'default',
"""(text) The style that is used by default for
the text of dialogue.""")
style.create('say_thought', 'default',
"""(text) The label that is used by default for
the text of thoughts or narration, when no
speaker is given.""")
style.create('say_window', 'window',
'(window, position) The default style for windows containing dialogue and thoughts.')
# Styles that are used for menus.
style.create('menu', 'default',
"(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, 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
style.menu_choice.idle_color = (0, 255, 255, 255) # cyan
style.create('menu_window', 'window',
'(window, position) The default style for windows containing a menu.')
# Styles that are used by input widgets.
style.create('input_text', 'default',
'(text) The style used for the text of an input box.')
style.input_text.color = (255, 255, 0, 255)
style.create('input_prompt', 'default',
'(text) The style used for the prompt of an input box.')
style.create('input_window', 'window',
'(window, position) The style used for the window of an input box.')
# Styles used by centered.
style.create('centered_window', 'default',
'(window) The style that is used for a "window" containing centered text.')
style.create('centered_text', 'default',
'(text) The style used for centered text.')
style.centered_window.xpos = 0.5
style.centered_window.xanchor = 'center'
style.centered_window.xfill = False
style.centered_window.xfill = False
style.centered_window.ypos = 0.5
style.centered_window.yanchor = 'center'
style.centered_window.yfill = False
style.centered_window.xpadding = 10
style.centered_text.textalign = 0.5
@@ -173,249 +253,175 @@ init -1000:
style.centered_text.xanchor = 'center'
style.centered_text.yanchor = 'center'
# Styles that are used by imagemaps
style.create('imagemap', 'image_placement',
'(sound, position) The style that is used for imagemaps.')
# Style that is used by imagebutttons.
style.create('image_button', 'default',
'(window, sound, hover) The default style used for image buttons.')
style.create('image_button_image', 'default',
'The default style used for images inside image buttons.')
# Styles that are used by all other Buttons.
style.create('button', 'default',
'(window, sound, hover) The default style used for buttons in the main and game menus.')
######################################################################
# Buttons.
style.button.xpos = 0.5
style.button.xanchor = 'center'
style.create('button_text', 'default',
'(text, hover) The default style used for the label of a button.')
style.button_text.xpos = 0.5
style.button_text.xanchor = 'center'
style.button_text.size = 24
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.
style.create('selected_button', 'button',
'(window, hover) The style that is used for a selected button (for example, the active screen or a chosen preference).')
style.create('selected_button_text', 'button_text',
'(text, hover) The style that is used for the label of a selected button.')
style.selected_button_text.color = dark_red
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
style.bar.thumb = None
style.bar.thumb_offset = 0
style.bar.thumb_shadow = None
# Styles that are used when laying out the main menu.
style.create('mm_root_window', 'default',
'(window) The style used for the root window of the main menu. This is primarily used to set a background for the main menu.')
######################################################################
# Main menu.
style.mm_root_window.background = Solid((0, 0, 0, 255))
style.create('mm_menu_window', 'default',
'(window, position) A window that contains the choices in the main menu. Change this to change the placement of these choices on the main menu screen.')
style.mm_root_window.xfill = True
style.mm_root_window.yfill = True
style.mm_menu_window.xpos = 0.9
style.mm_menu_window.xanchor = 'right'
style.mm_menu_window.ypos = 0.9
style.mm_menu_window.yanchor = 'bottom'
style.create('mm_button', 'button',
'(window, hover) The style that is used on buttons that are part of the main menu.')
style.create('mm_button_text', 'button_text',
'(text, hover) The style that is used for the labels of buttons that are part of the main menu.')
# Styles that are used to lay out the game menu.
style.create('gm_root_window', 'default',
'(window) The style used for the root window of the game menu. This is primarily used to change the background of the game menu.')
######################################################################
# Game menu common.
style.gm_root_window.background = Solid((0, 0, 0, 255))
style.gm_root_window.xfill = True
style.gm_root_window.yfill = True
style.create('gm_nav_window', 'default',
'(window, position) The style used by a window containing buttons that allow the user to navigate through the different screens of the game menu.')
style.gm_nav_window.xpos = 0.9
style.gm_nav_window.xanchor = 'right'
style.gm_nav_window.ypos = 0.95
style.gm_nav_window.yanchor = 'bottom'
style.create('gm_nav_button', 'button',
'(window, hover) The style of an unselected game menu navigation button.')
style.create('gm_nav_button_text', 'button_text',
'(text, hover) The style of the text of an unselected game menu navigation button.')
style.create('gm_nav_selected_button', 'selected_button',
'(window, hover) The style of a selected game menu navigation button.')
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.')
##############################################################################
# File picker.
style.file_picker_window.xpos = 0
style.file_picker_window.xanchor = 'left'
style.file_picker_window.ypos = 0
style.file_picker_window.yanchor = 'top'
style.file_picker_window.xpadding = 5
style.file_picker_navbox.xpos = 10
style.create('file_picker_navbox', 'default',
'(position) The position of the naviation (next/previous) buttons in the file picker.')
style.file_picker_navbox.xmargin = 10
style.create('file_picker_nav_button', 'button',
'(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.file_picker_grid.xfill = True
style.create('file_picker_entry', 'button',
'(window, hover) The style that is used for each of the slots in the file picker.')
style.file_picker_entry.xpadding = 5
style.file_picker_entry.ypadding = 2
style.file_picker_entry.xmargin = 10
style.file_picker_entry.xminimum = 400
style.file_picker_entry.ymargin = 2
style.file_picker_entry.idle_background = Solid((255, 255, 255, 255))
style.file_picker_entry.xmargin = 5
style.file_picker_entry.xfill = True
style.file_picker_entry.ymargin = 2
style.file_picker_entry.background = Solid((255, 255, 255, 255))
style.file_picker_entry.hover_background = Solid((255, 255, 192, 255))
style.file_picker_entry.activate_background = Solid((255, 255, 192, 255))
style.create('file_picker_text', 'default',
'(text) A base style for all text that is displayed in the file picker.')
style.file_picker_text.size = 18
style.file_picker_text.idle_color = dark_cyan
style.file_picker_text.color = dark_cyan
style.file_picker_text.hover_color = bright_cyan
style.create('file_picker_new', 'file_picker_text',
'(text) The style that is applied to the new indicator in the file picker.')
style.create('file_picker_old', 'file_picker_text',
'(text) The style that is applied to the old indicator in the file pciker.')
style.file_picker_new.hover_color = bright_red
style.file_picker_new.activate_color = bright_red
style.file_picker_new.idle_color = dark_red
style.file_picker_new.minwidth = 30
style.file_picker_old.minwidth = 30
style.create('file_picker_extra_info', 'file_picker_text',
'(text) The style that is applied to extra info in the file picker. The extra info is the save time, and the save_name if one exists.')
style.create('file_picker_empty_slot', 'file_picker_text',
'(text) The style that is used for the empty slot indicator in the file picker.')
style.create('yesno_label', 'default',
'(text, position) The style used for the prompt in a yes/no dialog.')
######################################################################
# Yes/No Dialog
style.yesno_label.color = green
style.create('yesno_button', 'button',
'(window, hover) The style of yes/no buttons.')
style.create('yesno_button_text', 'button_text',
'(window, hover) The style of yes/no button text.')
style.create('yesno_window', 'default',
'(window) The style of a window containing a yes/no dialogue.')
style.yesno_label.textalign = 0.5
style.yesno_window.xfill = True
style.yesno_window.yminimum = 0.5
style.yesno_window.xmargin = .1
# Preferences
style.yesno_window_vbox.xpos = 0.5
style.yesno_window_vbox.xanchor = 'center'
style.yesno_window_vbox.ypos = 0.5
style.yesno_window_vbox.yanchor = 'center'
##############################################################################
# Preferences.
style.create('prefs_pref', 'default',
'(window, position) The position of the box containing an individual preference.')
style.prefs_pref.xpos = 0.5
style.prefs_pref.xanchor = 'center'
style.create('prefs_label', 'default',
'(text, position) The style that is applied to the label of a block of preferences.')
style.prefs_pref.bottom_margin = 10
style.prefs_label.xpos = 0.5
style.prefs_label.xanchor = "center"
style.prefs_label.color = green
style.create('prefs_button', 'button',
'(window, hover) The style of an unselected preferences button.')
style.prefs_slider.xmaximum=200
style.prefs_slider.ymaximum=22
style.prefs_slider.xpos = 0.5
style.prefs_slider.xanchor = 'center'
style.prefs_hbox.xpos = 0.5
style.prefs_hbox.xanchor = 'center'
style.prefs_button.xpos = 0.5
style.prefs_button.xanchor = 'center'
style.create('prefs_button_text', 'button_text',
'(text, hover) The style of the text of an unselected preferences button.')
style.create('prefs_selected_button', 'selected_button',
'(window, hover) The style of a selected preferences button.')
style.prefs_selected_button.xpos = 0.5
style.prefs_selected_button.xanchor = 'center'
style.create('prefs_selected_button_text', 'selected_button_text',
'(text, hover) The style of the text of a selected preferences button.')
style.create('prefs_window', 'default',
'(window, position) A window containing all preferences.')
style.prefs_window.xfill=True
style.prefs_window.ypadding = 0.05
style.create('skip_indicator', 'default',
'(text, position) The style of the text that is used to indicate that skipping is in progress.')
style.prefs_column.box_spacing = 6
style.prefs_left.xanchor = 'center'
style.prefs_left.xpos = 1.0 / 6.0
style.prefs_center.xanchor = 'center'
style.prefs_center.xpos = 3.0 / 6.0
style.prefs_right.xanchor = 'center'
style.prefs_right.xpos = 5.0 / 6.0
style.prefs_spinner.xpos = 0.5
style.prefs_spinner.xanchor = 'center'
style.prefs_spinner_label.minwidth = 100
style.prefs_spinner_label.textalign = 0.5
######################################################################
# Skip indicator.
style.skip_indicator.xpos = 10
style.skip_indicator.ypos = 10
######################################################################
# Error messages.
style.error_window.background = Solid((220, 220, 255, 255))
style.error_window.xfill = True
style.error_window.yfill = True
style.error_window.xpadding = 20
style.error_window.ypadding = 20
style.error_title.color = (255, 128, 128, 255)
style.error_body.color = (128, 128, 255, 255)
+20 -21
View File
@@ -2,15 +2,6 @@
import os.path
# Enable psyco. Warning: Check for memory leaks!
try:
if not os.path.exists("nopsyco"):
import psyco
psyco.full()
except ImportError:
pass
import codecs
import optparse
import traceback
@@ -24,37 +15,36 @@ 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
import math
def main():
name = os.path.basename(sys.argv[0])
dirname = os.path.dirname(sys.argv[0])
if dirname:
os.chdir(dirname)
# Add the path to the module.
sys.path.append("module")
name = os.path.basename(sys.argv[0])
if name.find(".") != -1:
name = name[:name.find(".")]
if name.find("_") != -1:
name = name[name.find("_") + 1:]
if os.path.isdir(name):
game = name
else:
game = "game"
op = optparse.OptionParser()
op.add_option('--game', dest='game', default=game,
op.add_option('--game', dest='game', default=name,
help='The directory the game is in.')
op.add_option('--python', dest='python', default=None,
help='Run the argument in the python interpreter.')
op.add_option('--lint', dest='lint', default=False, action='store_true',
help='Run a number of expensive tests, to try to detect errors in the script.')
op.add_option('--leak', dest='leak', action='store_true', default=False,
help='When the game exits, dumps a profile of memory usage.')
@@ -64,8 +54,15 @@ def main():
execfile(options.python)
sys.exit(0)
if not options.lint:
import renpy.display.presplash
renpy.display.presplash.start(options.game)
# Load up all of Ren'Py, in the right order.
import renpy
try:
renpy.main.main(options.game)
renpy.main.main(options.game, lint=options.lint)
except Exception, e:
@@ -121,6 +118,8 @@ def main():
def memory_profile():
import renpy
print "Memory Profile"
print
print "Showing all objects in memory at program termination."
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 60 KiB

BIN
View File
Binary file not shown.
+754 -279
View File
File diff suppressed because it is too large Load Diff
+80 -20
View File
@@ -10,18 +10,30 @@ def match_times(source, dest):
def dosify(s):
return s.replace("\n", "\r\n")
def copy_file(source, dest, license=""):
def copy_file(source, dest, license="", dos=True):
if dest.endswith(".bak"):
return
if dest.endswith("~"):
return
print source, "->", dest
sf = file(source, "rb")
df = file(dest, "wb")
df.write(license)
if dest.endswith(".py") or dest.endswith(".rpy"):
if not dest.endswith("subprocess.py"):
df.write(license)
data = sf.read()
if dest.endswith(".txt") or dest.endswith(".py") or dest.endswith(".rpy") or dest.endswith(".bat"):
data = dosify(data)
if dest.endswith(".txt") or dest.endswith(".py") or dest.endswith(".pyw") or \
dest.endswith(".rpy") or dest.endswith(".bat"):
if dos:
data = dosify(data)
if data.startswith("#!"):
data = data.replace("\r\n", "\n\r\n", 1)
df.write(data)
@@ -34,7 +46,7 @@ def copy_file(source, dest, license=""):
def copy_tree(source, dest, should_copy=lambda fn : True, license=""):
os.makedirs(dest)
for dirpath, dirnames, filenames in os.walk(source):
if "/saves" in dirpath:
@@ -55,13 +67,16 @@ def copy_tree(source, dest, should_copy=lambda fn : True, license=""):
if i == ".svn":
continue
os.mkdir(dstrel + "/" + i)
for i in filenames:
if not should_copy(i):
continue
if i.startswith("."):
continue
copy_file(dirpath + "/" + i, dstrel + "/" + i, license=license)
@@ -74,9 +89,13 @@ def main():
# Read license.
lf = file("LICENSE.txt")
license = "#!/usr/bin/env python\n\n"
license = "#!/usr/bin/env python\n\r\n"
for l in lf:
if l.startswith("---"):
break
license += "# " + l
lf.close()
@@ -99,6 +118,8 @@ def main():
'example.html',
'reference.html',
'style.css',
'RELEASING.txt',
'EXTENDING.txt',
]
# Copy doc
@@ -107,31 +128,70 @@ def main():
# Copy the game
copy_tree(gamedir, target + "/game",
should_copy = lambda fn : not fn.startswith(".") and not fn.endswith("~") and not fn.endswith(".mpg"))
should_copy = lambda fn : not fn.endswith(".mpg"))
copy_tree("common", target + "/common",
should_copy = lambda fn : not fn.startswith(".") and not fn.endswith("~"))
copy_tree("common", target + "/common")
# copy_tree("dse", target + "/dse")
copy_tree("extras", target + "/extras",
should_copy = lambda fn : not fn.startswith(".") and not fn.endswith("~"))
should_copy = lambda fn : not (fn.endswith(".rpyc") or fn.endswith(".rpyb")) )
copy_tree("scripts", target + "/scripts")
def cp(x, license=""):
copy_file(x, target + "/" + x)
copy_tree("tools", target + "/tools", license=license)
def cp(x, license="", dos=True):
copy_file(x, target + "/" + x, dos=dos)
cp("CHANGELOG.txt")
cp("LICENSE.txt")
cp("README_RENPY.txt")
cp("archive_images.bat")
# cp("archive_images.bat")
# cp("lint.bat")
cp("run_game.py", license=license)
copy_file("run_game.py", target + "/run_game.pyw", license=license)
cp("archiver.py", license=license)
# copy_file("run_game.py", target + "/run_game.pyw", license=license)
# copy_file("run_game.py", target + "/run_dse.py", license=license)
# copy_file("run_game.py", target + "/run_dse.pyw", license=license)
# copy_file("run_game.rpyl", target + "/run_game.rpyl")
# copy_file("run_dse.rpyl", target + "/run_dse.rpyl")
# 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")
# cp("add_from.py", license=license)
# cp("dump_text.py", license=license)
# cp("renpy-mode.el")
os.mkdir(target + "/module")
module_files = [
"README.txt",
"_renpy.pyx",
"_renpy.c",
"core.c",
"linmixer.py",
"native_midi.h",
"native_midi_common.c",
"native_midi_common.h",
"native_midi_mac.c",
"native_midi_win32.c",
"nativemidi.c",
"nativemidi.pyx",
"pss.c",
"pss.h",
"pysdlsound.c",
"pysdlsound.pyx",
"rwobject.c",
"renpy.h",
"setup.py",
"setup_mac.py",
"setup_win32.py",
"winmixer.c",
"winmixer.pyx",
]
for i in module_files:
cp("module/" + i)
if __name__ == "__main__":
main()
+98
View File
@@ -0,0 +1,98 @@
================
Extending Ren'Py
================
This document talks a bit about extending Ren'Py using Python
modules. This is fairly advanced stuff, as several things change
between python-in-Ren'Py and python modules. Still, python modules
allow one to extend the very core of Ren'Py itself, which may come in
handy one day. They also let you leave Ren'Py entirely, to write
things like minigames in pygame directly.
It's assumed that if you're reading this, you have a decent amount of
familiarity with programming in Python. If this isn't the case, then
you should probably go and read the documentation on
http://www.python.org/.
Python Modules in Ren'Py
========================
Python modules can be imported from Ren'Py code using the standard
Python import statement. Ren'Py installs an import hook that will
attempt to load python modules from the same places that it searches
for all game data. This includes the game directory, and any archives
listed in config.archives.
It's important to ensure that your modules do not conflict with
standard python modules used by Ren'Py, or with the renpy modules
themselves. This is best done by putting your modules in an unused
package, such as local. Ren'Py respects the Python convention that a
package is a directory with an __init__.py file in it, and extends
this convention into .rpa files when necessary.
There are a number of differences between Python modules loaded by
Ren'Py and python-in-renpy as found in the python blocks used in .rpy
scripts. One of these differences is that the module names used are
fairly different. Some of the commonly used modules have different
from python-in-renpy (and hence the documentation). Some of these
changes are:
* ui is renpy.ui
* anim is renpy.display.anim
* config is renpy.config
* renpy.sound is renpy.audio.sound
* renpy.music is renpy.audio.music
* renpy is renpy.exports
* The top-level namespace is renpy.store
It's important to deal with these name changes when writing python
module code.
A second, and perhaps more important, change is that objects loaded
from python modules do not, by default, participate in
rollback. Because of this, we do not recommend returning dictionaries,
lists, or objects from python module code, but instead we prefer
returning immutable types (such as strings, integers, floats, and
tuples).
If it is necessary to return an object that needs to participate in
rollback, then one must construct objects of the appropriate types
manually. Lists must be of type renpy.python.RevertableList,
dictionaries of type renpy.python.RevertableDict, and object should be
of a subclass of renpy.python.RevertableObject.
Writing Pygame in Ren'Py
========================
It's possible (if perhaps not recommended) to include pygame code in
Ren'Py. One use of this would be to implement mini-games that would
otherwise not fit into the Ren'Py framework. (But it may make more
sense to implement a minigame as a Ren'Py widget.) It can be done by
importing the appropriate pygame modules, and then calling pygame
functions as desired.
Ren'Py initializes the pygame display to be 24 or 32 bits in depth, at
the sizes chosen by screen_width and screen_height. The display is set
to be a software surface. The display can be retrieved with
pygame.display.get_surface().
The pygame mixer is not initialized, and is often not included as part
of Ren'Py. Instead, we recommend using renpy.audio.sound and
renpy.audio.music calls to play sound and music. When playing music,
it is necessary to call renpy.audio.interact() after music is changed,
and renpy.audio.periodic() on a regular basis (Ren'Py does it at 20Hz)
to ensure that track updates occur when appropriate.
On returning to Ren'Py from a pygame interation that redraws the
screen, it is necessary to call
renpy.exports.force_full_redraw(). This will force the screen to be
redrawn completely. If this is not done, Ren'Py may not know about
areas of the screen drawn to directly with pygame.
Implementing Ren'Py Widgets
===========================
(This hasn't been written yet, bug PyTom if you really need it.)
+131
View File
@@ -0,0 +1,131 @@
Releasing Ren'Py Games
PyTom <pytom@bishoujo.us>
Updated: 2005-11-21
Okay, so you've finished creating a game using Ren'Py, and now you
want to release it. Congratulations! This tech note will help you to
package up your creation and send it into the world.
The first thing to do is to check to see if you have the latest
version of Ren'Py. New versions of Ren'Py fix bugs in old versions. If
you're not using the latest version, your game may have known bugs in
it. At the very least, ask about what was fixed between versions if
you insist on using an older version.
The next thing to do is to ensure that you actually have a finished
and working game. Did you remember to run add_from? The easiest way to
do this is to change into the tools/ directory, and run
game_add_from.bat. That tool will add from clauses to all of the call
statements in your program, ensuring that everything continues working
after a game is reloaded. It will also create .bak files for each of
the .rpy files it can find, so you'll probably want to delete those by
hand once you've tested the changes.
You'll also want to run game_lint.bat, also found in the tools/
directory. This is a tool that will check your Ren'Py game for subtle
bugs. If it reports any bugs, you should fix them, and run lint.bat
again until they're all fixed. If you've changed the name of the .exe,
you'll probably need to edit lint.bat so that it runs. In general, you
don't want to release a game with lint errors, or at least a good
excuse for said errors. Many of the problems that are found this way
are fairly subtle, and only manifest themselves on some of the
platforms Ren'Py supports.
If you're not totally sure that everything is A-OK, you can send me
the game and I will check it over in confidence. If there's enough
demand, I'll put together a tool that will elide the game text, so
just the code and structure is left.
I also ask that you add a Ren'Py credit into your game. I tend to use
the line "Powered by Ren'Py.", but feel free to word it however you
want.
Another thing to do is to change the icon of the exe file. The best
way to do this is to create a .ico file containing 48x48, 32x32, and
16x16 versions of your icon, and then to use XN Resource Explorer to
go in and change the icon of the file. Just delete off the existing
icon, and then do a command like "Import image resource..." (going
from memory here) to add in the new icon. Save the modified
executable.
Now, we have everything that goes into the final distribution. In
fact, we have more than enough, as Ren'Py ships with a number of tools
that aren't needed to run a game. So we need to decide which files to
keep in various circumstances.
There are basically three different kinds of distributions you can
make. The first is a windows executable distribution (abbreviated
windows), that allows the user to run your game without having to
download any additional software. The second kind is a
python-dependent (python) distribution, which can run on any computer
that supports python 2.3 and pygame. The final type of distribution
that is supported is a cross platform distribution, which can run both
on windows and on any computer that supports python.
To make a windows distribution, you need the files that I'll mark
windows or all. To make a python distribution, you need the files that
I'll mark python and all. And to make a cross-platform distribution,
you'll need all the indicated files.
Anyway, the list of files that you may concievably want to include as
part of your game are:
common/ [all] - A directory containing common script files that help
Ren'Py work.
lib/ [windows] - A directory containing the code and libraries needed
to run Ren'Py on windows.
game/ [all] - The game directory containing your scripts and
data. You probably do not want to distribute the saves/ directory
inside here, so remove that. Be sure not to distribute
game/saves/persistent, and be warned that it is recreated each time
Ren'Py is run.
run_game.exe [windows] - The game executable that the user runs.
moonlight.py [python] - The main python script that runs your game.
python23.dll [windows] - The DLL containing the python interpreter. It
may change versions in the future, in which case the numbers will
change.
renpy/ [python] - Contains the python source code for Ren'Py.
module/ [python] - Contains the source code for the _renpy module.
LICENSE.txt [all] - The license for Ren'Py. You may want to integrate
this into the license for your game as a whole.
README_RENPY.txt [all] - Instructions for running a Ren'Py game. You
may want to integrate this into your own README.
Anyway, once you have all of the files, you can zip them up (perhaps
adding in things like README and license files) and post them on the
web. You've just released a Ren'Py game!
For the record, the following are the files distributed with Ren'Py
that should not be distributed with a game.
CHANGELOG.txt - The Ren'Py changelog.
console.exe - A console/debugging version of Ren'Py.
extras - Scripts that give additional features. Uninteresting as a
whole, as to be used the scripts must be in the game/ directory.
scripts - Example scripts.
tools - Development tools.
game/saves - This is the directory containing the savegame info. It
should be deleted before distribution, but please note that running
Ren'Py recreates it automatically. It contains a file named persistent
that stores the list of seen lines. Shipping a persistent file
may lead to the option to skip seen text skipping text not seen by
the end-user.
+20 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/python
import os
import re
import sys
import time
@@ -7,8 +8,10 @@ import time
import inspect
sys.path.append('..')
import renpy
sys.path.append('../module')
import renpy
renpy.import_all()
keywords = [
r'\bimage\b',
@@ -103,6 +106,9 @@ def function(m):
else:
args = inspect.formatargspec(*inspect.getargspec(func))
args = re.sub(r"'\\x00\\x01.*?\\xff'", "im.ramp(0, 255)", args)
args = re.sub(r'<.*?>', '...', args)
docparas = []
@@ -124,6 +130,9 @@ def include(m):
f.close()
return rv
class ret_none(object):
def __getattr__(self, k):
return None
def main():
@@ -132,6 +141,16 @@ def main():
s = f.read()
f.close()
os.chdir("..")
try:
renpy.game.options = ret_none()
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)
+3084 -2015
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -50,6 +50,21 @@
</ul>
</xsl:template>
<xsl:template match="defnindex">
<ul>
<xsl:for-each select="//defn">
<xsl:sort select="@name" />
<li>
<a>
<xsl:attribute name="href">#<xsl:value-of select="@name" /></xsl:attribute>
<xsl:value-of select="@name"/>
(<xsl:value-of select="@type"/>)
</a>
</li>
</xsl:for-each>
</ul>
</xsl:template>
<xsl:template match="varindex">
<ul>
<xsl:for-each select="//var">
@@ -161,5 +176,14 @@
<xsl:apply-templates />
</dd>
</xsl:template>
<xsl:template match="defn">
<a><xsl:attribute name="name"><xsl:value-of select="@name" /></xsl:attribute></a>
<dt class="var"><b><xsl:value-of select="@name" /></b></dt>
<dd>
<xsl:apply-templates />
</dd>
</xsl:template>
</xsl:stylesheet>
+5 -3
View File
@@ -1,7 +1,9 @@
init:
image black = Solid((0, 0, 0, 255))
label start:
label main_menu:
$ style._write_docs("doc/styles.xml")
"Dumped style."
$ renpy.renpy.style.write_docs("doc/styles.xml")
$ renpy.renpy.style.write_hierarchy("doc/style_heirarchy.xml")
$ raise "foo"
+1 -6
View File
@@ -11,11 +11,6 @@ init 1:
$ style.button_text.size = 20
$ style.file_picker_text.size = 14
# Perhaps change the fudge factor on windows, if line spacing
# looks weird.
if renpy.windows():
$ style.default.line_height_fudge = -4
$ style.file_picker_entry.xminimum = 320
# Change the size of the thumbnails in the file picker.
$ library.thumbnail_width = 60
$ library.thumbnail_height = 45
+62
View File
@@ -0,0 +1,62 @@
# This file contains code to do chinese word-wrapping. It's based
# on what I gathered from the information found at:
#
# http://lemmasoft.renai.us/forums/viewtopic.php?p=9282#9282
init:
# You'll probably want to replace this with whatever you consider
# to be punctuation. I've used unicode escapes for many of the
# fullwidth characters, as I don't know how to type them otherwise.
$ chinese_punctuation = u";,.?\uff1b\uff1a\uff0c\u3002\u3001\uff1f\uff01"
python hide:
def text_tokenizer(s, style):
"""
This functions is used to tokenize text. It's called when laying
out a Text widget, and is given the string that is the text of the
widget, and the style associated with the widget.
It's expected to yield some number of pairs. In each pair, the
first element is the kind of token found, and the second element
is the text corresponding to that token. The following token
types are defined:
"newline" -- A newline, which when encountered starts a new line.
"word" -- A word of text. A line will never be broken inside of
a word.
"space" -- A space. Spaces are always placed on the current line,
and will never be placed as the start of a line.
"tag" -- A text tag. If encountered, the second element should be
the name of the tag, without any enclosing braces.
"""
regexp = r"""(?x)
(?P<space>\ )
| \{(?P<tag>[^{}]+)\}
| (?P<untag>\{\{)
| (?P<newline>\n)
| (?P<word>[^ \n\{][%s]*)
""" % chinese_punctuation
import re
for m in re.finditer(regexp, s):
if m.group('space'):
yield 'space', m.group('space')
elif m.group('word'):
yield 'word', m.group('word')
elif m.group('tag'):
yield 'tag', m.group('tag')
elif m.group('untag'):
yield 'word', '{'
elif m.group('newline'):
yield 'newline', m.group('newline')
config.text_tokenizer = text_tokenizer
+40
View File
@@ -0,0 +1,40 @@
# This function implements a commentary track, which is a special kind of
# say statement that only displays if commentary is set to on in the
# new preference that this mode creates. Making this unlockable is easy,
# but is left as an exercise for the reader.
#
# Use it by putting in lines like:
#
# commentary "Hey! This is commentary!"
init -10:
python:
# The style for the commentary window.
style.create('commentary_window', 'window', 'The commentary window.')
style.commentary_window.background = Solid((0, 0, 0, 192))
style.commentary_window.ypos = 0.0
style.commentary_window.yanchor = 'top'
# Ensure that the commentary attribute exists.
if persistent.commentary is None:
persistent.commentary = False
commentary = Character(None, window_style='commentary_window',
condition = 'persistent.commentary')
python hide:
cp = _Preference('Commentary', 'commentary', [
('Enabled', True, 'True'),
('Disabled', False, 'True')
], base=persistent)
library.preferences['prefs_right'].append(cp)
+83
View File
@@ -0,0 +1,83 @@
# This file contains code to do east asian word-wrapping. It's based
# on what I gathered from the information found at:
#
# http://lemmasoft.renai.us/forums/viewtopic.php?p=9282#9282
#
# It was then further enhanced with the information found at:
#
# http://zoonek.free.fr/LaTeX/Omega-Japanese/doc.html
#
# I'd expect it to do a decent job with Chinese and Japanese, but
# I have no idea how it does with Korean. If someone wants to clue
# me in, I'd be happy to hear more about it.
init:
python hide:
# These are characters for which line breaking is forbidden before them.
# In our algorithm, they try to cling to the back of a word.
not_before = ur'\!\"\%\)\,\-\.\:\;\?\]\}\u2010\u2019\u201d\u2030\u2032\u2033\u2103\u2212\u3001\u3002\u3005\u3009\u300b\u300d\u300f\u3011\u3015\u3017\u3041\u3043\u3045\u3047\u3049\u3063\u3083\u3085\u3087\u308e\u309b\u309c\u309d\u309e\u30a1\u30a3\u30a5\u30a7\u30a9\u30c3\u30e3\u30e5\u30e7\u30ee\u30f5\u30f6\u30fc\u30fd\u30fe\uff01\uff02\uff05\uff09\uff09\uff0c\uff0d\uff0e\uff1a\uff1b\uff1f\uff3d\uff5d\uff5d\uff61\uff63\uff9e\uff9f'
# These are characters for which line breaking is forbidden after them.
# In our algorithm, they try to cling to the front of a word.
not_after = ur'\"\#\$\(\@\[\{\xa2\xa3\xa5\xa7\u2018\u201c\u266f\u3008\u300a\u300c\u300e\u3010\u3012\u3014\u3016\uff03\uff04\uff08\uff08\uff20\uff3b\uff5b\uff5b\uff62\uffe0\uffe1\uffe5'
# These are ranges of characters that are treated as western. (And hence are always grouped
# together as a word.
western = ur'\'\w\u000a-\u0024f\uff10-\uff19\uff20-\uff2a\uff41-\uff5a'
regexp = ur"""(?x)
(?P<space>[ \u200b])
| \{(?P<tag>[^{}]+)\}
| (?P<untag>\{\{)
| (?P<newline>\n)
| (?P<word> [%(not_after)s]*
([^ \n\{\u200b%(not_before)s%(not_after)s%(western)s]|[%(western)s]+)
[%(not_before)s]*
)
""" % locals()
import re
store._cjk_regexp = re.compile(regexp)
def text_tokenizer(s, style):
"""
This functions is used to tokenize text. It's called when laying
out a Text widget, and is given the string that is the text of the
widget, and the style associated with the widget.
It's expected to yield some number of pairs. In each pair, the
first element is the kind of token found, and the second element
is the text corresponding to that token. The following token
types are defined:
"newline" -- A newline, which when encountered starts a new line.
"word" -- A word of text. A line will never be broken inside of
a word.
"space" -- A space. Spaces are always placed on the current line,
and will never be placed as the start of a line.
"tag" -- A text tag. If encountered, the second element should be
the name of the tag, without any enclosing braces.
"""
for m in store._cjk_regexp.finditer(s):
if m.group('space'):
yield 'space', m.group('space')
elif m.group('word'):
yield 'word', m.group('word')
elif m.group('tag'):
yield 'tag', m.group('tag')
elif m.group('untag'):
yield 'word', '{'
elif m.group('newline'):
yield 'newline', m.group('newline')
config.text_tokenizer = text_tokenizer
+262
View File
@@ -0,0 +1,262 @@
# This extra implements an image gallery, complete with automatic
# unlocking of the images that have been shown to the user. The images
# are divided into pages, with a fixed number of images on each page.
# Right now, this is configured to used the images found in the demo.
# The images are repeated quite a bit, so that we can get four pages
# of them. You probably wouldn't do that if you were using this in a
# real game.
# To see this in action, drop it into the game directory of the
# demo.
# Configuration.
init:
python hide:
# The number of columns and rows of images to show in the
# gallery.
store.gallery_cols = 3
store.gallery_rows = 4
# The size that each image should be scaled to, and that
# thumbnails should be.
store.gallery_width = 160
store.gallery_height = 120
# The contents of each of the page. Each of these is a list of
# tuples with the first element of the tuple being the image
# filename and the second element being the name of the image
# that will unlock the image with this filename. (That is,
# the name that is used in show or scene statements, as a
# string.)
#
# When displaying an image as a thumbnail, this code first
# looks for the file thumbnail_<filename>. If that file
# exists, it should be a gallery_width x gallery_height
# thumbnail. Otherwise, a thumbnail is automatically
# generated, but it may screw up the aspect ratio of the
# image.
#
# You probably want to create thumbnails for most images, to
# limit memory consumption.
page1 = [
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
]
page2 = [
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
( "carillon.jpg", "carillon" ),
]
page3 = [
( "whitehouse.jpg", "whitehouse" ),
( "washington.jpg", "washington" ),
]
page4 = [
( "9a_happy.png", "eileen happy" ),
( "9a_vhappy.png", "eileen vhappy" ),
( "9a_concerned.png", "eileen concerned" ),
]
# This is the actual list of gallery pages. It's a list
# of tuples, with the first element being the name of the
# page, the second being the contents of the page (one of
# the lists created above), and the third image being the
# image used as the background of the page.
store.gallery_pages = [
("Backgrounds 1", page1, "washington.jpg"),
("Backgrounds 2", page2, "whitehouse.jpg"),
("Backgrounds 3", page3, "carillon.jpg"),
("Character Art", page4, "washington.jpg"),
]
# A window containing the gallery page buttons.
style.create('gallery_pages', 'default')
style.gallery_pages.xpos = 0.99
style.gallery_pages.xanchor='right'
style.gallery_pages.ypos = 0.02
style.gallery_pages.yanchor = 'top'
# A button that links to a gallery page.
style.create('gallery_page_button', 'button')
style.create('gallery_page_button_text', 'button_text')
# A button that returns us to from whence we came.
style.create('gallery_return_button', 'button')
style.create('gallery_return_button_text', 'button_text')
style.gallery_return_button.xpos = 0.99
style.gallery_return_button.xanchor='right'
style.gallery_return_button.ypos = 0.98
style.gallery_return_button.yanchor = 'bottom'
# The grid containing the gallery image buttons.
style.create('gallery_grid', 'default')
# The style of the buttons in the gallery.
style.create('gallery_button', 'default')
# Right now, the backgrounds are all solids, but in a more
# professional version, the insensitive background would
# probably be a placeholder image that indicates that a
# picture has yet to be unlocked.
style.gallery_button.insensitive_background = Solid((192, 192, 192, 255))
style.gallery_button.idle_background = Solid((255, 255, 255, 255))
style.gallery_button.hover_background = Solid((255, 255, 192, 255))
style.gallery_button.left_margin = 5
style.gallery_button.right_margin = 5
style.gallery_button.top_margin = 5
style.gallery_button.bottom_margin = 5
style.gallery_button.left_padding = 5
style.gallery_button.right_padding = 5
style.gallery_button.top_padding = 5
style.gallery_button.bottom_padding = 5
# The style of the images in the buttons in the gallery.
style.create('gallery_button_image', 'default')
# The style of the images that are being shown to the user.
style.create('gallery_image', 'image_placement')
# The transition used when switching gallery pages.
store.gallery_transition = Dissolve(0.5)
python:
# The function that actually manages the display of the image
# gallery.
def gallery():
page = 0
while True:
images = gallery_pages[page][1]
ui.image(gallery_pages[page][2])
# Show the names of the various gallery pages.
ui.window(style='gallery_pages')
ui.vbox(focus="gallery_pages")
for i in range(0, len(gallery_pages)):
if i == page:
clicked = None
else:
clicked = ui.returns(("page", i))
ui.textbutton(gallery_pages[i][0],
style='gallery_page_button',
text_style='gallery_page_button_text',
clicked=clicked)
ui.close()
# Show the return button.
ui.textbutton('Return',
style='gallery_return_button',
text_style='gallery_return_button_text',
clicked=ui.returns(("return", None)))
# Show the grid for this page.
ui.grid(gallery_cols, gallery_rows, style='gallery_grid')
# For each grid cell.
for i in range(0, gallery_cols * gallery_rows):
# Fill empty space with nulls.
if i >= len(images):
ui.null()
continue
# Otherwise, get the filename and spec and see if
# we've unlocked it.
filename, spec = images[i]
if spec:
spec = spec.split()
if spec and tuple(spec) not in persistent._seen_images:
filename = None
clicked = None
else:
clicked = ui.returns(('show', filename))
# Create the button, containing the appropriate
# image or a null if we haven't unlocked it yet.
ui.button(style='gallery_button', clicked=clicked)
if not filename:
ui.null(width=gallery_width, height=gallery_height)
else:
if renpy.loadable("thumbnail_" + filename):
ui.image("thumbnail_" + filename,
style="gallery_button_image")
else:
ui.add(im.Scale(filename,
gallery_width,
gallery_height))
ui.close()
# Interact with the user.
renpy.transition(gallery_transition)
cmd, arg = ui.interact(suppress_overlay=True, suppress_underlay=True)
# Process the user's commands.
if cmd == "show":
ui.add(Solid((0, 0, 0, 255)))
ui.image(arg)
ui.saybehavior()
renpy.transition(gallery_transition)
ui.interact(suppress_overlay=True, suppress_underlay=True)
if cmd == "page":
page = arg
if cmd == "return":
renpy.transition(gallery_transition)
return
library.main_menu.insert(2, ("CG Gallery", ui.jumps("gallery"), 'True'))
label gallery:
$ gallery()
jump _main_menu
+18 -7
View File
@@ -13,7 +13,7 @@
init -100:
python:
# This is a list of KanaCharacter, line of dialogue tuples.
# This is a list of KanaCharacter, what tuples.
kana_display_list = [ ]
# Spacings used.
@@ -26,17 +26,24 @@ init -100:
def kana_show():
if not kana_display_list:
return
ui.window(style='say_window')
ui.vbox(kana_vspacing)
for char, what in kana_display_list:
char.show(what)
for i, (char, what) in enumerate(kana_display_list):
text = char.show(what, i == len(kana_display_list) - 1)
ui.close()
ui.saybehavior()
# Magic to support auto-forward mode.
b = ui.saybehavior()
b.set_afm_length(text.get_simple_length())
ui.interact()
renpy.checkpoint()
# Characters and the narrator should be instances of this
# object.
@@ -60,14 +67,18 @@ init -100:
kana_display_list.append((self, what))
kana_show()
def show(self, what):
def show(self, what, slow):
ui.hbox(kana_hspacing)
ui.text(self.who, style=self.who_style,
**self.properties)
ui.text(self.what_prefix + what + self.what_suffix,
style=self.what_style)
rv = ui.text(self.what_prefix + what + self.what_suffix,
style=self.what_style, slow=slow)
ui.close()
return rv
# This section updates the styles that are used by the Kana mode stuff
# so that they work well with Kana mode. The user may want to change
+27
View File
@@ -0,0 +1,27 @@
# This file gives an example of how the various entries on the main
# and game menus can be repositioned on the screen. Unlike the other
# extras, we don't intend this to be used as-is, but instead to be
# copied and changed to change the positions of buttons on the screen.
# Buttons should not overlap, and certain configurations may make
# buttons inaccessible with keyboard navigation.
init:
python hide:
library.main_menu_positions = {
'Start Game' : dict(xpos=400, ypos=400, xanchor='left', yanchor='top'),
'Continue Game' : dict(xpos=450, ypos=430, xanchor='left', yanchor='top'),
'Preferences' : dict(xpos=500, ypos=460, xanchor='left', yanchor='top'),
'Quit Game' : dict(xpos=550, ypos=490, xanchor='left', yanchor='top'),
}
library.game_menu_positions = {
'Return' : dict(xpos=400, ypos=400, xanchor='left', yanchor='top'),
'Preferences' : dict(xpos=450, ypos=430, xanchor='left', yanchor='top'),
'Save Game' : dict(xpos=500, ypos=460, xanchor='left', yanchor='top'),
'Load Game' : dict(xpos=550, ypos=490, xanchor='left', yanchor='top'),
'Main Menu' : dict(xpos=600, ypos=520, xanchor='left', yanchor='top'),
'Quit' : dict(xpos=650, ypos=550, xanchor='left', yanchor='top'),
}
+47
View File
@@ -0,0 +1,47 @@
# This file implements quick save and load functionality. The
# quicksave functionality adds a button to the overlay that is
# displayed over the game. When the button is clicked, the game is
# saved in a special quicksave slot. It also adds a quick load button
# to the main menu. When that button is clicked, the quicksave game is
# loaded.
# This is called when the quick save button is clicked.
label _quick_save:
call _enter_menu_without_scene from __call__enter_menu_4
# Save the game.
$ renpy.save('quicksave', _('Quick Save'))
# Tell the user that we saved the game.
$ ui.add(Solid((0, 0, 0, 128)))
$ ui.text(_('The game has been saved using quick save.'),
xpos=0.5, xanchor='center', ypos=0.5, yanchor='center')
$ ui.saybehavior()
$ ui.interact(suppress_overlay=True, suppress_underlay=True)
return
# This is called when the quick load button is clicked, to load the
# game.
label _quick_load:
$ renpy.load('quicksave')
return
init -1:
python hide:
# Add the quick save button in as an overlay function.
def quick_save_button():
ui.textbutton(_("Quick Save"),
xpos=0.98, ypos=0.02,
xanchor='right', yanchor='top',
clicked=renpy.curried_call_in_new_context('_quick_save'))
config.overlay_functions.append(quick_save_button)
# Add the quick load function to the main menu.
library.main_menu.insert(1, ('Quick Load',
ui.jumps("_quick_load"),
'renpy.can_load("quicksave")'))
+18 -36
View File
@@ -7,8 +7,8 @@
# either reduce config.hard_rollback_limit or set
# config.rollback_enabled to false.
# init:
# $ config.hard_rollback_limit = 1
init -100:
$ 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
@@ -115,7 +115,6 @@ init -100:
# Save the old character object.
readback_OldCharacter = Character
readback_OldDynamicCharacter = DynamicCharacter
readback_oldmenu = menu
class Character(readback_OldCharacter):
@@ -128,41 +127,23 @@ init -100:
self.readback_style = readback_style
def __call__(self, what, **kwargs):
if not self.check_condition():
return
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)
def store_readback(self, who, what):
readback_save(self, who, what)
def readback(self, who, what):
self.function(who, what,
who_style=self.who_style,
what_style=self.readback_style,
window_style=self.window_style,
interact=False,
all_at_once=True,
**self.properties)
class Sayer(object):
def __call__(self, who, what):
@@ -172,6 +153,7 @@ init -100:
def readback(self, who, what):
renpy.display_say(who, what,
what_style='readback_dialogue',
all_at_once=True,
interact=False)
@@ -179,7 +161,7 @@ init -100:
say = Sayer()
def readback(what):
readback_save(narrator, what)
narrator.store_readback(None, what)
def menu(menuitems):
rv = readback_oldmenu(menuitems)
+74
View File
@@ -0,0 +1,74 @@
# This extra demonstrates the two window say mode. In this mode, the
# character's name is placed in a different window than the line said
# by that character.
#
# You can use this by including this file in your game directory. You
# then need to change the definitions of Character and
# DynamicCharacter object to set function=two_window_say. For example:
#
# $ e = Character('Eileen', color=(200, 255, 200, 255),
# function=two_window_say)
#
# Once this is done, everything that is said by that character will be
# divided across two windows.
#
# You'll probably also want to customize the styles given below,
# especially by setting style.who_window.background to something
# a little more attractive.
init -100:
python:
style.create('two_window_say_position', 'default',
'(position) Used to get the position of the two windows in two window say mode.')
style.two_window_say_position.ypos = 1.0
style.two_window_say_position.yanchor = 'bottom'
style.create('who_window', 'default',
'(window) The style used for the window containing the who label when a Character uses the two_window_say function.')
style.who_window.background = Solid((0, 0, 255, 128))
style.who_window.xminimum = 150
style.who_window.xmargin = 10
style.who_window.xpadding = 10
style.who_window.ymargin = 5
style.who_window.ypadding = 5
def two_window_say(who, what,
who_style='say_label',
what_style='say_dialogue',
window_style='say_window',
who_window_style='who_window',
who_prefix='',
who_suffix=': ',
what_prefix='',
what_suffix='',
interact=True,
slow=True,
**properties):
if interact:
ui.saybehavior()
if who is not None:
who = who_prefix + who + who_suffix
what = what_prefix + what + what_suffix
ui.vbox(style='two_window_say_position')
if who is not None:
ui.window(style=who_window_style)
ui.text(who, style=who_style, **properties)
ui.window(style=window_style)
ui.text(what, style=what_style, slow=slow)
ui.close()
if interact:
ui.interact()
renpy.checkpoint()
+72
View File
@@ -0,0 +1,72 @@
# This extra contains a basic implementation of voice support. Right
# now, voice is given its own toggle, and can either be turned on or
# turned off. In the future, we'll probably provide some way of
# toggling it on or off for individual characters.
#
# To use it, place a voice "<wavfile>" line before each voiced line of
# dialogue.
#
# voice "e_1001.wav"
# e "Voice support lets you add the spoken word to your games."
#
# Normally, a voice is cancelled at the start of the next
# interaction. If you want a voice to span interactions, call
# voice_sustain.
#
# voice "e_1002.wav"
# e "Voice sustain is a technique that allows the same voice file.."
#
# $ voice_sustain()
# e "...to play for two lines of dialogue."
init -10:
python:
# Ensure the voice preference exists.
if persistent.voice is None:
persistent.voice = True
_voice = object()
_voice.play = None
_voice.sustain = False
# Call this to specify the voice file that will be played for
# the user.
def voice(file):
_voice.play = file
# Call this to specify that the currently playing voice file
# should be sustained through the current interaction.
def voice_sustain(ignored=""):
_voice.sustain = True
python hide:
renpy.sound.set_mixer(2, "voice")
library.has_voice = True
library.sample_voice = None
vp = _VolumePreference('Voice Volume',
'voice',
'library.has_voice',
'library.sample_voice')
library.preferences['prefs_right'].append(vp)
# This is called on each interaction, to ensure that the
# appropriate voice file is played for the user.
def voice_interact():
if not persistent.voice:
return
if _voice.play and not config.skipping:
renpy.sound.play(_voice.play, channel=2)
elif not _voice.sustain:
renpy.sound.stop(channel=2)
_voice.play = None
_voice.sustain = False
config.start_interact_callbacks.append(voice_interact)
+2
View File
@@ -0,0 +1,2 @@
console.exe --lint > lint.txt
start lint.txt
+17
View File
@@ -0,0 +1,17 @@
This file explains how to build the _renpy module for distribution on
the Mac and Windows.
(This file is not for distribution, as it doesn't make sense to
distribute it.)
Windows:
Get into msys.
cd /t/ab/renpy/module
/c/python23/python setup_win32 build --compiler mingw32 bdist_wininst
Macintosh:
python setup_mac bdist_mpkg --zipdist
+14
View File
@@ -0,0 +1,14 @@
# This makefile should work on Linux, and might work on other unix-like
# platforms as well. It probably won't work on Mac OS X, however.
build::
pyrexc _renpy.pyx
pyrexc pysdlsound.pyx
pyrexc nativemidi.pyx
pyrexc winmixer.pyx
python setup.py build_ext -i
clean:
-rm *.so
-rm -Rf build
+45
View File
@@ -0,0 +1,45 @@
This directory contains the source code for the _renpy module. This
module contains a number of image processing functions that aren't
present in pygame. If it's not present, then some of Ren'Py's
functionality will be missing. Games should still be playable, but
some effects may not be present, or may be present in a degraded form.
How does this affect you? Well, it depends on the platform you're
running on. So please read the appropriate selection below.
Windows
-------
If you're running an exe file under Windows, this doesn't affect you
one iota. Run it and be happy.
If you're running a python script under windows, you can download a
precompiled version of the module from the Ren'Py homepage,
http://www.bishoujo.us/renpy/.
Macintosh
---------
You'll need to download a mpkg containing the precompiled version of
the _renpy module. You can grab it from http://www.bishoujo.us/renpy/.
Linux/Unix
----------
You'll need to compile the module yourself. Ensure that you have the
SDL, pygame, and SDL_sound development headers and libraries
installed, and that you can run the sdl-config program. Then, in this
directory, type:
python setup.py build_ext -i
It should autodetect SDL, and build the module in the current
directory. If successful, a number of .so files will come into
existence in the current directory. You can then run Ren'Py.
If you have question or problems, please contact us via the Ren'Py web
page, http://www.bishoujo.us/renpy/.
+176
View File
@@ -0,0 +1,176 @@
# -*- python -*-
# Copyright 2005 PyTom <pytom@bishoujo.us>
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
cdef extern from "renpy.h":
void core_init()
void pixellate32_core(object, object, int, int, int, int)
void pixellate24_core(object, object, int, int, int, int)
void map32_core(object, object,
char *,
char *,
char *,
char *)
void map24_core(object, object,
char *,
char *,
char *)
void xblur32_core(object, object, int)
void alphamunge_core(object, object, int, int, int, char *)
import pygame
def version():
return 4008007
def pixellate(pysrc, pydst, avgwidth, avgheight, outwidth, outheight):
if not isinstance(pysrc, pygame.Surface):
raise Exception("pixellate requires a pygame Surface as its first argument.")
if not isinstance(pydst, pygame.Surface):
raise Exception("pixellate requires a pygame Surface as its second argument.")
if pysrc.get_bitsize() not in (24, 32):
raise Exception("pixellate requires a 24 or 32 bit surface.")
if pydst.get_bitsize() != pysrc.get_bitsize():
raise Exception("pixellate requires both surfaces have the same bitsize.")
pysrc.lock()
pydst.lock()
if pysrc.get_bitsize() == 32:
pixellate32_core(pysrc, pydst, avgwidth, avgheight, outwidth, outheight)
else:
pixellate24_core(pysrc, pydst, avgwidth, avgheight, outwidth, outheight)
pydst.unlock()
pysrc.unlock()
# Please note that r, g, b, and a are not necessarily red, green, blue
# and alpha. Instead, they are the first through fourth byte of data.
# The mapping between byte and color/alpha varies from system to
# system, and needs to be determined at a higher level.
def map(pysrc, pydst, r, g, b, a):
if not isinstance(pysrc, pygame.Surface):
raise Exception("map requires a pygame Surface as its first argument.")
if not isinstance(pydst, pygame.Surface):
raise Exception("map requires a pygame Surface as its second argument.")
if pysrc.get_bitsize() not in (24, 32):
raise Exception("map requires a 24 or 32 bit surface.")
if pydst.get_bitsize() != pysrc.get_bitsize():
raise Exception("map requires both surfaces have the same bitsize.")
if pydst.get_size() != pysrc.get_size():
raise Exception("map requires both surfaces have the same size.")
pysrc.lock()
pydst.lock()
if pysrc.get_bitsize() == 32:
map32_core(pysrc, pydst, r, g, b, a)
else:
map24_core(pysrc, pydst, r, g, b)
pydst.unlock()
pysrc.unlock()
def alpha_munge(pysrc, pydst, srcchan, dstchan, amap):
if not isinstance(pysrc, pygame.Surface):
raise Exception("map requires a pygame Surface as its first argument.")
if not isinstance(pydst, pygame.Surface):
raise Exception("map requires a pygame Surface as its second argument.")
if pysrc.get_bitsize() not in (24, 32):
raise Exception("map requires a 24 or 32 bit surface.")
if pydst.get_bitsize() != pysrc.get_bitsize():
raise Exception("map requires both surfaces have the same bitsize.")
if pydst.get_size() != pysrc.get_size():
raise Exception("map requires both surfaces have the same size.")
if pysrc.get_bitsize() == 24:
bytes = 3
else:
bytes = 4
pysrc.lock()
pydst.lock()
alphamunge_core(pysrc, pydst, bytes, srcchan, dstchan, amap)
pydst.unlock()
pysrc.unlock()
# def xblur(pysrc, pydst, radius):
# if not isinstance(pysrc, pygame.Surface):
# raise Exception("blur requires a pygame Surface as its first argument.")
# if not isinstance(pydst, pygame.Surface):
# raise Exception("blur requires a pygame Surface as its second argument.")
# if pysrc.get_bitsize() not in (24, 32):
# raise Exception("blur requires a 24 or 32 bit surface.")
# if pydst.get_bitsize() != pysrc.get_bitsize():
# raise Exception("blur requires both surfaces have the same bitsize.")
# if pydst.get_size() != pysrc.get_size():
# raise Exception("blur requires both surfaces have the same size.")
# pysrc.lock()
# pydst.lock()
# if pysrc.get_bitsize() == 32:
# xblur32_core(pysrc, pydst, radius)
# else:
# # blur24_core(pysrc, pydst, radius)
# assert False
# pydst.unlock()
# pysrc.unlock()
core_init()
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
pyrexc _renpy.pyx && python setup.py build_ext -i
+608
View File
@@ -0,0 +1,608 @@
#include "renpy.h"
#include <pygame/pygame.h>
#include <stdio.h>
// Shows how to do this.
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
#endif
/* Initializes the stuff found in this file.
*/
void core_init() {
import_pygame_base();
import_pygame_surface();
}
/* This pixellates a 32-bit RGBA pygame surface to a destination
* surface of a given size.
*
* pysrc - The source pygame surface, which must be 32-bit RGBA.
* pydst - The destination pygame surface, which should be 32-bit
* RGBA, and locked.
* avgwidth - The width of the pixels that will be averaged together.
* avgheight - The height of the pixels that will be averaged
* together.
* outwidth - The width of pixels that will be written to the output.
* outheight - The height of pixels that will be written to the
* output.
*
* We assume that pysrc and pydst have been locked before we are called.
*/
void pixellate32_core(PyObject *pysrc,
PyObject *pydst,
int avgwidth,
int avgheight,
int outwidth,
int outheight
) {
SDL_Surface *src;
SDL_Surface *dst;
int x, y, i, j;
Uint32 srcpitch, dstpitch;
Uint32 srcw, srch;
Uint32 dstw, dsth;
int vw, vh;
unsigned char *srcpixels;
unsigned char *dstpixels;
src = PySurface_AsSurface(pysrc);
dst = PySurface_AsSurface(pydst);
srcpixels = (unsigned char *) src->pixels;
dstpixels = (unsigned char *) dst->pixels;
srcpitch = src->pitch;
dstpitch = dst->pitch;
srcw = src->w;
dstw = dst->w;
srch = src->h;
dsth = dst->h;
/* Compute the virtual width and height. */
vw = ( srcw + avgwidth - 1) / avgwidth;
vh = ( srch + avgheight - 1) / avgheight;
/* Iterate through each of the virtual pixels. */
for (y = 0; y < vh; y++) {
int srcy = avgheight * y;
int dsty = outheight * y;
int srcylimit = srcy + avgheight;
int dstylimit = dsty + outheight;
if (srcylimit > srch) {
srcylimit = srch;
}
if (dstylimit > dsth) {
dstylimit = dsth;
}
for (x = 0; x < vw; x++) {
int srcx = avgwidth * x;
int dstx = outwidth * x;
int srcxlimit = srcx + avgwidth;
int dstxlimit = dstx + outheight;
if (srcxlimit > srcw) {
srcxlimit = srcw;
}
if (dstxlimit > dstw) {
dstxlimit = dstw;
}
// Please note that these names are just
// suggestions... It's possible that alpha will be
// in r, for example.
int r = 0;
int g = 0;
int b = 0;
int a = 0;
int number = 0;
// pos always points to the start of the current line.
unsigned char *pos = &srcpixels[srcy * srcpitch + srcx * 4];
/* Sum up the pixel values. */
for (j = srcy; j < srcylimit; j++) {
// po points to the current pixel.
unsigned char *po = pos;
for (i = srcx; i < srcxlimit; i++) {
r += *po++;
g += *po++;
b += *po++;
a += *po++;
number += 1;
}
pos += srcpitch;
}
/* Compute the average pixel values. */
r /= number;
g /= number;
b /= number;
a /= number;
/* Write out the average pixel values. */
pos = &dstpixels[dsty * dstpitch + dstx * 4];
for (j = dsty; j < dstylimit; j++) {
unsigned char *po = pos;
for (i = dstx; i < dstxlimit; i++) {
*po++ = r;
*po++ = g;
*po++ = b;
*po++ = a;
}
pos += dstpitch;
}
}
}
}
/* This pixellates a 32-bit RGBA pygame surface to a destination
* surface of a given size.
*
* pysrc - The source pygame surface, which must be 32-bit RGBA.
* pydst - The destination pygame surface, which should be 32-bit
* RGBA, and locked.
* avgwidth - The width of the pixels that will be averaged together.
* avgheight - The height of the pixels that will be averaged
* together.
* outwidth - The width of pixels that will be written to the output.
* outheight - The height of pixels that will be written to the
* output.
*
* We assume that pysrc and pydst have been locked before we are called.
*/
void pixellate24_core(PyObject *pysrc,
PyObject *pydst,
int avgwidth,
int avgheight,
int outwidth,
int outheight
) {
SDL_Surface *src;
SDL_Surface *dst;
int x, y, i, j;
Uint32 srcpitch, dstpitch;
Uint32 srcw, srch;
Uint32 dstw, dsth;
int vw, vh;
unsigned char *srcpixels;
unsigned char *dstpixels;
src = PySurface_AsSurface(pysrc);
dst = PySurface_AsSurface(pydst);
srcpixels = (unsigned char *) src->pixels;
dstpixels = (unsigned char *) dst->pixels;
srcpitch = src->pitch;
dstpitch = dst->pitch;
srcw = src->w;
dstw = dst->w;
srch = src->h;
dsth = dst->h;
/* Compute the virtual width and height. */
vw = ( srcw + avgwidth - 1) / avgwidth;
vh = ( srch + avgheight - 1) / avgheight;
/* Iterate through each of the virtual pixels. */
for (y = 0; y < vh; y++) {
int srcy = avgheight * y;
int dsty = outheight * y;
int srcylimit = srcy + avgheight;
int dstylimit = dsty + outheight;
if (srcylimit > srch) {
srcylimit = srch;
}
if (dstylimit > dsth) {
dstylimit = dsth;
}
for (x = 0; x < vw; x++) {
int srcx = avgwidth * x;
int dstx = outwidth * x;
int srcxlimit = srcx + avgwidth;
int dstxlimit = dstx + outheight;
if (srcxlimit > srcw) {
srcxlimit = srcw;
}
if (dstxlimit > dstw) {
dstxlimit = dstw;
}
// Please note that these names are just
// suggestions... It's possible that blue will be
// in r, for example.
int r = 0;
int g = 0;
int b = 0;
int number = 0;
// pos always points to the start of the current line.
unsigned char *pos = &srcpixels[srcy * srcpitch + srcx * 3];
/* Sum up the pixel values. */
for (j = srcy; j < srcylimit; j++) {
// po points to the current pixel.
unsigned char *po = pos;
for (i = srcx; i < srcxlimit; i++) {
r += *po++;
g += *po++;
b += *po++;
number += 1;
}
pos += srcpitch;
}
/* Compute the average pixel values. */
r /= number;
g /= number;
b /= number;
/* Write out the average pixel values. */
pos = &dstpixels[dsty * dstpitch + dstx * 3];
for (j = dsty; j < dstylimit; j++) {
unsigned char *po = pos;
for (i = dstx; i < dstxlimit; i++) {
*po++ = r;
*po++ = g;
*po++ = b;
}
pos += dstpitch;
}
}
}
}
/*
* This expects pysrc and pydst to be surfaces of the same size. It
* the source surface to the destination surface, using the r, g, b,
* and a maps. These maps are expected to be 256 bytes long, with each
* byte corresponding to a possible value of a channel in pysrc,
* giving what that value is mapped to in pydst.
*/
void map32_core(PyObject *pysrc,
PyObject *pydst,
char *rmap,
char *gmap,
char *bmap,
char *amap) {
SDL_Surface *src;
SDL_Surface *dst;
int x, y;
Uint32 srcpitch, dstpitch;
Uint32 srcw, srch;
Uint32 dstw, dsth;
char *srcpixels;
char *dstpixels;
char *srcrow;
char *dstrow;
char *srcp;
char *dstp;
src = PySurface_AsSurface(pysrc);
dst = PySurface_AsSurface(pydst);
srcpixels = (char *) src->pixels;
dstpixels = (char *) dst->pixels;
srcpitch = src->pitch;
dstpitch = dst->pitch;
srcw = src->w;
dstw = dst->w;
srch = src->h;
dsth = dst->h;
srcrow = srcpixels;
dstrow = dstpixels;
for (y = 0; y < srch; y++) {
srcp = srcrow;
dstp = dstrow;
for (x = 0; x < srcw; x++) {
*dstp++ = rmap[(unsigned char) *srcp++];
*dstp++ = gmap[(unsigned char) *srcp++];
*dstp++ = bmap[(unsigned char) *srcp++];
*dstp++ = amap[(unsigned char) *srcp++];
}
srcrow += srcpitch;
dstrow += dstpitch;
}
}
void map24_core(PyObject *pysrc,
PyObject *pydst,
char *rmap,
char *gmap,
char *bmap) {
SDL_Surface *src;
SDL_Surface *dst;
int x, y;
Uint32 srcpitch, dstpitch;
Uint32 srcw, srch;
Uint32 dstw, dsth;
char *srcpixels;
char *dstpixels;
char *srcrow;
char *dstrow;
char *srcp;
char *dstp;
src = PySurface_AsSurface(pysrc);
dst = PySurface_AsSurface(pydst);
srcpixels = (char *) src->pixels;
dstpixels = (char *) dst->pixels;
srcpitch = src->pitch;
dstpitch = dst->pitch;
srcw = src->w;
dstw = dst->w;
srch = src->h;
dsth = dst->h;
srcrow = srcpixels;
dstrow = dstpixels;
for (y = 0; y < srch; y++) {
srcp = srcrow;
dstp = dstrow;
for (x = 0; x < srcw; x++) {
*dstp++ = rmap[(unsigned char) *srcp++];
*dstp++ = gmap[(unsigned char) *srcp++];
*dstp++ = bmap[(unsigned char) *srcp++];
}
srcrow += srcpitch;
dstrow += dstpitch;
}
}
#if 0
void xblur32_core(PyObject *pysrc,
PyObject *pydst,
int radius) {
int i, x, y;
SDL_Surface *src;
SDL_Surface *dst;
Uint32 srcpitch, dstpitch;
Uint32 srcw, srch;
Uint32 dstw, dsth;
unsigned char *srcpixels;
unsigned char *dstpixels;
int count;
unsigned char *srcp;
unsigned char *dstp;
src = PySurface_AsSurface(pysrc);
dst = PySurface_AsSurface(pydst);
srcpixels = (unsigned char *) src->pixels;
dstpixels = (unsigned char *) dst->pixels;
srcpitch = src->pitch;
dstpitch = dst->pitch;
srcw = src->w;
dstw = dst->w;
srch = src->h;
dsth = dst->h;
int divisor = radius * 2 + 1;
for (y = 0; y < dsth; y++) {
// The values of the pixels on the left and right ends of the
// line.
unsigned char lr, lg, lb, la;
unsigned char rr, rg, rb, ra;
unsigned char *leader = srcpixels + y * srcpitch;
unsigned char *trailer = leader;
dstp = dstpixels + y * dstpitch;
lr = *leader;
lg = *(leader + 1);
lb = *(leader + 2);
la = *(leader + 3);
int sumr = lr * radius;
int sumg = lg * radius;
int sumb = lb * radius;
int suma = la * radius;
for (x = 0; x < radius + 0; x++) {
sumr += *leader++;
sumg += *leader++;
sumb += *leader++;
suma += *leader++;
}
// left side of the kernel is off of the screen.
for (x = 0; x < radius; x++) {
sumr += *leader++;
sumg += *leader++;
sumb += *leader++;
suma += *leader++;
*dstp++ = sumr / divisor;
*dstp++ = sumg / divisor;
*dstp++ = sumb / divisor;
*dstp++ = suma / divisor;
sumr -= lr;
sumg -= lg;
sumb -= lb;
suma -= la;
}
int end = srcw - radius - 1;
// The kernel is fully on the screen.
for (; x < end; x++) {
sumr += *leader++;
sumg += *leader++;
sumb += *leader++;
suma += *leader++;
*dstp++ = sumr / divisor;
*dstp++ = sumg / divisor;
*dstp++ = sumb / divisor;
*dstp++ = suma / divisor;
sumr -= *trailer++;
sumg -= *trailer++;
sumb -= *trailer++;
suma -= *trailer++;
}
rr = *leader++;
rg = *leader++;
rb = *leader++;
ra = *leader++;
// The kernel is off the right side of the screen.
for (; x < srcw; x++) {
sumr += rr;
sumg += rg;
sumb += rb;
suma += ra;
*dstp++ = sumr / divisor;
*dstp++ = sumg / divisor;
*dstp++ = sumb / divisor;
*dstp++ = suma / divisor;
sumr -= *trailer++;
sumg -= *trailer++;
sumb -= *trailer++;
suma -= *trailer++;
}
}
}
#endif
// Alpha Munge takes a channel from the source pixel, maps it, and
// sticks it into the alpha channel of the destination, overwriting
// the destination's alpha channel.
//
// It's used to implement SmartDissolve.
void alphamunge_core(PyObject *pysrc,
PyObject *pydst,
int src_bypp, // bytes per pixel.
int src_aoff, // alpha offset.
int dst_aoff, // alpha offset.
char *amap) {
int x, y;
SDL_Surface *src;
SDL_Surface *dst;
Uint32 srcpitch, dstpitch;
Uint32 srcw, srch;
Uint32 dstw, dsth;
unsigned char *srcpixels;
unsigned char *dstpixels;
unsigned char *srcline;
unsigned char *dstline;
unsigned char *srcp;
unsigned char *dstp;
src = PySurface_AsSurface(pysrc);
dst = PySurface_AsSurface(pydst);
srcpixels = (unsigned char *) src->pixels;
dstpixels = (unsigned char *) dst->pixels;
srcpitch = src->pitch;
dstpitch = dst->pitch;
srcw = src->w;
dstw = dst->w;
srch = src->h;
dsth = dst->h;
// We assume that src is bigger than dst, and so use dst
// to handle everything.
srcline = srcpixels;
dstline = dstpixels;
for (y = 0; y < dsth; y++) {
srcp = srcline + src_aoff;
dstp = dstline + dst_aoff;
for (x = 0; x < dstw; x++) {
*dstp = amap[*srcp];
srcp += src_bypp;
dstp += 4; // Need an alpha channel.
}
srcline += srcpitch;
dstline += dstpitch;
}
}
+25
View File
@@ -0,0 +1,25 @@
from ossaudiodev import *
mixer = openmixer()
def get_wave():
if not mixer.controls() & 1 << SOUND_MIXER_PCM:
return None
l, r = mixer.get(SOUND_MIXER_PCM)
return (l + r) / 200.0
def set_wave(vol):
if not mixer.controls() & 1 << SOUND_MIXER_PCM:
return None
v = int(vol * 100)
mixer.set(SOUND_MIXER_PCM, (v, v))
def get_midi():
return None
def set_midi(vol):
return
+40
View File
@@ -0,0 +1,40 @@
/*
native_midi: Hardware Midi support for the SDL_mixer library
Copyright (C) 2000 Florian 'Proff' Schulze
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Florian 'Proff' Schulze
florian.proff.schulze@gmx.net
*/
#ifndef _NATIVE_MIDI_H_
#define _NATIVE_MIDI_H_
#include <SDL/SDL_rwops.h>
typedef struct _NativeMidiSong NativeMidiSong;
int native_midi_detect();
NativeMidiSong *native_midi_loadsong(char *midifile);
NativeMidiSong *native_midi_loadsong_RW(SDL_RWops *rw);
void native_midi_freesong(NativeMidiSong *song);
void native_midi_start(NativeMidiSong *song);
void native_midi_stop();
int native_midi_active();
void native_midi_setvolume(int volume);
char *native_midi_error();
#endif /* _NATIVE_MIDI_H_ */
+404
View File
@@ -0,0 +1,404 @@
/*
native_midi: Hardware Midi support for the SDL_mixer library
Copyright (C) 2000,2001 Florian 'Proff' Schulze
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Florian 'Proff' Schulze
florian.proff.schulze@gmx.net
*/
#include "native_midi_common.h"
#include <stdlib.h>
#include <string.h>
#include <limits.h>
/* The maximum number of midi tracks that we can handle
#define MIDI_TRACKS 32 */
/* A single midi track as read from the midi file */
typedef struct
{
Uint8 *data; /* MIDI message stream */
int len; /* length of the track data */
} MIDITrack;
/* A midi file, stripped down to the absolute minimum - divison & track data */
typedef struct
{
int division; /* number of pulses per quarter note (ppqn) */
int nTracks; /* number of tracks */
MIDITrack *track; /* tracks */
} MIDIFile;
/* Some macros that help us stay endianess-independant */
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
#define BE_SHORT(x) (x)
#define BE_LONG(x) (x)
#else
#define BE_SHORT(x) ((((x)&0xFF)<<8) | (((x)>>8)&0xFF))
#define BE_LONG(x) ((((x)&0x0000FF)<<24) | \
(((x)&0x00FF00)<<8) | \
(((x)&0xFF0000)>>8) | \
(((x)>>24)&0xFF))
#endif
/* Get Variable Length Quantity */
static int GetVLQ(MIDITrack *track, int *currentPos)
{
int l = 0;
Uint8 c;
while(1)
{
c = track->data[*currentPos];
(*currentPos)++;
l += (c & 0x7f);
if (!(c & 0x80))
return l;
l <<= 7;
}
}
/* Create a single MIDIEvent */
static MIDIEvent *CreateEvent(Uint32 time, Uint8 event, Uint8 a, Uint8 b)
{
MIDIEvent *newEvent;
newEvent = calloc(1, sizeof(MIDIEvent));
if (newEvent)
{
newEvent->time = time;
newEvent->status = event;
newEvent->data[0] = a;
newEvent->data[1] = b;
}
return newEvent;
}
/* Convert a single midi track to a list of MIDIEvents */
static MIDIEvent *MIDITracktoStream(MIDITrack *track)
{
Uint32 atime = 0;
Uint32 len = 0;
Uint8 event,type,a,b;
Uint8 laststatus = 0;
Uint8 lastchan = 0;
int currentPos = 0;
int end = 0;
MIDIEvent *head = CreateEvent(0,0,0,0); /* dummy event to make handling the list easier */
MIDIEvent *currentEvent = head;
while (!end)
{
if (currentPos >= track->len)
break; /* End of data stream reached */
atime += GetVLQ(track, &currentPos);
event = track->data[currentPos++];
/* Handle SysEx seperatly */
if (((event>>4) & 0x0F) == MIDI_STATUS_SYSEX)
{
if (event == 0xFF)
{
type = track->data[currentPos];
currentPos++;
switch(type)
{
case 0x2f: /* End of data marker */
end = 1;
case 0x51: /* Tempo change */
/*
a=track->data[currentPos];
b=track->data[currentPos+1];
c=track->data[currentPos+2];
AddEvent(song, atime, MEVT_TEMPO, c, b, a);
*/
break;
}
}
else
type = 0;
len = GetVLQ(track, &currentPos);
/* Create an event and attach the extra data, if any */
currentEvent->next = CreateEvent(atime, event, type, 0);
currentEvent = currentEvent->next;
if (NULL == currentEvent)
{
FreeMIDIEventList(head);
return NULL;
}
if (len)
{
currentEvent->extraLen = len;
currentEvent->extraData = malloc(len);
memcpy(currentEvent->extraData, &(track->data[currentPos]), len);
currentPos += len;
}
}
else
{
a = event;
if (a & 0x80) /* It's a status byte */
{
/* Extract channel and status information */
lastchan = a & 0x0F;
laststatus = (a>>4) & 0x0F;
/* Read the next byte which should always be a data byte */
a = track->data[currentPos++] & 0x7F;
}
switch(laststatus)
{
case MIDI_STATUS_NOTE_OFF:
case MIDI_STATUS_NOTE_ON: /* Note on */
case MIDI_STATUS_AFTERTOUCH: /* Key Pressure */
case MIDI_STATUS_CONTROLLER: /* Control change */
case MIDI_STATUS_PITCH_WHEEL: /* Pitch wheel */
b = track->data[currentPos++] & 0x7F;
currentEvent->next = CreateEvent(atime, (Uint8)((laststatus<<4)+lastchan), a, b);
currentEvent = currentEvent->next;
if (NULL == currentEvent)
{
FreeMIDIEventList(head);
return NULL;
}
break;
case MIDI_STATUS_PROG_CHANGE: /* Program change */
case MIDI_STATUS_PRESSURE: /* Channel pressure */
a &= 0x7f;
currentEvent->next = CreateEvent(atime, (Uint8)((laststatus<<4)+lastchan), a, 0);
currentEvent = currentEvent->next;
if (NULL == currentEvent)
{
FreeMIDIEventList(head);
return NULL;
}
break;
default: /* Sysex already handled above */
break;
}
}
}
currentEvent = head->next;
free(head); /* release the dummy head event */
return currentEvent;
}
/*
* Convert a midi song, consisting of up to 32 tracks, to a list of MIDIEvents.
* To do so, first convert the tracks seperatly, then interweave the resulting
* MIDIEvent-Lists to one big list.
*/
static MIDIEvent *MIDItoStream(MIDIFile *mididata)
{
MIDIEvent **track;
MIDIEvent *head = CreateEvent(0,0,0,0); /* dummy event to make handling the list easier */
MIDIEvent *currentEvent = head;
int trackID;
if (NULL == head)
return NULL;
track = (MIDIEvent**) calloc(1, sizeof(MIDIEvent*) * mididata->nTracks);
if (NULL == head)
return NULL;
/* First, convert all tracks to MIDIEvent lists */
for (trackID = 0; trackID < mididata->nTracks; trackID++)
track[trackID] = MIDITracktoStream(&mididata->track[trackID]);
/* Now, merge the lists. */
/* TODO */
while(1)
{
Uint32 lowestTime = INT_MAX;
int currentTrackID = -1;
/* Find the next event */
for (trackID = 0; trackID < mididata->nTracks; trackID++)
{
if (track[trackID] && (track[trackID]->time < lowestTime))
{
currentTrackID = trackID;
lowestTime = track[currentTrackID]->time;
}
}
/* Check if we processes all events */
if (currentTrackID == -1)
break;
currentEvent->next = track[currentTrackID];
track[currentTrackID] = track[currentTrackID]->next;
currentEvent = currentEvent->next;
lowestTime = 0;
}
/* Make sure the list is properly terminated */
currentEvent->next = 0;
currentEvent = head->next;
free(track);
free(head); /* release the dummy head event */
return currentEvent;
}
static int ReadMIDIFile(MIDIFile *mididata, SDL_RWops *rw)
{
int i = 0;
Uint32 ID;
Uint32 size;
Uint16 format;
Uint16 tracks;
Uint16 division;
if (!mididata)
return 0;
if (!rw)
return 0;
/* Make sure this is really a MIDI file */
SDL_RWread(rw, &ID, 1, 4);
if (BE_LONG(ID) != 'MThd')
return 0;
/* Header size must be 6 */
SDL_RWread(rw, &size, 1, 4);
size = BE_LONG(size);
if (size != 6)
return 0;
/* We only support format 0 and 1, but not 2 */
SDL_RWread(rw, &format, 1, 2);
format = BE_SHORT(format);
if (format != 0 && format != 1)
return 0;
SDL_RWread(rw, &tracks, 1, 2);
tracks = BE_SHORT(tracks);
mididata->nTracks = tracks;
/* Allocate tracks */
mididata->track = (MIDITrack*) calloc(1, sizeof(MIDITrack) * mididata->nTracks);
if (NULL == mididata->track)
{
goto bail;
}
/* Retrieve the PPQN value, needed for playback */
SDL_RWread(rw, &division, 1, 2);
mididata->division = BE_SHORT(division);
for (i=0; i<tracks; i++)
{
SDL_RWread(rw, &ID, 1, 4); /* We might want to verify this is MTrk... */
SDL_RWread(rw, &size, 1, 4);
size = BE_LONG(size);
mididata->track[i].len = size;
mididata->track[i].data = malloc(size);
if (NULL == mididata->track[i].data)
{
goto bail;
}
SDL_RWread(rw, mididata->track[i].data, 1, size);
}
return 1;
bail:
for(;i >= 0; i--)
{
if (mididata->track[i].data)
free(mididata->track[i].data);
}
return 0;
}
MIDIEvent *CreateMIDIEventList(SDL_RWops *rw, Uint16 *division)
{
MIDIFile *mididata = NULL;
MIDIEvent *eventList;
int trackID;
mididata = calloc(1, sizeof(MIDIFile));
if (!mididata)
return NULL;
/* Open the file */
if ( rw != NULL )
{
/* Read in the data */
if ( ! ReadMIDIFile(mididata, rw))
{
free(mididata);
return NULL;
}
}
else
{
free(mididata);
return NULL;
}
if (division)
*division = mididata->division;
eventList = MIDItoStream(mididata);
for(trackID = 0; trackID < mididata->nTracks; trackID++)
{
if (mididata->track[trackID].data)
free(mididata->track[trackID].data);
}
free(mididata->track);
free(mididata);
return eventList;
}
void FreeMIDIEventList(MIDIEvent *head)
{
MIDIEvent *cur, *next;
cur = head;
while (cur)
{
next = cur->next;
if (cur->extraData)
free (cur->extraData);
free (cur);
cur = next;
}
}
+67
View File
@@ -0,0 +1,67 @@
/*
native_midi: Hardware Midi support for the SDL_mixer library
Copyright (C) 2000,2001 Florian 'Proff' Schulze & Max Horn
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Florian 'Proff' Schulze
florian.proff.schulze@gmx.net
Max Horn
max@quendi.de
*/
#ifndef _NATIVE_MIDI_COMMON_H_
#define _NATIVE_MIDI_COMMON_H_
#include <SDL/SDL.h>
/* Midi Status Bytes */
#define MIDI_STATUS_NOTE_OFF 0x8
#define MIDI_STATUS_NOTE_ON 0x9
#define MIDI_STATUS_AFTERTOUCH 0xA
#define MIDI_STATUS_CONTROLLER 0xB
#define MIDI_STATUS_PROG_CHANGE 0xC
#define MIDI_STATUS_PRESSURE 0xD
#define MIDI_STATUS_PITCH_WHEEL 0xE
#define MIDI_STATUS_SYSEX 0xF
/* We store the midi events in a linked list; this way it is
easy to shuffle the tracks together later on; and we are
flexible in the size of each elemnt.
*/
typedef struct MIDIEvent
{
Uint32 time; /* Time at which this midi events occurs */
Uint8 status; /* Status byte */
Uint8 data[2]; /* 1 or 2 bytes additional data for most events */
Uint32 extraLen; /* For some SysEx events, we need additional storage */
Uint8 *extraData;
struct MIDIEvent *next;
} MIDIEvent;
/* Load a midifile to memory, converting it to a list of MIDIEvents.
This function returns a linked lists of MIDIEvents, 0 if an error occured.
*/
MIDIEvent *CreateMIDIEventList(SDL_RWops *rw, Uint16 *division);
/* Release a MIDIEvent list after usage. */
void FreeMIDIEventList(MIDIEvent *head);
#endif /* _NATIVE_MIDI_COMMON_H_ */
+703
View File
@@ -0,0 +1,703 @@
/*
native_midi_mac: Native Midi support on MacOS for the SDL_mixer library
Copyright (C) 2001 Max Horn
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Max Horn
max@quendi.de
*/
#if defined(macintosh) || defined(MACOSX)
#include "native_midi.h"
#include "native_midi_common.h"
#ifdef MACOSX
#include <QuickTime/QuickTimeMusic.h>
#else
#include <QuickTimeMusic.h>
#endif
#include <assert.h>
#include <stdlib.h>
#include <string.h>
/* Native Midi song */
struct _NativeMidiSong
{
Uint32 *tuneSequence;
Uint32 *tuneHeader;
};
enum
{
/* number of (32-bit) long words in a note request event */
kNoteRequestEventLength = ((sizeof(NoteRequest)/sizeof(long)) + 2),
/* number of (32-bit) long words in a marker event */
kMarkerEventLength = 1,
/* number of (32-bit) long words in a general event, minus its data */
kGeneralEventLength = 2
};
#define ERROR_BUF_SIZE 256
#define BUFFER_INCREMENT 5000
#define REST_IF_NECESSARY() do {\
int timeDiff = eventPos->time - lastEventTime; \
if(timeDiff) \
{ \
timeDiff = (int)(timeDiff*tick); \
qtma_StuffRestEvent(*tunePos, timeDiff); \
tunePos++; \
lastEventTime = eventPos->time; \
} \
} while(0)
static Uint32 *BuildTuneSequence(MIDIEvent *evntlist, int ppqn, int part_poly_max[32], int part_to_inst[32], int *numParts);
static Uint32 *BuildTuneHeader(int part_poly_max[32], int part_to_inst[32], int numParts);
/* The global TunePlayer instance */
static TunePlayer gTunePlayer = NULL;
static int gInstaceCount = 0;
static Uint32 *gCurrentTuneSequence = NULL;
static char gErrorBuffer[ERROR_BUF_SIZE] = "";
/* Check whether QuickTime is available */
int native_midi_detect()
{
/* TODO */
return 1;
}
NativeMidiSong *native_midi_loadsong(char *midifile)
{
NativeMidiSong *song = NULL;
MIDIEvent *evntlist = NULL;
int part_to_inst[32];
int part_poly_max[32];
int numParts = 0;
Uint16 ppqn;
SDL_RWops *rw;
/* Init the arrays */
memset(part_poly_max,0,sizeof(part_poly_max));
memset(part_to_inst,-1,sizeof(part_to_inst));
/* Attempt to load the midi file */
rw = SDL_RWFromFile(midifile, "rb");
if (rw) {
evntlist = CreateMIDIEventList(rw, &ppqn);
SDL_RWclose(rw);
if (!evntlist)
goto bail;
}
/* Allocate memory for the song struct */
song = malloc(sizeof(NativeMidiSong));
if (!song)
goto bail;
/* Build a tune sequence from the event list */
song->tuneSequence = BuildTuneSequence(evntlist, ppqn, part_poly_max, part_to_inst, &numParts);
if(!song->tuneSequence)
goto bail;
/* Now build a tune header from the data we collect above, create
all parts as needed and assign them the correct instrument.
*/
song->tuneHeader = BuildTuneHeader(part_poly_max, part_to_inst, numParts);
if(!song->tuneHeader)
goto bail;
/* Increment the instance count */
gInstaceCount++;
if (gTunePlayer == NULL)
gTunePlayer = OpenDefaultComponent(kTunePlayerComponentType, 0);
/* Finally, free the event list */
FreeMIDIEventList(evntlist);
return song;
bail:
if (evntlist)
FreeMIDIEventList(evntlist);
if (song)
{
if(song->tuneSequence)
free(song->tuneSequence);
if(song->tuneHeader)
DisposePtr((Ptr)song->tuneHeader);
free(song);
}
return NULL;
}
NativeMidiSong *native_midi_loadsong_RW(SDL_RWops *rw)
{
NativeMidiSong *song = NULL;
MIDIEvent *evntlist = NULL;
int part_to_inst[32];
int part_poly_max[32];
int numParts = 0;
Uint16 ppqn;
/* Init the arrays */
memset(part_poly_max,0,sizeof(part_poly_max));
memset(part_to_inst,-1,sizeof(part_to_inst));
/* Attempt to load the midi file */
evntlist = CreateMIDIEventList(rw, &ppqn);
if (!evntlist)
goto bail;
/* Allocate memory for the song struct */
song = malloc(sizeof(NativeMidiSong));
if (!song)
goto bail;
/* Build a tune sequence from the event list */
song->tuneSequence = BuildTuneSequence(evntlist, ppqn, part_poly_max, part_to_inst, &numParts);
if(!song->tuneSequence)
goto bail;
/* Now build a tune header from the data we collect above, create
all parts as needed and assign them the correct instrument.
*/
song->tuneHeader = BuildTuneHeader(part_poly_max, part_to_inst, numParts);
if(!song->tuneHeader)
goto bail;
/* Increment the instance count */
gInstaceCount++;
if (gTunePlayer == NULL)
gTunePlayer = OpenDefaultComponent(kTunePlayerComponentType, 0);
/* Finally, free the event list */
FreeMIDIEventList(evntlist);
return song;
bail:
if (evntlist)
FreeMIDIEventList(evntlist);
if (song)
{
if(song->tuneSequence)
free(song->tuneSequence);
if(song->tuneHeader)
DisposePtr((Ptr)song->tuneHeader);
free(song);
}
return NULL;
}
void native_midi_freesong(NativeMidiSong *song)
{
if(!song || !song->tuneSequence)
return;
/* If this is the currently playing song, stop it now */
if (song->tuneSequence == gCurrentTuneSequence)
native_midi_stop();
/* Finally, free the data storage */
free(song->tuneSequence);
DisposePtr((Ptr)song->tuneHeader);
free(song);
/* Increment the instance count */
gInstaceCount--;
if ((gTunePlayer != NULL) && (gInstaceCount == 0))
{
CloseComponent(gTunePlayer);
gTunePlayer = NULL;
}
}
void native_midi_start(NativeMidiSong *song)
{
UInt32 queueFlags = 0;
ComponentResult tpError;
assert (gTunePlayer != NULL);
SDL_PauseAudio(1);
// SDL_UnlockAudio();
/* First, stop the currently playing music */
native_midi_stop();
/* Set up the queue flags */
queueFlags = kTuneStartNow;
/* Set the time scale (units per second), we want milliseconds */
tpError = TuneSetTimeScale(gTunePlayer, 1000);
if (tpError != noErr)
{
strncpy (gErrorBuffer, "MIDI error during TuneSetTimeScale", ERROR_BUF_SIZE);
goto done;
}
/* Set the header, to tell what instruments are used */
tpError = TuneSetHeader(gTunePlayer, (UInt32 *)song->tuneHeader);
if (tpError != noErr)
{
strncpy (gErrorBuffer, "MIDI error during TuneSetHeader", ERROR_BUF_SIZE);
goto done;
}
/* Have it allocate whatever resources are needed */
tpError = TunePreroll(gTunePlayer);
if (tpError != noErr)
{
strncpy (gErrorBuffer, "MIDI error during TunePreroll", ERROR_BUF_SIZE);
goto done;
}
/* We want to play at normal volume */
tpError = TuneSetVolume(gTunePlayer, 0x00010000);
if (tpError != noErr)
{
strncpy (gErrorBuffer, "MIDI error during TuneSetVolume", ERROR_BUF_SIZE);
goto done;
}
/* Finally, start playing the full song */
gCurrentTuneSequence = song->tuneSequence;
tpError = TuneQueue(gTunePlayer, (UInt32 *)song->tuneSequence, 0x00010000, 0, 0xFFFFFFFF, queueFlags, NULL, 0);
if (tpError != noErr)
{
strncpy (gErrorBuffer, "MIDI error during TuneQueue", ERROR_BUF_SIZE);
goto done;
}
done:
1;
// SDL_LockAudio();
SDL_PauseAudio(0);
}
void native_midi_stop()
{
if (gTunePlayer == NULL)
return;
/* Stop music */
TuneStop(gTunePlayer, 0);
/* Deallocate all instruments */
TuneUnroll(gTunePlayer);
}
int native_midi_active()
{
if (gTunePlayer != NULL)
{
TuneStatus ts;
TuneGetStatus(gTunePlayer,&ts);
return ts.queueTime != 0;
}
else
return 0;
}
void native_midi_setvolume(int volume)
{
if (gTunePlayer == NULL)
return;
/* QTMA olume may range from 0.0 to 1.0 (in 16.16 fixed point encoding) */
TuneSetVolume(gTunePlayer, (0x00010000 * volume)/SDL_MIX_MAXVOLUME);
}
char *native_midi_error()
{
return gErrorBuffer;
}
Uint32 *BuildTuneSequence(MIDIEvent *evntlist, int ppqn, int part_poly_max[32], int part_to_inst[32], int *numParts)
{
int part_poly[32];
int channel_to_part[16];
int channel_pan[16];
int channel_vol[16];
int channel_pitch_bend[16];
int lastEventTime = 0;
int tempo = 500000;
double Ippqn = 1.0 / (1000*ppqn);
double tick = tempo * Ippqn;
MIDIEvent *eventPos = evntlist;
MIDIEvent *noteOffPos;
Uint32 *tunePos, *endPos;
Uint32 *tuneSequence;
size_t tuneSize;
/* allocate space for the tune header */
tuneSize = 5000;
tuneSequence = (Uint32 *)malloc(tuneSize * sizeof(Uint32));
if (tuneSequence == NULL)
return NULL;
/* Set starting position in our tune memory */
tunePos = tuneSequence;
endPos = tuneSequence + tuneSize;
/* Initialise the arrays */
memset(part_poly,0,sizeof(part_poly));
memset(channel_to_part,-1,sizeof(channel_to_part));
memset(channel_pan,-1,sizeof(channel_pan));
memset(channel_vol,-1,sizeof(channel_vol));
memset(channel_pitch_bend,-1,sizeof(channel_pitch_bend));
*numParts = 0;
/*
* Now the major work - iterate over all GM events,
* and turn them into QuickTime Music format.
* At the same time, calculate the max. polyphony for each part,
* and also the part->instrument mapping.
*/
while(eventPos)
{
int status = (eventPos->status&0xF0)>>4;
int channel = eventPos->status&0x0F;
int part = channel_to_part[channel];
int velocity, pitch;
int value, controller;
int bend;
int newInst;
/* Check if we are running low on space... */
if((tunePos+16) > endPos)
{
/* Resize our data storage. */
Uint32 *oldTuneSequence = tuneSequence;
tuneSize += BUFFER_INCREMENT;
tuneSequence = (Uint32 *)realloc(tuneSequence, tuneSize * sizeof(Uint32));
if(oldTuneSequence != tuneSequence)
tunePos += tuneSequence - oldTuneSequence;
endPos = tuneSequence + tuneSize;
}
switch (status)
{
case MIDI_STATUS_NOTE_OFF:
assert(part>=0 && part<=31);
/* Keep track of the polyphony of the current part */
part_poly[part]--;
break;
case MIDI_STATUS_NOTE_ON:
if (part < 0)
{
/* If no part is specified yet, we default to the first instrument, which
is piano (or the first drum kit if we are on the drum channel)
*/
int newInst;
if (channel == 9)
newInst = kFirstDrumkit + 1; /* the first drum kit is the "no drum" kit! */
else
newInst = kFirstGMInstrument;
part = channel_to_part[channel] = *numParts;
part_to_inst[(*numParts)++] = newInst;
}
/* TODO - add support for more than 32 parts using eXtended QTMA events */
assert(part<=31);
/* Decode pitch & velocity */
pitch = eventPos->data[0];
velocity = eventPos->data[1];
if (velocity == 0)
{
/* was a NOTE OFF in disguise, so we decrement the polyphony */
part_poly[part]--;
}
else
{
/* Keep track of the polyphony of the current part */
int foo = ++part_poly[part];
if (part_poly_max[part] < foo)
part_poly_max[part] = foo;
/* Now scan forward to find the matching NOTE OFF event */
for(noteOffPos = eventPos; noteOffPos; noteOffPos = noteOffPos->next)
{
if ((noteOffPos->status&0xF0)>>4 == MIDI_STATUS_NOTE_OFF
&& channel == (eventPos->status&0x0F)
&& pitch == noteOffPos->data[0])
break;
/* NOTE ON with velocity == 0 is the same as a NOTE OFF */
if ((noteOffPos->status&0xF0)>>4 == MIDI_STATUS_NOTE_ON
&& channel == (eventPos->status&0x0F)
&& pitch == noteOffPos->data[0]
&& 0 == noteOffPos->data[1])
break;
}
/* Did we find a note off? Should always be the case, but who knows... */
if (noteOffPos)
{
/* We found a NOTE OFF, now calculate the note duration */
int duration = (int)((noteOffPos->time - eventPos->time)*tick);
REST_IF_NECESSARY();
/* Now we need to check if we get along with a normal Note Event, or if we need an extended one... */
if (duration < 2048 && pitch>=32 && pitch<=95 && velocity>=0 && velocity<=127)
{
qtma_StuffNoteEvent(*tunePos, part, pitch, velocity, duration);
tunePos++;
}
else
{
qtma_StuffXNoteEvent(*tunePos, *(tunePos+1), part, pitch, velocity, duration);
tunePos+=2;
}
}
}
break;
case MIDI_STATUS_AFTERTOUCH:
/* NYI - use kControllerAfterTouch. But how are the parameters to be mapped? */
break;
case MIDI_STATUS_CONTROLLER:
controller = eventPos->data[0];
value = eventPos->data[1];
switch(controller)
{
case 0: /* bank change - igore for now */
break;
case kControllerVolume:
if(channel_vol[channel] != value<<8)
{
channel_vol[channel] = value<<8;
if(part>=0 && part<=31)
{
REST_IF_NECESSARY();
qtma_StuffControlEvent(*tunePos, part, kControllerVolume, channel_vol[channel]);
tunePos++;
}
}
break;
case kControllerPan:
if(channel_pan[channel] != (value << 1) + 256)
{
channel_pan[channel] = (value << 1) + 256;
if(part>=0 && part<=31)
{
REST_IF_NECESSARY();
qtma_StuffControlEvent(*tunePos, part, kControllerPan, channel_pan[channel]);
tunePos++;
}
}
break;
default:
/* No other controllers implemented yet */;
break;
}
break;
case MIDI_STATUS_PROG_CHANGE:
/* Instrument changed */
newInst = eventPos->data[0];
/* Channel 9 (the 10th channel) is different, it indicates a drum kit */
if (channel == 9)
newInst += kFirstDrumkit;
else
newInst += kFirstGMInstrument;
/* Only if the instrument for this channel *really* changed, add a new part. */
if(newInst != part_to_inst[part])
{
/* TODO maybe make use of kGeneralEventPartChange here,
to help QT reuse note channels?
*/
part = channel_to_part[channel] = *numParts;
part_to_inst[(*numParts)++] = newInst;
if(channel_vol[channel] >= 0)
{
REST_IF_NECESSARY();
qtma_StuffControlEvent(*tunePos, part, kControllerVolume, channel_vol[channel]);
tunePos++;
}
if(channel_pan[channel] >= 0)
{
REST_IF_NECESSARY();
qtma_StuffControlEvent(*tunePos, part, kControllerPan, channel_pan[channel]);
tunePos++;
}
if(channel_pitch_bend[channel] >= 0)
{
REST_IF_NECESSARY();
qtma_StuffControlEvent(*tunePos, part, kControllerPitchBend, channel_pitch_bend[channel]);
tunePos++;
}
}
break;
case MIDI_STATUS_PRESSURE:
/* NYI */
break;
case MIDI_STATUS_PITCH_WHEEL:
/* In the midi spec, 0x2000 = center, 0x0000 = - 2 semitones, 0x3FFF = +2 semitones
but for QTMA, we specify it as a 8.8 fixed point of semitones
TODO: detect "pitch bend range changes" & honor them!
*/
bend = (eventPos->data[0] & 0x7f) | ((eventPos->data[1] & 0x7f) << 7);
/* "Center" the bend */
bend -= 0x2000;
/* Move it to our format: */
bend <<= 4;
/* If it turns out the pitch bend didn't change, stop here */
if(channel_pitch_bend[channel] == bend)
break;
channel_pitch_bend[channel] = bend;
if(part>=0 && part<=31)
{
/* Stuff a control event */
REST_IF_NECESSARY();
qtma_StuffControlEvent(*tunePos, part, kControllerPitchBend, bend);
tunePos++;
}
break;
case MIDI_STATUS_SYSEX:
if (eventPos->status == 0xFF && eventPos->data[0] == 0x51) /* Tempo change */
{
tempo = (eventPos->extraData[0] << 16) +
(eventPos->extraData[1] << 8) +
eventPos->extraData[2];
tick = tempo * Ippqn;
}
break;
}
/* on to the next event */
eventPos = eventPos->next;
}
/* Finally, place an end marker */
*tunePos = kEndMarkerValue;
return tuneSequence;
}
Uint32 *BuildTuneHeader(int part_poly_max[32], int part_to_inst[32], int numParts)
{
Uint32 *myHeader;
Uint32 *myPos1, *myPos2; /* pointers to the head and tail long words of a music event */
NoteRequest *myNoteRequest;
NoteAllocator myNoteAllocator; /* for the NAStuffToneDescription call */
ComponentResult myErr = noErr;
int part;
myHeader = NULL;
myNoteAllocator = NULL;
/*
* Open up the Note Allocator
*/
myNoteAllocator = OpenDefaultComponent(kNoteAllocatorComponentType,0);
if (myNoteAllocator == NULL)
goto bail;
/*
* Allocate space for the tune header
*/
myHeader = (Uint32 *)
NewPtrClear((numParts * kNoteRequestEventLength + kMarkerEventLength) * sizeof(Uint32));
if (myHeader == NULL)
goto bail;
myPos1 = myHeader;
/*
* Loop over all parts
*/
for(part = 0; part < numParts; ++part)
{
/*
* Stuff request for the instrument with the given polyphony
*/
myPos2 = myPos1 + (kNoteRequestEventLength - 1); /* last longword of general event */
qtma_StuffGeneralEvent(*myPos1, *myPos2, part, kGeneralEventNoteRequest, kNoteRequestEventLength);
myNoteRequest = (NoteRequest *)(myPos1 + 1);
myNoteRequest->info.flags = 0;
/* I'm told by the Apple people that the Quicktime types were poorly designed and it was
* too late to change them. On little endian, the BigEndian(Short|Fixed) types are structs
* while on big endian they are primitive types. Furthermore, Quicktime failed to
* provide setter and getter functions. To get this to work, we need to case the
* code for the two possible situations.
* My assumption is that the right-side value was always expected to be BigEndian
* as it was written way before the Universal Binary transition. So in the little endian
* case, OSSwap is used.
*/
#if __LITTLE_ENDIAN__
myNoteRequest->info.polyphony.bigEndianValue = OSSwapHostToBigInt16(part_poly_max[part]);
myNoteRequest->info.typicalPolyphony.bigEndianValue = OSSwapHostToBigInt32(0x00010000);
#else
myNoteRequest->info.polyphony = part_poly_max[part];
myNoteRequest->info.typicalPolyphony = 0x00010000;
#endif
myErr = NAStuffToneDescription(myNoteAllocator,part_to_inst[part],&myNoteRequest->tone);
if (myErr != noErr)
goto bail;
/* move pointer to beginning of next event */
myPos1 += kNoteRequestEventLength;
}
*myPos1 = kEndMarkerValue; /* end of sequence marker */
bail:
if(myNoteAllocator)
CloseComponent(myNoteAllocator);
/* if we encountered an error, dispose of the storage we allocated and return NULL */
if (myErr != noErr) {
DisposePtr((Ptr)myHeader);
myHeader = NULL;
}
return myHeader;
}
#endif /* MacOS native MIDI support */
+321
View File
@@ -0,0 +1,321 @@
/*
native_midi: Hardware Midi support for the SDL_mixer library
Copyright (C) 2000,2001 Florian 'Proff' Schulze
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Florian 'Proff' Schulze
florian.proff.schulze@gmx.net
*/
/* everything below is currently one very big bad hack ;) Proff */
#ifdef WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <mmsystem.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include "native_midi.h"
#include "native_midi_common.h"
struct _NativeMidiSong {
int MusicLoaded;
int MusicPlaying;
MIDIHDR MidiStreamHdr;
MIDIEVENT *NewEvents;
Uint16 ppqn;
int Size;
int NewPos;
};
static UINT MidiDevice=MIDI_MAPPER;
static HMIDISTRM hMidiStream;
static NativeMidiSong *currentsong;
static int BlockOut(NativeMidiSong *song)
{
MMRESULT err;
int BlockSize;
if ((song->MusicLoaded) && (song->NewEvents))
{
// proff 12/8/98: Added for savety
midiOutUnprepareHeader((HMIDIOUT)hMidiStream,&song->MidiStreamHdr,sizeof(MIDIHDR));
if (song->NewPos>=song->Size)
return 0;
BlockSize=(song->Size-song->NewPos);
if (BlockSize<=0)
return 0;
if (BlockSize>36000)
BlockSize=36000;
song->MidiStreamHdr.lpData=(void *)((unsigned char *)song->NewEvents+song->NewPos);
song->NewPos+=BlockSize;
song->MidiStreamHdr.dwBufferLength=BlockSize;
song->MidiStreamHdr.dwBytesRecorded=BlockSize;
song->MidiStreamHdr.dwFlags=0;
err=midiOutPrepareHeader((HMIDIOUT)hMidiStream,&song->MidiStreamHdr,sizeof(MIDIHDR));
if (err!=MMSYSERR_NOERROR)
return 0;
err=midiStreamOut(hMidiStream,&song->MidiStreamHdr,sizeof(MIDIHDR));
return 0;
}
return 1;
}
static void MIDItoStream(NativeMidiSong *song, MIDIEvent *evntlist)
{
int eventcount;
MIDIEvent *event;
MIDIEVENT *newevent;
eventcount=0;
event=evntlist;
while (event)
{
eventcount++;
event=event->next;
}
song->NewEvents=malloc(eventcount*3*sizeof(DWORD));
if (!song->NewEvents)
return;
memset(song->NewEvents,0,(eventcount*3*sizeof(DWORD)));
eventcount=0;
event=evntlist;
newevent=song->NewEvents;
while (event)
{
int status = (event->status&0xF0)>>4;
switch (status)
{
case MIDI_STATUS_NOTE_OFF:
case MIDI_STATUS_NOTE_ON:
case MIDI_STATUS_AFTERTOUCH:
case MIDI_STATUS_CONTROLLER:
case MIDI_STATUS_PROG_CHANGE:
case MIDI_STATUS_PRESSURE:
case MIDI_STATUS_PITCH_WHEEL:
newevent->dwDeltaTime=event->time;
newevent->dwEvent=(event->status|0x80)|(event->data[0]<<8)|(event->data[1]<<16)|(MEVT_SHORTMSG<<24);
newevent=(MIDIEVENT*)((char*)newevent+(3*sizeof(DWORD)));
eventcount++;
break;
case MIDI_STATUS_SYSEX:
if (event->status == 0xFF && event->data[0] == 0x51) /* Tempo change */
{
int tempo = (event->extraData[0] << 16) |
(event->extraData[1] << 8) |
event->extraData[2];
newevent->dwDeltaTime=event->time;
newevent->dwEvent=(MEVT_TEMPO<<24) | tempo;
newevent=(MIDIEVENT*)((char*)newevent+(3*sizeof(DWORD)));
eventcount++;
}
break;
}
event=event->next;
}
song->Size=eventcount*3*sizeof(DWORD);
{
int time;
int temptime;
song->NewPos=0;
time=0;
newevent=song->NewEvents;
while (song->NewPos<song->Size)
{
temptime=newevent->dwDeltaTime;
newevent->dwDeltaTime-=time;
time=temptime;
if ((song->NewPos+12)>=song->Size)
newevent->dwEvent |= MEVT_F_CALLBACK;
newevent=(MIDIEVENT*)((char*)newevent+(3*sizeof(DWORD)));
song->NewPos+=12;
}
}
song->NewPos=0;
song->MusicLoaded=1;
}
void CALLBACK MidiProc( HMIDIIN hMidi, UINT uMsg, DWORD dwInstance,
DWORD dwParam1, DWORD dwParam2 )
{
switch( uMsg )
{
case MOM_DONE:
if ((currentsong->MusicLoaded) && ((DWORD)dwParam1 == (DWORD)&currentsong->MidiStreamHdr))
BlockOut(currentsong);
break;
case MOM_POSITIONCB:
if ((currentsong->MusicLoaded) && ((DWORD)dwParam1 == (DWORD)&currentsong->MidiStreamHdr))
currentsong->MusicPlaying=0;
break;
default:
break;
}
}
int native_midi_detect()
{
MMRESULT merr;
HMIDISTRM MidiStream;
merr=midiStreamOpen(&MidiStream,&MidiDevice,1,(DWORD)&MidiProc,0,CALLBACK_FUNCTION);
if (merr!=MMSYSERR_NOERROR)
MidiStream=0;
midiStreamClose(MidiStream);
if (!MidiStream)
return 0;
else
return 1;
}
NativeMidiSong *native_midi_loadsong(char *midifile)
{
NativeMidiSong *newsong;
MIDIEvent *evntlist = NULL;
SDL_RWops *rw;
newsong=malloc(sizeof(NativeMidiSong));
if (!newsong)
return NULL;
memset(newsong,0,sizeof(NativeMidiSong));
/* Attempt to load the midi file */
rw = SDL_RWFromFile(midifile, "rb");
if (rw) {
evntlist = CreateMIDIEventList(rw, &newsong->ppqn);
SDL_RWclose(rw);
if (!evntlist)
{
free(newsong);
return NULL;
}
}
MIDItoStream(newsong, evntlist);
FreeMIDIEventList(evntlist);
return newsong;
}
NativeMidiSong *native_midi_loadsong_RW(SDL_RWops *rw)
{
NativeMidiSong *newsong;
MIDIEvent *evntlist = NULL;
newsong=malloc(sizeof(NativeMidiSong));
if (!newsong)
return NULL;
memset(newsong,0,sizeof(NativeMidiSong));
/* Attempt to load the midi file */
evntlist = CreateMIDIEventList(rw, &newsong->ppqn);
if (!evntlist)
{
free(newsong);
return NULL;
}
MIDItoStream(newsong, evntlist);
FreeMIDIEventList(evntlist);
return newsong;
}
void native_midi_freesong(NativeMidiSong *song)
{
if (hMidiStream)
{
midiStreamStop(hMidiStream);
midiStreamClose(hMidiStream);
}
if (song)
{
if (song->NewEvents)
free(song->NewEvents);
free(song);
}
}
void native_midi_start(NativeMidiSong *song)
{
MMRESULT merr;
MIDIPROPTIMEDIV mptd;
native_midi_stop();
if (!hMidiStream)
{
merr=midiStreamOpen(&hMidiStream,&MidiDevice,1,(DWORD)&MidiProc,0,CALLBACK_FUNCTION);
if (merr!=MMSYSERR_NOERROR)
{
hMidiStream=0;
return;
}
//midiStreamStop(hMidiStream);
currentsong=song;
currentsong->NewPos=0;
currentsong->MusicPlaying=1;
mptd.cbStruct=sizeof(MIDIPROPTIMEDIV);
mptd.dwTimeDiv=currentsong->ppqn;
merr=midiStreamProperty(hMidiStream,(LPBYTE)&mptd,MIDIPROP_SET | MIDIPROP_TIMEDIV);
BlockOut(song);
merr=midiStreamRestart(hMidiStream);
}
}
void native_midi_stop()
{
if (!hMidiStream)
return;
midiStreamStop(hMidiStream);
midiStreamClose(hMidiStream);
currentsong=NULL;
hMidiStream = 0;
}
int native_midi_active()
{
return currentsong->MusicPlaying;
}
void native_midi_setvolume(int volume)
{
int calcVolume;
if (volume > 128)
volume = 128;
if (volume < 0)
volume = 0;
calcVolume = (65535 * volume / 128);
midiOutSetVolume((HMIDIOUT)hMidiStream, MAKELONG(calcVolume , calcVolume));
}
char *native_midi_error()
{
return "";
}
#endif /* Windows native MIDI support */
+104
View File
@@ -0,0 +1,104 @@
# Copyright 2005 PyTom <pytom@bishoujo.us>
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
cdef extern from "pss.h":
cdef struct SDL_RWops:
int (* close)(SDL_RWops *)
SDL_RWops* RWopsFromPython(object obj)
cdef int SDL_GetTicks()
cdef extern from "native_midi.h":
cdef struct _NativeMidiSong:
pass
ctypedef _NativeMidiSong NativeMidiSong
int native_midi_detect()
NativeMidiSong *native_midi_loadsong(char *midifile)
NativeMidiSong *native_midi_loadsong_RW(SDL_RWops *rw)
void native_midi_freesong(NativeMidiSong *song)
void native_midi_start(NativeMidiSong *song)
void native_midi_stop()
int native_midi_active()
void native_midi_setvolume(int volume)
char *native_midi_error()
cdef NativeMidiSong *current
cdef int start
current = NULL
def init():
if not native_midi_detect():
raise Exception("Could not initialize native midi support.")
def play(file):
cdef SDL_RWops *rw
if current:
stop()
rw = RWopsFromPython(file)
if rw == NULL:
raise Exception, "Could not create RWops."
global current
current = native_midi_loadsong_RW(rw)
rw.close(rw)
if current == NULL:
raise Exception, "Could not load midi file."
global start
start = SDL_GetTicks()
native_midi_start(current)
def stop():
global current
if not current:
return
native_midi_stop()
native_midi_freesong(current)
current = NULL
def busy():
return native_midi_active()
def get_pos():
if not busy():
return -1
return SDL_GetTicks() - start
def set_volume(volume):
native_midi_setvolume(int(volume * 128))
+865
View File
@@ -0,0 +1,865 @@
/*
Copyright 2005 PyTom <pytom@bishoujo.us>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "pss.h"
#include <Python.h>
#include <SDL/SDL.h>
#include <SDL/SDL_sound.h>
#include <stdio.h>
/* The current Python. */
PyInterpreterState* interp;
PyThreadState* thread = NULL;
static void incref(PyObject *ref) {
PyThreadState *oldstate;
PyEval_AcquireLock();
oldstate = PyThreadState_Swap(thread);
Py_INCREF(ref);
PyThreadState_Swap(oldstate);
PyEval_ReleaseLock();
}
static void decref(PyObject *ref) {
PyThreadState *oldstate;
PyEval_AcquireLock();
oldstate = PyThreadState_Swap(thread);
Py_DECREF(ref);
PyThreadState_Swap(oldstate);
PyEval_ReleaseLock();
}
/* Locking on entry from python... */
#define BEGIN() PyThreadState *_save;
#define ENTER() { _save = PyEval_SaveThread(); SDL_LockAudio(); }
#define EXIT() { SDL_UnlockAudio(); PyEval_RestoreThread(_save); }
/* Min and Max */
#define min(a, b) (((a) < (b)) ? (a) : (b))
#define max(a, b) (((a) > (b)) ? (a) : (b))
/* The number of channels we support. */
#define NUM_CHANNELS 8
/* Various error codes. */
#define SUCCESS 0
#define SDL_ERROR -1
#define SOUND_ERROR -2
#define PSS_ERROR -3
/* This is called with the appropriate error code at the end of a
* function. */
#define error(err) PSS_error = err
int PSS_error = SUCCESS;
static const char *error_msg = NULL;
/* Have we been initialized? */
static int initialized = 0;
/*
* This structure represents a channel the system knows about
* and can play from.
*/
struct Channel {
/* The currently playing sample, NULL if this sample isn't playing
anything. */
Sound_Sample *playing;
/* The name of the playing music. */
PyObject *playing_name;
/* The queued up sample. */
Sound_Sample *queued;
/* The name of the queued up sample. */
PyObject *queued_name;
/* Is this channel paused? */
int paused;
/* The volume of the channel. */
int volume;
/* The number of decoded bytes in the buffer. */
int decoded;
/* The offset into the buffer that we last stopped at. */
int last;
/* The position (in bytes) that this channel has queued to. */
int pos;
/*
* The number of bytes for each step of fade.
* 0 when no fade is in progress.
*/
int fade_step_len;
/* How many bytes we are into the current fade step. */
int fade_off;
/* The current fade volume. */
int fade_vol;
/* The change in fade_vol for each step. */
int fade_delta;
/* The number of bytes in which we'll stop. */
int stop_bytes;
/* The event posted to the queue when we finish a track. */
int event;
};
/*
* All of the channels that the system knows about.
*/
struct Channel channels[NUM_CHANNELS];
/*
* The spec of the audio that is playing.
*/
SDL_AudioSpec audio_spec;
static int ms_to_bytes(int ms) {
return ms * audio_spec.freq * audio_spec.channels * 2 / 1000;
}
static int bytes_to_ms(int bytes) {
return ((long long) bytes) * 1000 / (audio_spec.freq * audio_spec.channels * 2);
}
static void start_sample(struct Channel* c) {
if (!c) return;
c->decoded = 0;
c->last = 0;
c->pos = 0;
c->fade_step_len = 0;
c->stop_bytes = -1;
}
static void update_pause(void) {
int i;
int pause = 1;
for (i = 0; i < NUM_CHANNELS; i++) {
if (channels[i].playing) {
pause = 0;
break;
}
}
SDL_PauseAudio(pause);
}
static void free_sample(Sound_Sample *ss) {
Sound_FreeSample(ss);
}
// Mixes the audio, while performing fading.
static void fade_mixaudio(struct Channel *c,
Uint8 *dst, Uint8 *src, int length) {
while (length) {
// No fade case.
if (c->fade_step_len == 0) {
SDL_MixAudio(dst, src, length, c->volume);
return;
}
// Fading, but we have some space left in the current step.
if (c->fade_off < c->fade_step_len) {
int l = min(c->fade_step_len - c->fade_off, length);
SDL_MixAudio(dst, src, l, c->fade_vol);
length -= l;
dst += l;
src += l;
c->fade_off += l;
}
// Otherwise, we have no space left in the current fade step.
// Go to the next step.
c->fade_off = 0;
c->fade_vol += c->fade_delta;
// Don't stop on a fadeout.
if (c->fade_vol <= 0) {
c->fade_vol = 0;
}
// Stop on a fadein.
if (c->fade_vol >= c->volume) {
c->fade_vol = c->volume;
c->fade_step_len = 0;
}
}
return;
}
static void post_event(struct Channel *c) {
if (! c->event) {
return;
}
SDL_Event e;
memset(&e, 0, sizeof(e));
e.type = c->event;
SDL_PushEvent(&e);
}
static void callback(void *userdata, Uint8 *stream, int length) {
int channel = 0;
for (channel = 0; channel < NUM_CHANNELS; channel++) {
int mixed = 0;
struct Channel *c = &channels[channel];
if (! c->playing) {
continue;
}
if (c->paused) {
continue;
}
while (mixed < length && c->playing) {
int mixleft = length - mixed;
int bufleft = c->decoded - c->last;
// We have some data in the buffer.
if (c->stop_bytes && bufleft) {
int bytes = min(bufleft, mixleft);
if (c->stop_bytes != -1)
bytes = min(c->stop_bytes, bytes);
fade_mixaudio(c, &stream[mixed],
&( ((Uint8*) c->playing->buffer) [c->last]),
bytes);
mixed += bytes;
c->last += bytes;
if (c->stop_bytes != -1)
c->stop_bytes -= bytes;
c->pos += bytes;
continue;
}
// Otherwise, no data is left in the buffer. Check why,
// and act accordingly.
// EOF or ERROR -- Skip to the next sample.
if (c->stop_bytes == 0 || c->playing->flags & (SOUND_SAMPLEFLAG_ERROR | SOUND_SAMPLEFLAG_EOF)) {
post_event(c);
free_sample(c->playing);
decref(c->playing_name);
c->playing = c->queued;
c->playing_name = c->queued_name;
c->queued = NULL;
c->queued_name = NULL;
start_sample(c);
update_pause();
continue;
}
// If we're here, we're simply out of data in the
// buffer. Decode some.
c->decoded = Sound_Decode(c->playing);
c->last = 0;
}
}
}
/*
* Checks that the given channel is in range. Returns 0 if it is,
* sets an error and returns -1 if it is not.
*/
static int check_channel(int c) {
if (c < 0 || c >= NUM_CHANNELS) {
error(PSS_ERROR);
error_msg = "Channel number out of range.";
return -1;
}
return 0;
}
/*
* Loads the provided sample. Returns the sample on success, NULL on
* failure.
*/
static Sound_Sample *load_sample(SDL_RWops *rw, const char *ext) {
Sound_AudioInfo ai;
Sound_Sample *rv;
ai.format = audio_spec.format;
ai.channels = audio_spec.channels;
ai.rate = audio_spec.freq;
rv = Sound_NewSample(rw, ext, &ai, audio_spec.samples * audio_spec.channels * 2);
return rv;
}
void PSS_play(int channel, SDL_RWops *rw, const char *ext, PyObject *name, int paused) {
BEGIN();
struct Channel *c;
if (check_channel(channel)) {
return;
}
c = &channels[channel];
ENTER();
/* Free playing and queued samples. */
if (c->playing) {
free_sample(c->playing);
c->playing = NULL;
decref(c->playing_name);
c->playing_name = NULL;
}
if (c->queued) {
free_sample(c->queued);
c->queued = NULL;
decref(c->queued_name);
c->queued_name = NULL;
}
/* Allocate playing sample. */
c->playing = load_sample(rw, ext);
if (! c->playing) {
EXIT();
error(SOUND_ERROR);
return;
}
incref(name);
c->playing_name = name;
c->paused = paused;
start_sample(c);
update_pause();
EXIT();
error(SUCCESS);
}
void PSS_queue(int channel, SDL_RWops *rw, const char *ext, PyObject *name) {
BEGIN();
struct Channel *c;
if (check_channel(channel)) {
return;
}
c = &channels[channel];
ENTER();
/* If we're not playing, then we should play instead of queue. */
if (!c->playing) {
EXIT();
PSS_play(channel, rw, ext, name, 0);
return;
}
/* Free queued sample. */
if (c->queued) {
free_sample(c->queued);
c->queued = NULL;
decref(c->queued_name);
c->queued_name = NULL;
}
/* Allocate queued sample. */
c->queued = load_sample(rw, ext);
if (! c->queued) {
EXIT();
error(SOUND_ERROR);
return;
}
incref(name);
c->queued_name = name;
EXIT();
error(SUCCESS);
}
/*
* Stops all music from playing, freeing the data used by the
* music.
*/
void PSS_stop(int channel) {
BEGIN();
struct Channel *c;
if (check_channel(channel)) {
return;
}
c = &channels[channel];
ENTER();
if (c->playing) {
post_event(c);
}
/* Free playing and queued samples. */
if (c->playing) {
free_sample(c->playing);
c->playing = NULL;
decref(c->playing_name);
c->playing_name = NULL;
}
if (c->queued) {
free_sample(c->queued);
c->queued = NULL;
decref(c->queued_name);
c->queued_name = NULL;
}
update_pause();
EXIT();
error(SUCCESS);
}
/*
* This dequeues the queued sound from the supplied channel, if
* such a sound is queued. This does nothing to the playing
* sound.
*/
void PSS_dequeue(int channel) {
BEGIN();
struct Channel *c;
if (check_channel(channel)) {
return;
}
c = &channels[channel];
ENTER();
if (c->queued) {
free_sample(c->queued);
c->queued = NULL;
decref(c->queued_name);
c->queued_name = NULL;
}
EXIT();
error(SUCCESS);
}
/*
* Returns the queue depth of the current channel. This is 0 if we're
* stopped, 1 if there's something playing but nothing queued, and 2
* if there's both something playing and something queued.
*/
int PSS_queue_depth(int channel) {
int rv = 0;
BEGIN();
struct Channel *c;
if (check_channel(channel)) {
return 0;
}
c = &channels[channel];
ENTER();
if (c->playing) rv++;
if (c->queued) rv++;
EXIT();
error(SUCCESS);
return rv;
}
PyObject *PSS_playing_name(int channel) {
BEGIN();
PyObject *rv;
struct Channel *c;
if (check_channel(channel)) {
Py_INCREF(Py_None);
return Py_None;
}
ENTER();
c = &channels[channel];
if (c->playing_name) {
rv = c->playing_name;
} else {
rv = Py_None;
}
incref(rv);
EXIT();
error(SUCCESS);
return rv;
}
/*
* Causes the given channel to fadeout playing after a specified
* number of milliseconds. The playing sound stops once the
* fadeout finishes (a queued sound may then start at full volume).
*/
void PSS_fadeout(int channel, int ms) {
BEGIN();
int fade_steps;
struct Channel *c;
if (check_channel(channel)) {
return;
}
c = &channels[channel];
ENTER();
if (ms == 0) {
c->stop_bytes = 0;
EXIT();
error(SUCCESS);
return;
}
fade_steps = c->volume;
c->fade_delta = -1;
c->fade_off = 0;
c->fade_vol = c->volume;
c->fade_step_len = ms_to_bytes(ms) / fade_steps;
c->fade_step_len &= ~0x7; // Even sample.
c->stop_bytes = ms_to_bytes(ms);
EXIT();
error(SUCCESS);
}
/*
* Sets the pause flag on the given channel 0 = unpaused, 1 = paused.
*/
void PSS_pause(int channel, int pause) {
BEGIN();
struct Channel *c;
if (check_channel(channel)) {
return;
}
c = &channels[channel];
ENTER();
c->paused = pause;
EXIT();
error(SUCCESS);
}
void PSS_unpause_all(void) {
int i;
BEGIN();
ENTER();
for (i = 0; i < NUM_CHANNELS; i++) {
channels[i].paused = 0;
}
EXIT();
error(SUCCESS);
}
/*
* Returns the position of the given channel, in ms.
*/
int PSS_get_pos(int channel) {
int rv;
struct Channel *c;
BEGIN();
if (check_channel(channel)) {
return -1;
}
c = &channels[channel];
ENTER();
if (c->playing) {
rv = bytes_to_ms(c->pos);
} else {
rv = -1;
}
EXIT();
error(SUCCESS);
return rv;
}
/*
* Sets an event that is queued up when the track on the given channel
* ends due to natural termination or a forced stop.
*/
void PSS_set_endevent(int channel, int event) {
struct Channel *c;
BEGIN();
if (check_channel(channel)) {
return;
}
c = &channels[channel];
ENTER();
c->event = event;
EXIT();
error(SUCCESS);
}
/*
* This sets the natural volume of the channel. (This may not take
* effect immediately if a fade is going on.)
*/
void PSS_set_volume(int channel, float volume) {
struct Channel *c;
BEGIN();
if (check_channel(channel)) {
return;
}
c = &channels[channel];
ENTER();
c->volume = (int) (volume * SDL_MIX_MAXVOLUME);
EXIT();
error(SUCCESS);
}
float PSS_get_volume(int channel) {
float rv;
struct Channel *c;
BEGIN();
if (check_channel(channel)) {
return 0.0;
}
c = &channels[channel];
ENTER();
rv = 1.0 * c->volume / SDL_MIX_MAXVOLUME;
EXIT();
error(SUCCESS);
return rv;
}
/*
* Initializes the sound to the given frequencies, channels, and
* sample buffer size.
*/
void PSS_init(int freq, int stereo, int samples) {
int i;
if (initialized) {
return;
}
PyEval_InitThreads();
if (!thread) {
thread = PyThreadState_Get();
interp = thread->interp;
thread = PyThreadState_New(interp);
}
if (!thread) {
error(SDL_ERROR);
return;
}
if (SDL_Init(SDL_INIT_AUDIO)) {
error(SDL_ERROR);
return;
}
audio_spec.freq = freq;
audio_spec.format = AUDIO_S16SYS;
audio_spec.channels = stereo;
audio_spec.samples = samples;
audio_spec.callback = callback;
audio_spec.userdata = NULL;
if (SDL_OpenAudio(&audio_spec, NULL)) {
error(SDL_ERROR);
return;
}
if (! Sound_Init()) {
SDL_CloseAudio();
error(SOUND_ERROR);
return;
}
/*
* Initialize the channels.
*/
for (i = 0; i < NUM_CHANNELS; i++) {
channels[i].playing = NULL;
channels[i].queued = NULL;
channels[i].volume = SDL_MIX_MAXVOLUME;
channels[i].paused = 1;
channels[i].event = 0;
}
initialized = 1;
error(SUCCESS);
}
void PSS_quit() {
BEGIN();
if (! initialized) {
return;
}
int i;
ENTER();
SDL_PauseAudio(1);
EXIT();
for (i = 0; i < NUM_CHANNELS; i++) {
PSS_stop(i);
}
Sound_Quit();
SDL_CloseAudio();
initialized = 0;
error(SUCCESS);
}
/*
* Returns the error message string if an error has occured, or
* NULL if no error has happened.
*/
const char *PSS_get_error() {
switch(PSS_error) {
case 0:
return "";
case SDL_ERROR:
return SDL_GetError();
case SOUND_ERROR:
return Sound_GetError();
case PSS_ERROR:
return error_msg;
default:
return "Error getting error.";
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright 2005 PyTom <pytom@bishoujo.us>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef PSS_H
#define PSS_H
#include <Python.h>
#include <SDL/SDL.h>
SDL_RWops* RWopsFromPythonThreaded(PyObject* obj);
void PSS_play(int channel, SDL_RWops *rw, const char *ext, PyObject *name, int paused);
void PSS_queue(int channel, SDL_RWops *rw, const char *ext, PyObject *name);
void PSS_stop(int channel);
void PSS_dequeue(int channel);
int PSS_queue_depth(int channel);
PyObject *PSS_playing_name(int channel);
void PSS_fadeout(int channel, int ms);
void PSS_pause(int channel, int pause);
void PSS_unpause_all(void);
void PSS_set_endevent(int channel, int event);
int PSS_get_pos(int channel);
void PSS_set_volume(int channel, float volume);
float PSS_get_volume(int channel);
void PSS_init(int freq, int stereo, int samples);
void PSS_quit(void);
const char *PSS_get_error(void);
#endif
+136
View File
@@ -0,0 +1,136 @@
# Copyright 2005 PyTom <pytom@bishoujo.us>
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
cdef extern from "pss.h":
cdef struct SDL_RWops:
pass
SDL_RWops* RWopsFromPythonThreaded(object obj)
void PSS_play(int channel, SDL_RWops *rw, char *ext, object name, int paused)
void PSS_queue(int channel, SDL_RWops *rw, char *ext, object name)
void PSS_stop(int channel)
void PSS_dequeue(int channel)
int PSS_queue_depth(int channel)
object PSS_playing_name(int channel)
void PSS_fadeout(int channel, int ms)
void PSS_pause(int channel, int pause)
void PSS_unpause_all()
int PSS_get_pos(int channel)
void PSS_set_endevent(int channel, int event)
void PSS_set_volume(int channel, float volume)
float PSS_get_volume(int channel)
void PSS_init(int freq, int stereo, int samples)
void PSS_quit()
char *PSS_get_error()
def _extension(s):
i = s.rfind('.')
if i == -1:
return s
return s[i+1:]
def check_error():
e = PSS_get_error();
if e:
raise Exception, e
def play(channel, file, name, paused=False):
cdef SDL_RWops *rw
rw = RWopsFromPythonThreaded(file)
if rw == NULL:
raise Exception, "Could not create RWops."
if paused:
pause = 1
else:
pause = 0
ext = _extension(name)
PSS_play(channel, rw, ext, name, pause)
check_error()
def queue(channel, file, name):
cdef SDL_RWops *rw
rw = RWopsFromPythonThreaded(file)
ext = _extension(name)
PSS_queue(channel, rw, ext, name)
check_error()
def stop(channel):
PSS_stop(channel)
check_error()
def dequeue(channel):
PSS_dequeue(channel)
def queue_depth(channel):
return PSS_queue_depth(channel)
def playing_name(channel):
return PSS_playing_name(channel)
def pause(channel):
PSS_pause(channel, 1)
check_error()
def unpause(channel):
PSS_pause(channel, 0)
check_error()
def unpause_all():
PSS_unpause_all()
def fadeout(channel, ms):
PSS_fadeout(channel, ms)
check_error()
def busy(channel):
return PSS_get_pos(channel) != -1
def get_pos(channel):
return PSS_get_pos(channel)
def set_volume(channel, volume):
PSS_set_volume(channel, 10 ** volume / 10 )
check_error()
def set_end_event(channel, event):
PSS_set_endevent(channel, event)
check_error()
def get_volume(channel):
return PSS_get_volume(channel)
def init(freq, stereo, samples):
PSS_init(freq, stereo, samples)
check_error()
def quit():
PSS_quit()
+53
View File
@@ -0,0 +1,53 @@
#ifndef RENPY_H
#define RENPY_H
#include <Python.h>
void core_init(void);
void pixellate32_core(PyObject *pysrc,
PyObject *pydst,
int avgwidth,
int avgheight,
int outwidth,
int outheight);
void pixellate24_core(PyObject *pysrc,
PyObject *pydst,
int avgwidth,
int avgheight,
int outwidth,
int outheight);
void map32_core(PyObject *pysrc,
PyObject *pydst,
char *rmap,
char *gmap,
char *bmap,
char *amap);
void map24_core(PyObject *pysrc,
PyObject *pydst,
char *rmap,
char *gmap,
char *bmap);
#if 0
void xblur32_core(PyObject *pysrc,
PyObject *pydst,
int radius);
#endif
void alphamunge_core(PyObject *pysrc,
PyObject *pydst,
int src_bypp, // bytes per pixel.
int src_aoff, // alpha offset.
int dst_aoff, // alpha offset.
char *amap);
#endif
+430
View File
@@ -0,0 +1,430 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
/*
* Updated by Tom to separate from pygame.
*/
#define RAISE(x,y) (PyErr_SetString((x), (y)), (PyObject*)NULL)
/*
* SDL_RWops support for python objects
*/
#include <Python.h>
#include <SDL/SDL.h>
typedef struct
{
PyObject* read;
PyObject* write;
PyObject* seek;
PyObject* tell;
PyObject* close;
#ifdef WITH_THREAD
PyThreadState* thread;
#endif
}RWHelper;
static int rw_seek(SDL_RWops* context, int offset, int whence);
static int rw_read(SDL_RWops* context, void* ptr, int size, int maxnum);
static int rw_write(SDL_RWops* context, const void* ptr, int size, int maxnum);
static int rw_close(SDL_RWops* context);
#ifdef WITH_THREAD
static int rw_seek_th(SDL_RWops* context, int offset, int whence);
static int rw_read_th(SDL_RWops* context, void* ptr, int size, int maxnum);
static int rw_write_th(SDL_RWops* context, const void* ptr, int size, int maxnum);
static int rw_close_th(SDL_RWops* context);
#endif
static SDL_RWops* get_standard_rwop(PyObject* obj)
{
if(PyString_Check(obj) || PyUnicode_Check(obj))
{
int result;
char* name;
PyObject* tuple = PyTuple_New(1);
PyTuple_SET_ITEM(tuple, 0, obj);
Py_INCREF(obj);
if(!tuple) return NULL;
result = PyArg_ParseTuple(tuple, "s", &name);
Py_DECREF(tuple);
if(!result)
return NULL;
return SDL_RWFromFile(name, "rb");
}
// else if(PyFile_Check(obj))
// return SDL_RWFromFP(PyFile_AsFile(obj), 0);
return NULL;
}
static void fetch_object_methods(RWHelper* helper, PyObject* obj)
{
helper->read = helper->write = helper->seek =
helper->tell = helper->close = NULL;
if(PyObject_HasAttrString(obj, "read"))
{
helper->read = PyObject_GetAttrString(obj, "read");
if(helper->read && !PyCallable_Check(helper->read))
helper->read = NULL;
}
if(PyObject_HasAttrString(obj, "write"))
{
helper->write = PyObject_GetAttrString(obj, "write");
if(helper->write && !PyCallable_Check(helper->write))
helper->write = NULL;
}
if(PyObject_HasAttrString(obj, "seek"))
{
helper->seek = PyObject_GetAttrString(obj, "seek");
if(helper->seek && !PyCallable_Check(helper->seek))
helper->seek = NULL;
}
if(PyObject_HasAttrString(obj, "tell"))
{
helper->tell = PyObject_GetAttrString(obj, "tell");
if(helper->tell && !PyCallable_Check(helper->tell))
helper->tell = NULL;
}
if(PyObject_HasAttrString(obj, "close"))
{
helper->close = PyObject_GetAttrString(obj, "close");
if(helper->close && !PyCallable_Check(helper->close))
helper->close = NULL;
}
}
SDL_RWops* RWopsFromPython(PyObject* obj)
{
SDL_RWops* rw;
RWHelper* helper;
if(!obj)
return (SDL_RWops*) RAISE(PyExc_TypeError, "Invalid filetype object");
rw = get_standard_rwop(obj);
if(rw) return rw;
helper = PyMem_New(RWHelper, 1);
fetch_object_methods(helper, obj);
rw = SDL_AllocRW();
rw->hidden.unknown.data1 = (void*)helper;
rw->seek = rw_seek;
rw->read = rw_read;
rw->write = rw_write;
rw->close = rw_close;
return rw;
}
static int RWopsCheckPython(SDL_RWops* rw)
{
return rw->close == rw_close;
}
static int rw_seek(SDL_RWops* context, int offset, int whence)
{
RWHelper* helper = (RWHelper*)context->hidden.unknown.data1;
PyObject* result;
int retval;
if(!helper->seek || !helper->tell)
return -1;
if(!(offset == 0 && whence == SEEK_CUR)) /*being called only for 'tell'*/
{
result = PyObject_CallFunction(helper->seek, "ii", offset, whence);
if(!result)
return -1;
Py_DECREF(result);
}
result = PyObject_CallFunction(helper->tell, NULL);
if(!result)
return -1;
retval = PyInt_AsLong(result);
Py_DECREF(result);
return retval;
}
static int rw_read(SDL_RWops* context, void* ptr, int size, int maxnum)
{
RWHelper* helper = (RWHelper*)context->hidden.unknown.data1;
PyObject* result;
int retval;
if(!helper->read)
return -1;
result = PyObject_CallFunction(helper->read, "i", size * maxnum);
if(!result)
return -1;
if(!PyString_Check(result))
{
Py_DECREF(result);
return -1;
}
retval = PyString_GET_SIZE(result);
memcpy(ptr, PyString_AsString(result), retval);
retval /= size;
Py_DECREF(result);
return retval;
}
static int rw_write(SDL_RWops* context, const void* ptr, int size, int num)
{
RWHelper* helper = (RWHelper*)context->hidden.unknown.data1;
PyObject* result;
if(!helper->write)
return -1;
result = PyObject_CallFunction(helper->write, "s#", ptr, size * num);
if(!result)
return -1;
Py_DECREF(result);
return num;
}
static int rw_close(SDL_RWops* context)
{
RWHelper* helper = (RWHelper*)context->hidden.unknown.data1;
PyObject* result;
int retval = 0;
if(helper->close)
{
result = PyObject_CallFunction(helper->close, NULL);
if(result)
retval = -1;
Py_XDECREF(result);
}
Py_XDECREF(helper->seek);
Py_XDECREF(helper->tell);
Py_XDECREF(helper->write);
Py_XDECREF(helper->read);
Py_XDECREF(helper->close);
PyMem_Del(helper);
SDL_FreeRW(context);
return retval;
}
SDL_RWops* RWopsFromPythonThreaded(PyObject* obj)
{
SDL_RWops* rw;
RWHelper* helper;
PyInterpreterState* interp;
PyThreadState* thread;
if(!obj)
return (SDL_RWops*)RAISE(PyExc_TypeError, "Invalid filetype object");
rw = get_standard_rwop(obj);
if(rw)
return rw;
#ifndef WITH_THREAD
return (SDL_RWops*)RAISE(PyExc_NotImplementedError, "Python built without thread support");
#else
helper = PyMem_New(RWHelper, 1);
fetch_object_methods(helper, obj);
rw = SDL_AllocRW();
rw->hidden.unknown.data1 = (void*)helper;
rw->seek = rw_seek_th;
rw->read = rw_read_th;
rw->write = rw_write_th;
rw->close = rw_close_th;
PyEval_InitThreads();
thread = PyThreadState_Get();
interp = thread->interp;
helper->thread = PyThreadState_New(interp);
return rw;
#endif
}
static int RWopsCheckPythonThreaded(SDL_RWops* rw)
{
#ifdef WITH_THREAD
return rw->close == rw_close_th;
#else
return 0;
#endif
}
#ifdef WITH_THREAD
static int rw_seek_th(SDL_RWops* context, int offset, int whence)
{
RWHelper* helper = (RWHelper*)context->hidden.unknown.data1;
PyObject* result;
int retval;
PyThreadState* oldstate;
if(!helper->seek || !helper->tell)
return -1;
PyEval_AcquireLock();
oldstate = PyThreadState_Swap(helper->thread);
if(!(offset == 0 && whence == SEEK_CUR)) /*being called only for 'tell'*/
{
result = PyObject_CallFunction(helper->seek, "ii", offset, whence);
if(!result)
return -1;
Py_DECREF(result);
}
result = PyObject_CallFunction(helper->tell, NULL);
if(!result)
return -1;
retval = PyInt_AsLong(result);
Py_DECREF(result);
PyThreadState_Swap(oldstate);
PyEval_ReleaseLock();
return retval;
}
static int rw_read_th(SDL_RWops* context, void* ptr, int size, int maxnum)
{
RWHelper* helper = (RWHelper*)context->hidden.unknown.data1;
PyObject* result;
int retval;
PyThreadState* oldstate;
if(!helper->read)
return -1;
PyEval_AcquireLock();
oldstate = PyThreadState_Swap(helper->thread);
result = PyObject_CallFunction(helper->read, "i", size * maxnum);
if(!result)
return -1;
if(!PyString_Check(result))
{
Py_DECREF(result);
return -1;
}
retval = PyString_GET_SIZE(result);
memcpy(ptr, PyString_AsString(result), retval);
retval /= size;
Py_DECREF(result);
PyThreadState_Swap(oldstate);
PyEval_ReleaseLock();
return retval;
}
static int rw_write_th(SDL_RWops* context, const void* ptr, int size, int num)
{
RWHelper* helper = (RWHelper*)context->hidden.unknown.data1;
PyObject* result;
PyThreadState* oldstate;
if(!helper->write)
return -1;
PyEval_AcquireLock();
oldstate = PyThreadState_Swap(helper->thread);
result = PyObject_CallFunction(helper->write, "s#", ptr, size * num);
if(!result)
return -1;
Py_DECREF(result);
PyThreadState_Swap(oldstate);
PyEval_ReleaseLock();
return num;
}
static int rw_close_th(SDL_RWops* context)
{
RWHelper* helper = (RWHelper*)context->hidden.unknown.data1;
PyObject* result;
int retval = 0;
PyThreadState* oldstate;
PyEval_AcquireLock();
oldstate = PyThreadState_Swap(helper->thread);
if(helper->close)
{
result = PyObject_CallFunction(helper->close, NULL);
if(result)
retval = -1;
Py_XDECREF(result);
}
Py_XDECREF(helper->seek);
Py_XDECREF(helper->tell);
Py_XDECREF(helper->write);
Py_XDECREF(helper->read);
Py_XDECREF(helper->close);
PyMem_Del(helper);
PyThreadState_Swap(oldstate);
PyThreadState_Clear(helper->thread);
PyThreadState_Delete(helper->thread);
PyEval_ReleaseLock();
SDL_FreeRW(context);
return retval;
}
#endif
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python
# Set this to false to disable the use of sdl-config to automatically
# figure out the compile arguments.
auto_configure = True
# The following are only respected if auto_configure is False.
include_dirs = [ "." ]
libraries = [ "SDL" ]
extra_compile_args = [ ]
extra_link_args = [ ]
# The following turn on optional modules.
nativemidi = None
winmixer = None
import distutils.core
def common():
extensions = [ ]
rpe = distutils.core.Extension(
"_renpy",
[ "core.c", "_renpy.c" ],
extra_compile_args=extra_compile_args,
extra_link_args=extra_link_args,
include_dirs=include_dirs,
libraries=libraries,
)
extensions.append(rpe)
psse = distutils.core.Extension(
"pysdlsound",
[ "pss.c", "rwobject.c", "pysdlsound.c" ],
extra_compile_args=extra_compile_args,
extra_link_args=extra_link_args,
include_dirs=include_dirs,
libraries=[ "SDL_sound" ] + libraries,
)
extensions.append(psse)
if nativemidi:
nme = distutils.core.Extension(
"nativemidi",
nativemidi,
extra_compile_args=extra_compile_args,
libraries=nativemidi_libs,
include_dirs=include_dirs,
extra_link_args=extra_link_args,
)
extensions.append(nme)
if winmixer:
wme = distutils.core.Extension(
"winmixer",
[ 'winmixer.c' ],
libraries=['winmm'],
)
extensions.append(wme)
distutils.core.setup(
name = "renpy_module",
version = "5.1.0",
ext_modules = extensions,
)
if __name__ == "__main__":
if auto_configure:
try:
import os
extra_compile_args = os.popen("sdl-config --cflags").read().split()
extra_link_args = os.popen("sdl-config --libs").read().split()
include_dirs = [ ]
libraries = [ ]
except:
print "I was unable to automatically configure the Ren'Py module."
print "Perhaps sdl-config was not found, or could not be run."
print
print "Hopefully, you can figure it out yourself from the traceback below."
print "Otherwise, email pytom@bishoujo.us for help."
print
raise
common()
+22
View File
@@ -0,0 +1,22 @@
# This file will really only work on my mac. But hopefully it will give you
# an idea of how to do things on the mac platform.
import setup
import bdist_mpkg
setup.extra_compile_args = [ "-framework", "SDL",
"-I/Library/Frameworks/SDL.framework/Headers",
"-I/Users/tom/install/include/",
"-DMACOSX" ]
setup.extra_link_args = [ "-framework", "QuickTime",
"-framework", "SDL",
"-framework", "Cocoa",
"-L/Users/tom/install/lib",
"-framework", "smpeg" ]
setup.includes = [ ]
setup.libraries = [ ]
setup.nativemidi = [ 'nativemidi.c', 'native_midi_mac.c', 'native_midi_common.c', 'rwobject.c' ]
setup.nativemidi_libs = [ ]
setup.common()
+16
View File
@@ -0,0 +1,16 @@
# This file will really only work on my Win32 system, as it uses
# sdl-config directly.
import setup
setup.extra_compile_args = [ "-O3", "-Ic:\\msys\\1.0\\local\\include", "-Ic:\\msys\\1.0\\local\\include\\SDL" ]
setup.extra_link_args = [ "-Lc:\\msys\\1.0\\local\\lib", "-lSDL" ]
setup.includes = [ ]
setup.libraries = [ 'smpeg', 'vorbisfile', 'vorbis', 'ogg', 'mikmod', 'stdc++' ]
setup.nativemidi = [ 'nativemidi.c', 'native_midi_win32.c', 'native_midi_common.c', 'rwobject.c' ]
setup.nativemidi_libs = [ 'winmm', 'SDL' ]
setup.winmixer = True
setup.common()
+34
View File
@@ -0,0 +1,34 @@
import pygame
import _renpy
import time
pygame.init()
screen = pygame.display.set_mode((800, 600), 0, 32)
s = pygame.image.load("whitehouse.jpg")
# s = s.convert_alpha()
s2 = pygame.Surface(s.get_size(), 0, s)
px = 2
for r in range(10):
for px in range(1, 10) + range(10, 0, -1):
start = time.time()
px = 2 ** px
for i in range(5):
_renpy.pixellate(s, s2, px, px, px, px)
print px, ( time.time() - start ) / 4.0
screen.blit(s2, (0, 0))
pygame.display.flip()
while True:
if pygame.event.wait().type == pygame.constants.QUIT:
break
+109
View File
@@ -0,0 +1,109 @@
# Copyright 2005 PyTom <pytom@bishoujo.us>
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
cdef extern from "windows.h":
pass
cdef extern from "mmsystem.h":
unsigned int waveOutGetNumDevs()
unsigned int waveOutGetVolume(int dev, unsigned int *volume)
unsigned int waveOutSetVolume(int dev, unsigned int volume)
unsigned int midiOutGetNumDevs()
unsigned int midiOutGetVolume(int dev, unsigned int *volume)
unsigned int midiOutSetVolume(int dev, unsigned int volume)
# Setting volume done through hMidiStream.
cdef unsigned int vol_to_uint(vol):
cdef unsigned int v
v = int(0xffff * vol)
return v + (v << 16)
cdef uint_to_vol(unsigned int u):
cdef unsigned int v1
cdef unsigned int v2
v1 = u & 0xffff
v2 = u >> 16
return (v1 + v2) / 2.0 / 0xffff
midi_dev = None
wave_dev = None
def init():
global midi_dev
global wave_dev
cdef unsigned int volume
for i in range(0, waveOutGetNumDevs()):
if not waveOutGetVolume(i, &volume):
wave_dev = i
break
for i in range(0, midiOutGetNumDevs()):
if not midiOutGetVolume(i, &volume):
midi_dev = i
break
return
init()
def get_wave():
global wave_dev
cdef unsigned int volume
if wave_dev is None:
return None
waveOutGetVolume(wave_dev, &volume)
return uint_to_vol(volume)
def set_wave(vol):
global wave_dev
if wave_dev is None:
return
waveOutSetVolume(wave_dev, vol_to_uint(vol))
def get_midi():
global midi_dev
cdef unsigned int volume
if midi_dev is None:
return None
midiOutGetVolume(midi_dev, &volume)
return uint_to_vol(volume)
def set_midi(vol):
global midi_dev
if midi_dev is None:
return
midiOutSetVolume(midi_dev, vol_to_uint(vol))
+18 -4
View File
@@ -5,12 +5,26 @@ if test "x$1" = "x"; then
exit -1
fi
rm -Rf build dist
rm -Rf build dist "/tmp/$1.zip"
cp run_game.py console.py
cmd /c build_exe.bat
python distribute.py ../$1 demo2
cd ..
zip -9 -r $1.zip $1
# strip dist/*.dll dist/lib/*.pyd dist/lib/*.dll dist/lib/*.exe
python distribute.py "../$1" demo2
export CYGWIN=smbntsec
cd ..
chmod +x "$1/run_game.py"
chmod +x "$1/tools/add_from"
chmod +x "$1/tools/archiver"
chmod +x "$1/tools/dump_text"
chmod +x "$1/tools/run_dse"
zip -X9 -r "/tmp/$1.zip" "$1"
unset CYGWIN
cp "/tmp/$1.zip" "$1.zip"
+51 -32
View File
@@ -2,42 +2,61 @@
# order.
# Some version numbers and things.
version = "Ren'Py 4.7.2"
script_version = 7
savegame_suffix = "-7.save"
version = "Ren'Py 5.3.0"
script_version = 5003000
savegame_suffix = "-5.3.0.save"
def import_all():
# Can be first, because has no dependencies, and may be imported
# directly.
import renpy.game
import renpy.game
# Should probably be early, as we will add it as a base to serialized things.
import renpy.object # ?
# Should probably be early, as we will add it as a base to serialized things.
import renpy.object
import renpy.ast
import renpy.curry
import renpy.execution
import renpy.loader
import renpy.loadsave
import renpy.parser
import renpy.python # object
import renpy.script
import renpy.style
# Adds in the Ren'Py loader.
import renpy.loader
import renpy.display
import renpy.display.render # Most display stuff depends on this.
import renpy.display.core # object
import renpy.display.audio
import renpy.display.text # core
import renpy.display.layout # core
import renpy.display.behavior # layout
import renpy.display.transition # core
import renpy.display.image # core, behavior
import renpy.display.video
import renpy.ast
import renpy.curry
import renpy.execution
import renpy.loadsave
import renpy.parser
import renpy.python # object
import renpy.script
import renpy.style
import renpy.ui
import renpy.display
import renpy.display.presplash
import renpy.display.module
import renpy.display.render # Most display stuff depends on this.
import renpy.display.core # object
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.video
import renpy.display.focus
import renpy.display.anim
import renpy.exports
import renpy.config # depends on lots.
import renpy.store # depends on everything.
import renpy.main
# Note: For windows to work, renpy.audio.audio needs to be after
# renpy.display.module.
import renpy.audio.audio
import renpy.audio.sound
import renpy.audio.music
import renpy.ui
import renpy.lint
import renpy.warp
import renpy.exports
import renpy.config # depends on lots.
import renpy.store # depends on everything.
import renpy.main
# Import everything into renpy.exports, provided it isn't
# already there.
for k, v in globals().iteritems():
vars(renpy.exports).setdefault(k, v)
+161 -86
View File
@@ -1,8 +1,45 @@
# This file contains the AST for the Ren'Py script language. Each class
# here corresponds to a statement in the script language.
# NOTE:
# When updating this file, consider if lint.py or warp.py also need
# updating.
import renpy
import re
class PyCode(object):
__slots__ = [
'source',
'location',
'mode',
'bytecode',
]
# All PyCodes known to the system.
extent = [ ]
def __getstate__(self):
return (1, self.source, self.location, self.mode)
def __setstate__(self, state):
(_, self.source, self.location, self.mode) = state
self.bytecode = None
PyCode.extent.append(self)
def __init__(self, source, loc=('<none>', 1, 0), mode='exec'):
# The source code.
self.source = source
self.location = loc
self.mode = mode
# This will be initialized later on, after we are serialized.
self.bytecode = None
PyCode.extent.append(self)
def chain_block(block, next):
"""
@@ -30,6 +67,13 @@ class Node(object):
# a node from that file.
serials = { }
__slots__ = [
'name',
'filename',
'linenumber',
'next',
]
def __init__(self, loc):
"""
Initializes this Node object.
@@ -83,7 +127,7 @@ class Node(object):
program or init block.
"""
assert False, "AST subclass forgot to define execute."
assert False, "Node subclass forgot to define execute."
def predict(self, callback):
"""
@@ -120,18 +164,38 @@ def say_menu_with(expression):
class Say(Node):
__slots__ = [
'who',
'who_fast',
'what',
'with',
]
def __init__(self, loc, who, what, with):
super(Say, self).__init__(loc)
self.who = who
if who is not None:
self.who = who.strip()
if re.match(r'[a-zA-Z_]\w*$', self.who):
self.who_fast = True
else:
self.who_fast = False
else:
self.who = None
self.who_fast = False
self.what = what
self.with = with
def execute(self):
if self.who is not None:
who = renpy.python.py_eval(self.who)
if self.who_fast:
who = getattr(renpy.store, self.who)
else:
who = renpy.python.py_eval(self.who)
else:
who = None
@@ -142,6 +206,11 @@ class Say(Node):
class Init(Node):
__slots__ = [
'block',
'priority',
]
def __init__(self, loc, block, priority):
super(Init, self).__init__(loc)
@@ -169,6 +238,11 @@ class Init(Node):
class Label(Node):
__slots__ = [
'name',
'block',
]
def __init__(self, loc, name, block):
"""
Constructs a new Label node.
@@ -199,9 +273,14 @@ class Label(Node):
class Python(Node):
__slots__ = [
'hide',
'code',
]
def __init__(self, loc, python_code, hide=False):
"""
@param python_code: Properly-indented python code.
@param code: A PyCode object.
@param hide: If True, the code will be executed with its
own local dictionary.
@@ -213,18 +292,23 @@ class Python(Node):
old_ei = renpy.game.exception_info
renpy.game.exception_info = "While compiling python block starting at line %d of %s." % (self.linenumber, self.filename)
self.bytecode = renpy.python.py_compile_exec_bytecode(python_code)
renpy.game.exception_info = old_ei
# renpy.game.exception_info = "While compiling python block starting at line %d of %s." % (self.linenumber, self.filename)
# renpy.python.py_compile_exec_bytecode(python_code, filename=filename, lineno=lineno)
self.code = PyCode(python_code, loc=loc, mode='exec')
# renpy.game.exception_info = old_ei
def execute(self):
renpy.python.py_exec_bytecode(self.bytecode, self.hide)
renpy.python.py_exec_bytecode(self.code.bytecode, self.hide)
return self.next
class Image(Node):
__slots__ = [
'imgname',
'code',
]
def __init__(self, loc, name, expr):
"""
@param name: The name of the image being defined.
@@ -235,59 +319,16 @@ class Image(Node):
super(Image, self).__init__(loc)
self.name = name
self.expr = expr
self.imgname = name
self.code = PyCode(expr, loc=loc, mode='eval')
def execute(self):
import renpy.exports as exports
if not renpy.game.init_phase:
raise Exception("image statement should only be inside an init: block.")
img = renpy.python.py_eval(self.expr)
exports.images[self.name] = img
img = renpy.python.py_eval_bytecode(self.code.bytecode)
renpy.exports.image(self.imgname, img)
return self.next
def imspec_common(imspec, hide=False):
"""
This is code that's common to the three statements that can
take imspecs (scene, show, and hide).
It parses the imspec into a key, and an image, perhaps applying
at clauses.
@param hide: Reduces error checking, and changes the sticky position
logic.
"""
import renpy.display.image
sls = renpy.game.context().scene_lists
name, at_list = imspec
key = name[0]
# Handle sticky positions.
if renpy.config.sticky_positions:
if hide:
if key in sls.sticky_positions:
del sls.sticky_positions[key]
else:
if not at_list and key in sls.sticky_positions:
at_list = sls.sticky_positions[key]
sls.sticky_positions[key] = at_list
# Get a reference to the base image.
img = renpy.display.image.ImageReference(name)
# Now, apply the at_list, from left to right.
for i in at_list:
img = renpy.python.py_eval(i)(img)
return key, img
def predict_imspec(imspec, callback):
"""
Call this to use the given callback to predict the image named
@@ -304,11 +345,14 @@ def predict_imspec(imspec, callback):
class Show(Node):
__slots__ = [
'imspec',
]
def __init__(self, loc, imspec):
"""
@param imspec: A triple consisting of an image name (itself a
tuple of strings), a list of at expressions, and a list of
with expressions.
tuple of strings), a list of at expressions, and a layer.
"""
super(Show, self).__init__(loc)
@@ -317,10 +361,10 @@ class Show(Node):
def execute(self):
key, img = imspec_common(self.imspec)
sls = renpy.game.context().scene_lists
sls.add('master', img, key)
name, at_list, layer = self.imspec
at_list = [ renpy.python.py_eval(i) for i in at_list ]
renpy.exports.show(name, at_list, layer)
return self.next
@@ -331,31 +375,33 @@ class Show(Node):
class Scene(Node):
def __init__(self, loc, imgspec):
__slots__ = [
'imspec',
'layer',
]
def __init__(self, loc, imgspec, layer):
"""
@param imspec: A triple consisting of an image name (itself a
tuple of strings), a list of at expressions, and a list of
with expressions, or None to not have this scene statement
also display an image.
tuple of strings), a list of at expressions, and a layer, or
None to not have this scene statement also display an image.
"""
super(Scene, self).__init__(loc)
self.imspec = imgspec
self.layer = layer
def execute(self):
import renpy.exports as exports
sls = renpy.game.context().scene_lists
sls.clear('master')
sls.sticky_positions.clear()
renpy.exports.scene(self.layer)
if self.imspec:
key, img = imspec_common(self.imspec)
sls.add('master', img, key)
name, at_list, layer = self.imspec
at_list = [ renpy.python.py_eval(i) for i in at_list ]
renpy.exports.show(name, at_list, layer)
return self.next
@@ -368,6 +414,10 @@ class Scene(Node):
class Hide(Node):
__slots__ = [
'imspec',
]
def __init__(self, loc, imgspec):
"""
@param imspec: A triple consisting of an image name (itself a
@@ -381,17 +431,15 @@ class Hide(Node):
def execute(self):
import renpy.exports as exports
sls = renpy.game.context().scene_lists
key, img = imspec_common(self.imspec, hide=True)
sls.remove('master', key)
renpy.exports.hide(self.imspec[0], self.imspec[2])
return self.next
class With(Node):
__slots__ = [
'expr',
]
def __init__(self, loc, expr):
"""
@param expr: An expression giving a transition or None.
@@ -411,6 +459,11 @@ class With(Node):
class Call(Node):
__slots__ = [
'label',
'expression',
]
def __init__(self, loc, label, expression):
super(Call, self).__init__(loc)
@@ -433,6 +486,8 @@ class Call(Node):
class Return(Node):
__slots__ = [ ]
# No __init__ needed.
# We don't care what the next node is.
@@ -451,6 +506,12 @@ class Return(Node):
class Menu(Node):
__slots__ = [
'items',
'set',
'with',
]
def __init__(self, loc, items, set, with):
super(Menu, self).__init__(loc)
@@ -509,6 +570,11 @@ class Menu(Node):
# instead.
class Jump(Node):
__slots__ = [
'target',
'expression',
]
def __init__(self, loc, target, expression):
super(Jump, self).__init__(loc)
@@ -537,11 +603,18 @@ class Jump(Node):
# GNDN
class Pass(Node):
__slots__ = [ ]
def execute(self):
return self.next
class While(Node):
__slots__ = [
'condition',
'block',
]
def __init__(self, loc, condition, block):
super(While, self).__init__(loc)
@@ -568,6 +641,8 @@ class While(Node):
class If(Node):
__slots__ = [ 'entries' ]
def __init__(self, loc, entries):
"""
@param entries: A list of (condition, block) tuples.
View File
+647
View File
@@ -0,0 +1,647 @@
# The latest and greatest Ren'Py audio system.
# Invariants: The periodic callback assumes pcm_ok. If we don't have
# at least pcm_ok, we have no sound whatsoever.
import renpy
import os
import atexit
# Import the appropriate modules, or set them to None if we cannot.
disable = os.environ.get("RENPY_DISABLE_SOUND", "")
pss = None
nativemidi = None
mix = None
if 'pss' not in disable:
try:
import pysdlsound as pss
atexit.register(pss.quit)
except:
pass
if 'nativemidi' not in disable:
try:
import nativemidi as nativemidi
except:
pass
if 'mix' not in disable:
try:
import winmixer as mix
except:
try:
import linmixer as mix
except:
pass
# Save the mixer, and restore it at exit.
if mix:
old_wave = mix.get_wave()
old_midi = mix.get_midi()
def restore_volumes():
if old_wave is not None:
mix.set_wave(old_wave)
if old_midi is not None:
mix.set_midi(old_midi)
atexit.register(restore_volumes)
# This is True if we were able to sucessfully enable the pcm audio.
pcm_ok = None
# This is True if we were able to succesfully enable native midi.
midi_ok = None
# True if we are managing the mixers ourselves.
mix_ok = None
def ismidi(s):
"""
Returns true if s is the filename of a midi file.
"""
return midi_ok and s is not None and s.endswith(".mid")
def load(fn):
"""
Returns a file-like object for the given filename.
"""
rv = renpy.loader.load(fn)
return rv
class Midi(object):
"""
This is the object that manages the native midi playback. It ensures
that only one channel can play midi at a time, and it keeps track
of what's playing at any given time.
"""
def __init__(self):
# The filename of the music that is currently playing through
# native midi.
self.playing = None
# The natural volume of the music that is playing through
# native midi.
self.volume = 1.0
def play(self, f, filename):
# Native midi must work for us to do anything.
if not midi_ok:
return
# If the midi channel is still busy, then ignore this request
# to play something new.
if self.playing:
return
# Otherwise, immediately play this midi file,
# replacing whatever is already playing.
try:
nativemidi.play(f)
self.playing = filename
except:
self.playing = None
if renpy.config.debug_sound:
raise
def busy(self):
"""
Is midi busy?
"""
return self.playing is not None
def periodic(self):
"""
The 20hz callback that checks on the status of midi. This should
be called before the updates for the various channel objects, below.
"""
if not midi_ok:
return
if self.playing and not nativemidi.busy():
self.playing = None
# Eventually... add nativemidi fade code here.
def stop(self):
"""
Called to stop the native midi subsystem from playing music.
"""
if not midi_ok:
return
nativemidi.stop()
self.playing = None
def set_volume(self, volume):
if midi_ok:
nativemidi.set_volume(volume)
self.volume = volume
def get_pos(self):
return nativemidi.get_pos()
# A singleton Midi object that manages hardware midi playback.
midi = Midi()
class Channel(object):
def __init__(self, number):
# The number of this channel.
self.number = number
# The name of the mixer this channel uses. Set below, as there's
# no good default.
self.mixer = None
# The volume imparted to this channel, as a fraction of the
# mixer volume.
self.chan_volume = 1.0
# The actual volume we imparted onto this channel.
self.actual_volume = 1.0
# The filenames that are being queued for playback on this
# channel.
self.queue = [ ]
# If true, we loop the music. This entails re-adding the element to
# the queue if it would make the queue empty.
self.loop = False
# Are we playing anything at all?
self.playing = False
# If True, then this channel is playing midi. If False, it's
# playing PCM instead.
self.playing_midi = False
# If True, we'll wait for this channel to stop before
# loading in more music from the queue. (This is necessary to
# do a synchro-start.)
self.wait_stop = False
# If True, then this channel will participate in a synchro-start
# once all channels are ready.
self.synchro_start = False
# If we're a music channel, the time the music in this channel was
# last changed.
self.music_last_changed = 0
# The callback that is called if the queue becomes empty.
self.callback = None
def periodic(self):
"""
This is the periodic call that causes this channel to load new stuff
into its queues, if necessary.
"""
# This should be set from something that checks to see if our
# mixer is muted.
force_stop = renpy.game.preferences.mute[self.mixer]
if self.playing and force_stop:
if self.playing_midi:
midi.stop()
self.playing = False
self.wait_stop = False
self.playing_midi = False
else:
pss.stop(self.number)
self.playing = False
self.wait_stop = False
if force_stop:
if self.loop:
self.queue = self.queue[-1:]
else:
self.queue = [ ]
return
# If we're playing midi, and the midi device is busy, return.
# Otherwise, it's stopped, so we can stop waiting.
if self.playing_midi:
if midi.busy():
return
else:
self.playing = False
self.wait_stop = False
self.playing_midi = False
# Should we do the callback?
do_callback = False
while True:
depth = pss.queue_depth(self.number)
if depth == 0 and not self.playing_midi:
self.wait_stop = False
self.playing = False
# Need to check this, so we don't do pointless work.
if not self.queue:
break
# If we're still playing midi, then we can't queue
# anything up, so bail out now.
if self.playing_midi:
break
# If the pcm_queue is full, then we can't queue
# anything, regardless of if it is midi or pcm.
if depth >= 2:
break
# We can't queue anything if the depth is > 0 and we're
# waiting for a synchro_start.
if self.synchro_start and depth:
break
# If the queue is full, return.
if pss.queue_depth(self.number) >= 2:
break
# Otherwise, we might be able to enqueue something.
top = self.queue[0]
# We can only play midi if we're not playing PCM.
if ismidi(top):
if depth != 0:
break
# At this point, we've decided to try to play
# top. So let's see how far we can get.
# Update the queue
self.queue = self.queue[1:]
try:
topf = load(top)
if ismidi(top):
# If someone else is playing midi, then raise
# an error to that effect.
if midi.busy():
raise Exception, "We can only play one midi at a time."
# Play midi.
midi.play(topf, top)
self.playing = True
self.playing_midi = True
else:
if depth == 0:
pss.play(self.number, topf, top, self.synchro_start)
else:
pss.queue(self.number, topf, top)
self.playing = True
except:
if renpy.config.debug_sound:
raise
else:
return
if self.loop and not self.queue:
self.queue.append(top)
else:
do_callback = True
# TODO: Queue empty callback.
if do_callback and self.callback:
self.callback()
def dequeue(self):
"""
Clears any queued music. Doesn't stop the playing music, if
any, but will prevent looping from occuring.
"""
self.queue = [ ]
self.loop = False
if not pcm_ok:
return
if not self.playing_midi:
pss.dequeue(self.number)
def interact(self):
"""
Called (mostly) once per interaction. Calls the queue callback
if it's becoming empty.
"""
if not self.queue and self.callback:
self.callback()
def fadeout(self, secs):
"""
Causes the playing music to be faded out for the given number
of seconds. Also clears any queued music.
"""
self.dequeue()
if not pcm_ok:
return
if self.playing_midi:
midi.stop()
else:
pss.fadeout(self.number, secs * 1000)
def enqueue(self, filename, loop=True, synchro_start=False):
assert filename
if not pcm_ok:
return
self.queue.append(filename)
self.loop = loop
self.wait_stop = synchro_start
self.synchro_start = synchro_start
def get_playing(self):
if self.playing_midi:
return midi.playing
else:
return pss.playing_name(self.number)
def set_volume(self, volume):
self.chan_volume = volume
def get_pos(self):
if self.playing_midi:
return midi.get_pos()
else:
return pss.get_pos(self.number)
# The number of channels we support.
NUM_CHANNELS = 8
# A list of channels.
channels = [ Channel(i) for i in range(NUM_CHANNELS) ]
channels[0].mixer = 'sfx'
channels[1].mixer = 'sfx'
channels[2].mixer = 'sfx'
channels[3].mixer = 'music'
channels[4].mixer = 'music'
channels[5].mixer = 'music'
channels[6].mixer = 'music'
channels[7].mixer = 'music'
def get_channel(number):
if not 0 <= number < NUM_CHANNELS:
raise Exception("Channel number %d out of bounds." % channel)
return channels[number]
def init():
global pcm_ok
global midi_ok
global mix_ok
if pcm_ok is None and pss:
bufsize = 4096
if 'RENPY_SOUND_BUFSIZE' in os.environ:
bufsize = int(os.environ['RENPY_SOUND_BUFSIZE'])
try:
pss.init(renpy.config.sound_sample_rate, 2, bufsize)
pcm_ok = True
except:
if renpy.config.debug_sound:
raise
pcm_ok = False
if midi_ok is None and nativemidi:
try:
nativemidi.init()
midi_ok = True
except:
if renpy.config.debug_sound:
raise
midi_ok = False
# Find all of the mixers in the game.
mixers = [ ]
for c in channels:
if c.mixer not in mixers:
mixers.append(c.mixer)
default_volume = 1.0
if mix and not 'RENPY_NOMIXER' in os.environ and mix.get_wave() is not None:
default_volume = mix.get_wave()
mix_ok = True
for m in mixers:
renpy.game.preferences.volumes.setdefault(m, default_volume)
renpy.game.preferences.mute.setdefault(m, False)
def quit():
"""
Deinitialize the mixer and the various libraries.
"""
global pcm_ok
global midi_ok
global mix_ok
if not pcm_ok:
return
for c in channels:
c.dequeue()
c.fadeout(0)
c.queue = [ ]
c.loop = False
c.playing = False
c.playing_midi = False
c.wait_stop = False
c.synchro_start = False
pss.quit()
pcm_ok = None
midi_ok = None
mix_ok = None
# The last-set pcm volume.
pcm_volume = None
# The last-set midi volume.
midi_volume = None
def periodic():
"""
The periodic sound callback. This is called at around 20hz, and is
responsible for adjusting the volume of the playing music if
necessary, and also for calling the periodic functions of midi and
the various channels, which then may play music.
"""
global pcm_volume
global midi_volume
if not pcm_ok:
return False
try:
for c in channels:
c.periodic()
midi.periodic()
# Perform a synchro-start if necessary.
need_ss = False
for c in channels:
if c.synchro_start and c.wait_stop:
need_ss = False
break
if c.synchro_start and not c.wait_stop:
need_ss = True
if need_ss:
pss.unpause_all()
for c in channels:
c.synchro_start = False
# Now, consider adjusting the volume of the channel.
max_volume = -1.0
volumes = renpy.game.preferences.volumes
if mix_ok:
for c in channels:
# if not c.playing:
# continue
vol = c.chan_volume * volumes[c.mixer]
max_volume = max(max_volume, vol)
if max_volume == -1.0:
return
if max_volume == 0.0:
return
if max_volume != pcm_volume:
mix.set_wave(max_volume)
pcm_volume = max_volume
for c in channels:
# if not c.playing:
# continue
vol = c.chan_volume * volumes[c.mixer]
if c.playing_midi:
if midi_volume != vol:
# mix.set_midi(vol)
midi.set_volume(vol)
midi_volume = vol
else:
vol /= max_volume
if c.actual_volume != vol:
pss.set_volume(c.number, vol)
c.actual_volume = vol
else:
for c in channels:
if not c.playing:
continue
vol = c.chan_volume * volumes[c.mixer]
if c.playing_midi:
if vol != midi_volume:
midi.set_volume(vol)
midi_volume = vol
else:
if vol != c.actual_volume:
pss.set_volume(c.number, vol)
c.actual_volume = vol
except:
if renpy.config.debug_sound:
raise
def interact():
"""
Called at least once per interaction.
"""
if not pcm_ok:
return
try:
for c in channels:
c.interact()
except:
if renpy.config.debug_sound:
raise
renpy.audio.music.interact()
periodic()
+274
View File
@@ -0,0 +1,274 @@
# The public API for music in games.
import time
import renpy
# The music channels.
music_channels = [ 3, 4, 5, 6, 7 ]
def get_info():
"""
Returns the info object. If the music fields are not on it, then
add them.
"""
info = renpy.game.context().info
if getattr(info, "_music_last_file", None) is None:
info._music_last_file = renpy.python.RevertableDict()
if getattr(info, "_music_last_changed", None) is None:
info._music_last_changed = renpy.python.RevertableDict()
if getattr(info, "_music_volumes", None) is None:
info._music_volumes = renpy.python.RevertableDict()
return info
def get_channel(channel):
if not channel in music_channels:
raise Exception("Channel %d is not a music channel." % channel)
c = renpy.audio.audio.get_channel(channel)
return c
def play(filename, channel=7, loop=True, fadeout=None, synchro_start=False):
"""
This stops the music currently playing on the numbered channel, dequeues
any queued music, and begins playing the specified filename. If loop
is True, the track will loop while it is still the last thing in
the queue. If fadeout is None, the fadeout time is taken from
config.fade_music, otherwise it is a time in seconds to fade for.
If synchro_start is given, all the channels that have had play
called on them with synchro_start set to True will be started at
the same time, in a sample accurate manner.
The filename given becomes the last queued file if loop is
True. If loop is False, then the last queued file is set to None.
"""
try:
c = get_channel(channel)
info = get_info()
c.dequeue()
if fadeout is None:
fadeout = renpy.config.fade_music
c.fadeout(fadeout)
c.enqueue(filename, loop=loop, synchro_start=synchro_start)
t = time.time()
info._music_last_changed[channel] = t
c.music_last_changed = t
if loop:
info._music_last_file[channel] = filename
else:
info._music_last_file[channel] = None
except:
if renpy.config.debug_sound:
raise
def queue(filename, channel=7, loop=True, clear_queue=True):
"""
This queues the given filename on the specified channel. If
clear_queue is True, then the queue is cleared, making this file
the file that is played when the currently playing music
finishes. If it is False, then this file is placed at the back of
the queue. In either case, if no music is playing this music
begins playing immediately.
If loop is True, then this music will repeat as long as it is the
last element of the queue.
The filename given becomes the last queued file if loop is
True. If loop is False, then the last queued file is set to None.
"""
try:
c = get_channel(channel)
info = get_info()
if clear_queue:
c.dequeue()
c.enqueue(filename, loop=loop)
t = time.time()
info._music_last_changed[channel] = t
c.music_last_changed = t
if loop:
info._music_last_file[channel] = filename
else:
info._music_last_file[channel] = None
except:
if renpy.config.debug_sound:
raise
def stop(channel=7, fadeout=None):
"""
This stops the music that is currently playing, and dequeues all
queued music. If fadeout is None, the music is faded out for the
time given in config.fade_music, otherwise it is faded for fadeout
seconds.
This sets the last queued file to None.
"""
try:
c = get_channel(channel)
info = get_info()
c.dequeue()
if fadeout is None:
fadeout = renpy.config.fade_music
c.fadeout(fadeout)
t = time.time()
info._music_last_changed[channel] = t
c.music_last_changed = t
info._music_last_file[channel] = None
except:
if renpy.config.debug_sound:
raise
def set_volume(volume, channel=7):
"""
This sets the volume of the given channel. The volume is a number
between 0 and 1.0, and is interpreted as a fraction of the mixer
volume for the channel.
This value is persisted, and takes effect immediately. It also
participates in rollback.
"""
try:
c = get_channel(channel)
info = get_info()
c.set_volume(channel, volume)
info._music_volumes[channel] = volume
except:
if renpy.config.debug_sound:
raise
def set_music(channel, flag):
"""
This should be called to indicate if the given channel should be
treated as a music channel. If the flag is True, the channel will
be treated as a music channel, if False, the channel will be
treated as a sound effects channel. Please note that this will not
change the mixer controlling the channel. Use
renpy.sound.set_mixer to do that.
By default, channels 3-7 are considered music channels.
"""
if not 0 <= channel < renpy.audio.audio.NUM_CHANNELS:
raise Exception("Not a music channel.")
if flag:
if channel in music_channels:
music_channels.remove(channel)
else:
if channel not in music_channels:
music_channels.append(channel)
def get_delay(time, channel=7):
"""
Returns the number of seconds left until the given time in the
music.
"""
try:
c = renpy.audio.audio.get_channel(channel)
t = c.get_pos()
if not t or t < 0:
return None
if t > time:
return 0
return time - t
except:
if renpy.config.debug_sound:
raise
return None
def interact():
"""
This is the music change logic that is called at least once per
interaction.
"""
try:
info = get_info()
for i in music_channels:
c = renpy.audio.audio.get_channel(i)
# If we're in the same music change, then do nothing with the
# music.
if c.music_last_changed == info._music_last_changed.get(i, 0):
continue
file = info._music_last_file.get(i, None)
c.dequeue()
if file != c.get_playing():
c.fadeout(renpy.config.fade_music)
if file:
c.enqueue(file, loop=True, synchro_start=True)
c.music_last_changed = info._music_last_changed.get(i, 0)
except:
if renpy.config.debug_sound:
raise
# Music change logic:
# Use the queueing time to determine what should or should not be
# queued
# m_filename - music filename from info object
# m_loop - music loop from info object
# c_filename - music filename from channel
# c_filename - music loop from channel
# if m_filename == c_filename and m_loop == c_loop:
# do nothing, the music is right.
# otherwise,
# dequeue music from the channel.
# if m_filename != c_playing_filename:
# stop the music with fade. The music is wrong, change it.
# if m_loop:
# queue m_filename looping
+134
View File
@@ -0,0 +1,134 @@
# The public API for sound playback from games.
# TODO: Check to see if SFX are enabled before playing sounds with play or
# queue.
import renpy
def play(filename, channel=0, fadeout=0):
"""
Plays the named file once on the given channel. This will cause any
playing sound effect to be stopped (after the given fadeout number of
seconds, if necessary), and the new sound to be played in its
place.
The filename may be that of a file in an archive.
"""
if filename is None:
return
stop(channel=channel, fadeout=fadeout)
queue(filename, channel=channel, clear_queue=True)
def queue(filename, channel=0, clear_queue=True):
"""
This causes the name file to be queued to be played on the given
channel. If clear_queue is True, the queue will be cleared before
playback, so this sound is played immediately after the currently
playing sound. If False, the channel's queue will not be cleared,
and the sound will only be played after every other playing sound.
If no sound is currently playing, then the sound will be played
immediately.
"""
try:
c = renpy.audio.audio.get_channel(channel)
if clear_queue:
c.dequeue()
c.enqueue(filename, loop=False)
except:
if renpy.config.debug_sound:
raise
def stop(channel=0, fadeout=0):
"""
This dequeues everything from the given channel, and stops the
currently playing sound. If fadeout is 0, the sound is stopped
immediately. Otherwise, it is interpreted as a number of seconds
to fadeout for.
"""
try:
c = renpy.audio.audio.get_channel(channel)
c.dequeue()
c.fadeout(int(fadeout * 1000))
except:
if renpy.config.debug_sound:
raise
def set_mixer(channel, mixer):
"""
This sets the name of the mixer associated with a given
channel. By default, there are two mixers, 'sfx' and
'music'. 'sfx' is on channels 0 to 3, and 'music'
on 3 to 7. The voice module calls this function to set channel 2 to voice.
You can create your own mixer, but will need to add a preference if you
wish to allow the user to set it.
This function should only be called in an init block.
"""
try:
c = renpy.audio.audio.get_channel(channel)
c.mixer = mixer
except:
if renpy.config.debug_sound:
raise
def set_queue_empty_callback(callback, channel=0):
"""
This sets a callback that is called when the queue is empty. This
callback is called when the queue first becomes empty, and at
least once per interaction while the queue is empty.
The callback is called with no parameters. It can queue sounds by
calling renpy.sound.queue with the appropriate arguments. Please
note that the callback may be called while a sound is playing, as
long as a queue slot is empty.
"""
try:
c = renpy.audio.audio.get_channel(channel)
c.callback = callback
except:
if renpy.config.debug_sound:
raise
def set_volume(volume, channel=0):
"""
Sets the volume of this channel, as a fraction of the volume of the
mixer controlling the channel.
This volume is not persisted or rolled-back, as are volumes set with
renpy.music.set_volume for music channels.
"""
try:
c = renpy.audio.audio.get_channel(channel)
c.set_volume(volume)
except:
if renpy.config.debug_sound:
raise
def is_playing(channel=0):
"""
Returns True if the channel is currently playing a sound, False if
it is not, or if the sound system isn't working.
This works with both sound and music channels, although it's intended
for the former.
"""
try:
c = renpy.audio.audio.get_channel(channel)
return c.playing
except:
if renpy.config.debug_sound:
raise
else:
return False
+166
View File
@@ -0,0 +1,166 @@
import optparse
import os
import os.path
import sys
# Extra things used for distribution.
def extra_imports():
import encodings.utf_8
import encodings.zlib_codec
import encodings.unicode_escape
import encodings.string_escape
import encodings.raw_unicode_escape
import math
import datetime
import glob
def bootstrap(renpy_base):
name = os.path.basename(sys.argv[0])
if name.find(".") != -1:
name = name[:name.find(".")]
if name.find("_") != -1:
name = name[name.find("_") + 1:]
op = optparse.OptionParser()
op.add_option('--game', dest='game', default=name,
help='The directory the game is in.')
op.add_option('--lock', dest='lock', default=None, action='store',
help='If True, produce locked version of the .rpyc files.')
op.add_option('--python', dest='python', default=None,
help='Run the argument in the python interpreter.')
op.add_option('--lint', dest='lint', default=False, action='store_true',
help='Run a number of expensive tests, to try to detect errors in the script.')
op.add_option('--profile', dest='profile', action='store_true', default=False,
help='Causes the amount of time it takes to draw the screen to be profiled.')
op.add_option('--leak', dest='leak', action='store_true', default=False,
help='When the game exits, dumps a profile of memory usage.')
op.add_option('--warp', dest='warp', default=None,
help='This takes as an argument a filename:linenumber pair, and tries to warp to the statement before that line number.')
options, args = op.parse_args()
if options.python:
sys.argv = [ options.python ] + args
execfile(renpy_base + "/" + options.python, globals(), globals())
sys.exit(0)
# If we made it this far, we will be running the game, or at least
# doing a lint.
os.chdir(renpy_base)
# Force windib on windows, unless the user explicitly overrides.
if hasattr(sys, 'winver') and not 'SDL_VIDEODRIVER' in os.environ:
os.environ['SDL_VIDEODRIVER'] = 'windib'
# Show the presplash.
if not options.lint:
import renpy.display.presplash
renpy.display.presplash.start(options.game)
# Load up all of Ren'Py, in the right order.
import renpy
renpy.import_all()
renpy.game.options = options
try:
renpy.main.main(options.game)
except Exception, e:
import codecs
import traceback
f = file("traceback.txt", "wU")
f.write(codecs.BOM_UTF8)
print >>f, "I'm sorry, but an exception occured while executing your Ren'Py"
print >>f, "script."
print >>f
type, value, tb = sys.exc_info()
print >>f, type.__name__ + ":",
print >>f, unicode(e).encode('utf-8')
print >>f
print >>f, renpy.game.exception_info
print >>f
print >>f, "-- Full Traceback ------------------------------------------------------------"
print >>f
traceback.print_tb(tb, None, sys.stdout)
traceback.print_tb(tb, None, f)
print >>f, type.__name__ + ":",
print type.__name__ + ":",
print >>f, unicode(e).encode('utf-8')
print unicode(e).encode('utf-8')
print
print >>f
print renpy.game.exception_info
print >>f, renpy.game.exception_info
print >>f
print >>f, "Ren'Py Version:", renpy.version
f.close()
try:
os.startfile('traceback.txt')
except:
pass
if options.leak:
memory_profile()
sys.exit(0)
def memory_profile():
import renpy
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. ***"
+101 -34
View File
@@ -2,6 +2,9 @@
# This includes both simple settings (like the screen dimensions) and
# methods that perform standard tasks, like the say and menu methods.
# This will be deleted by the end of this file.
import renpy
# The title of the game window.
window_title = "A Ren'Py Game"
@@ -31,7 +34,7 @@ rollback_length = 128
# The maximum number of steps the user can rollback the game,
# interactively.
hard_rollback_limit = 10
hard_rollback_limit = 100
# A list of functions returning lists of displayables that will be
# added to the end of the display list.
@@ -47,9 +50,9 @@ profile = False
# The directory save files will be saved to.
savedir = None
# The number of images that are allowed to live in the image cache
# at once.
image_cache_size = 10
# The number of screens worth of images that are allowed to live in the image
# cache at once.
image_cache_size = 8
# The number of statements we will analyze when doing predictive
# loading. Please note that this is a total number of statements in a
@@ -64,8 +67,12 @@ debug_image_cache = False
# Should we allow skipping at all?
allow_skipping = True
# Are we currently skipping?
skipping = False
# Should we allow fast skipping?
fast_skipping = False
# Are we currently skipping? If so, how fast?
# May be "slow", "fast", or None.
skipping = None
# The delay while we are skipping say statements.
skip_delay = 75
@@ -83,15 +90,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
# How fast text is displayed on the screen, by default.
annoying_text_cps = None
# The amount of time music is faded out between tracks.
fade_music = 0.0
@@ -109,9 +110,23 @@ transient_layers = [ 'transient' ]
# overlays.
overlay_layers = [ 'overlay' ]
# A list of layers that are displayed above all other layers.
top_layers = [ ]
# True if we want to show overlays during wait statements, or
# false otherwise.
overlay_during_wait = True
overlay_during_with = True
# True if we want to allow the fast dissolve.
enable_fast_dissolve = True
# When using the keyboard to navigate, how much we penalize
# distance out of the preferred direction.
focus_crossrange_penalty = 1024
# If True, then we force all loading to occur before transitions
# start.
load_before_transition = True
# The keymap that is used to change keypresses and mouse events.
keymap = dict(
@@ -123,24 +138,19 @@ keymap = dict(
toggle_fullscreen = [ 'f' ],
toggle_music = [ 'm' ],
game_menu = [ 'K_ESCAPE', 'mouseup_3' ],
hide_windows = [ 'mouseup_2' ],
hide_windows = [ 'mouseup_2', 'h' ],
launch_editor = [ 'E' ],
# Say.
rollforward = [ 'mousedown_5', 'K_PAGEDOWN' ],
dismiss = [ 'mouseup_1', 'K_RETURN', 'K_SPACE', 'K_KP_ENTER' ],
# Keymouse.
keymouse_left = [ 'K_LEFT' ],
keymouse_right = [ 'K_RIGHT' ],
keymouse_up = [ 'K_UP' ],
keymouse_down = [ 'K_DOWN' ],
# Menu.
menu_mouseselect = [ 'mouseup_1' ],
menu_keyselect = ['K_RETURN', 'K_KP_ENTER' ],
menu_keyup = [ 'K_UP' ],
menu_keydown = [ 'K_DOWN' ],
# Focus.
focus_left = [ 'K_LEFT' ],
focus_right = [ 'K_RIGHT' ],
focus_up = [ 'K_UP' ],
focus_down = [ 'K_DOWN' ],
# Button.
button_select = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER' ],
@@ -148,24 +158,75 @@ 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' ],
fast_skip = [ '>' ],
# These control the bar.
bar_activate = [ 'mousedown_1', 'K_RETURN', 'K_KP_ENTER' ],
bar_deactivate = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER' ],
bar_decrease = [ 'K_LEFT' ],
bar_increase = [ 'K_RIGHT' ],
)
# A list of functions that are called when an interaction is started or
# restarted.
interact_callbacks = [ ]
# A list of functions that are called when an interaction is started.
start_interact_callbacks = [ ]
# A function that is called to tokenize text.
text_tokenizer = renpy.display.text.text_tokenizer
# The number of characters per AFM time period.
afm_characters = 250
# The number of bonus characters to add to a string for afm.
afm_bonus = 25
# A function that must return True for afm mode to forward.
afm_callback = None
# The amount of time we delay before making an automatic choice from a
# menu. This can be used for making a demo version of a game. It
# should be set to None in a deployed game.
auto_choice_delay = None
# A map from font, bold, italic to font, bold, italic. This is used
# to replace (say) the italic version of a regular font with the regular
# version of an italic font.
font_replacement_map = { }
# A callback that is called when a with statement (but not the with
# clause of a say or menu statement) executes. If not None, it's
# called with a single argument, the transition supplied to the with
# clause.
with_callback = None
# The framerate limit.
framerate = None
# The number of frames that Ren'Py has shown.
frames = 0
# A text editor that is launched at the location of the current
# statement.
editor = None
# Enable developer mode?
developer = False
# A logfile that logging messages are sent to.
log = None
del renpy
def backup():
import copy
import types
global _globals
_globals = globals().copy()
@@ -174,7 +235,13 @@ def backup():
del _globals["reload"]
del _globals["__builtins__"]
_globals = copy.deepcopy(_globals)
for k, v in _globals.items():
if k == "text_tokenizer":
continue
_globals[k] = copy.deepcopy(v)
def reload():
globals().update(_globals)
+399
View File
@@ -0,0 +1,399 @@
# This file contains support for state-machine controlled animations.
import renpy
import pygame
import random
class State(object):
"""
This creates a state that can be used in a SMAnimation.
"""
def __init__(self, name, image, *atlist):
"""
@param name: A string giving the name of this state.
@param image: The displayable that is shown to the user while
we are in (entering) this state. For convenience, this can
also be a string or tuple, which is interpreted with Image.
image should be None when this State is used with motion,
to indicate that the image will be replaced with the child of
the motion.
@param atlist: A list of functions to call on the image. (In
general, if something can be used in an at clause, it can be
used here as well.)
"""
if image and not isinstance(image, renpy.display.core.Displayable):
image = renpy.display.image.Image(image)
self.name = name
self.image = image
self.atlist = atlist
def add(self, sma):
sma.states[self.name] = self
def get_image(self):
rv = self.image
for i in self.atlist:
rv = i(rv)
return rv
def motion_copy(self, child):
if self.image is not None:
child = self.mage
return State(self.name, child, *self.atlist)
class Edge(object):
"""
This creates an edge that can be used with a SMAnimation.
"""
def __init__(self, old, delay, new, trans=None, prob=1):
"""
@param old: The name (a string) of the state that this transition is from.
@param delay: The number of seconds that this transition takes.
@param new: The name (a string) of the state that this transition is to.
@param trans: The transition that will be used to show the
image found in the new state. If None, the image is show
immediately.
When used with an SMMotion, the transition should probably be
move.
@param prob: The number of times this edge is added. This can
be used to make a transition more probable then others. For
example, if one transition out of a state has prob=5, and the
other has prob=1, then the one with prob=5 will execute 5/6 of
the time, while the one with prob=1 will only occur 1/6 of the
time. (Don't make this too large, as memory use is proportional to
this value.)
"""
self.old = old
self.delay = delay
self.new = new
self.trans = trans
self.prob = prob
def add(self, sma):
for i in range(0, self.prob):
sma.edges.setdefault(self.old, []).append(self)
class SMAnimation(renpy.display.core.Displayable):
"""
This creates a state-machine animation. Such an animation is
created by randomly traversing the edges between states in a
defined state machine. Each state corresponds to an image shown to
the user, with the edges corresponding to the amount of time an
image is shown, and the transition it is shown with.
Images are shown, perhaps with a transition, when we are
transitioning into a state containing that image.
"""
def __init__(self, initial, *args, **properties):
"""
@param initial: The name (a string) of the initial state we
start in.
This accepts as additional arguments the anim.State and
anim.Edge objects that are used to make up this state
machine.
"""
if 'delay' in properties:
self.delay = properties['delay']
del properties['delay']
else:
self.delay = None
super(SMAnimation, self).__init__(**properties)
self.properties = properties
# The initial state.
self.initial = initial
# A map from state name to State object.
self.states = { }
# A map from state name to list of Edge objects.
self.edges = { }
for i in args:
i.add(self)
# The time at which the current edge started. If None, will be
# set to st by render.
self.edge_start = None
# A cache for what the current edge looks like when rendered.
self.edge_cache = None
# The current edge.
self.edge = None
# The state we're in.
self.state = None
def predict(self, callback):
for i in self.states.itervalues():
i.image.predict(callback)
def pick_edge(self, state):
"""
This randomly picks an edge out of the given state, if
one exists. It updates self.edge if a transition has
been selected, or returns None if none can be found. It also
updates self.image to be the new image on the selected edge.
"""
if state not in self.edges:
self.edge = None
return
edges = self.edges[state]
self.edge = random.choice(edges)
self.state = self.edge.new
def update_cache(self):
"""
Places the correct Displayable into the edge cache, based on
what is contained in the given edge. This takes into account
the old and new states, and any transition that is present.
"""
im = self.states[self.edge.new].get_image()
if self.edge.trans:
im = self.edge.trans(old_widget=self.states[self.edge.old].get_image(),
new_widget=im)
self.edge_cache = im
def get_placement(self):
if self.edge_cache:
return self.edge_cache.get_placement()
if self.state:
return self.states[self.state].get_image().get_placement()
return self.style
def render(self, width, height, st):
if self.edge_start is None or st < self.edge_start:
self.edge_start = st
self.edge_cache = None
self.pick_edge(self.initial)
while self.edge and st > self.edge_start + self.edge.delay:
self.edge_start += self.edge.delay
self.edge_cache = None
self.pick_edge(self.edge.new)
# If edge is None, then we have a permanent, static picture. Deal
# with that.
if not self.edge:
im = renpy.display.render.render(self.states[self.state].get_image(),
width, height,
st - self.edge_start)
# Otherwise, we have another edge.
else:
if not self.edge_cache:
self.update_cache()
im = renpy.display.render.render(self.edge_cache, width, height, st - self.edge_start)
renpy.display.render.redraw(self.edge_cache, self.edge.delay - (st - self.edge_start))
iw, ih = im.get_size()
rv = renpy.display.render.Render(iw, ih)
rv.blit(im, (0, 0))
return rv
def __call__(self, child=None, new_widget=None, old_widget=None):
"""
Used when this SMAnimation is used as a SMMotion. This creates
a duplicate of the animation, with all states containing None
as the image having that None replaced with the image that is provided here.
"""
if child is None:
child = new_widget
args = [ ]
for state in self.states.itervalues():
args.append(state.motion_copy(child))
for edges in self.edges.itervalues():
args.extend(edges)
return SMAnimation(self.initial, delay=self.delay, *args, **self.properties)
def Animation(*args):
"""
A Displayable that draws an animation, which is a series of images
that are displayed with time delays between them.
Odd (first, third, fifth, etc.) arguments to Animation are
interpreted as image filenames, while even arguments are the time
to delay between each image. If the number of arguments is odd,
the animation will stop with the last image (well, actually delay
for a year before looping). Otherwise, the animation will restart
after the final delay time.
"""
sm = [ 0 ]
for i, arg in enumerate(args):
if i % 2 == 0:
sm.append(State(i, arg))
else:
if i == len(args) - 1:
new = 0
else:
new = i + 1
sm.append(Edge(i - 1, arg, new))
return SMAnimation(*sm)
class Blink(renpy.display.core.Displayable):
"""
"""
def __init__(self, image, on=0.5, off=0.5, rise=0.5, set=0.5,
high=1.0, low=0.0, offset=0.0, **properties):
"""
This takes as an argument an image or widget, and blinks that image
by varying its alpha. The sequence of phases is
on - set - off - rise - on - ... All times are given in seconds, all
alphas are fractions between 0 and 1.
@param image: The image or widget that will be blinked.
@param on: The amount of time the widget spends on, at high alpha.
@param off: The amount of time the widget spends off, at low alpha.
@param rise: The amount time the widget takes to ramp from low to high alpha.
@param set: The amount of time the widget takes to ram from high to low.
@param high: The high alpha.
@param low: The low alpha.
@param offset: A time offset, in seconds. Use this to have a
blink that does not start at the start of the on phase.
"""
super(Blink, self).__init__(**properties)
self.image = renpy.display.im.image(image, loose=True)
self.on = on
self.off = off
self.rise = rise
self.set = set
self.high = high
self.low = low
self.offset = offset
self.cycle = on + set + off + rise
def get_placement(self):
return self.style
def render(self, height, width, st):
time = (self.offset + st) % self.cycle
alpha = self.high
if 0 <= time < self.on:
delay = self.on - time
alpha = self.high
time -= self.on
if 0 <= time < self.set:
delay = 0
frac = time / self.set
alpha = self.low * frac + self.high * (1.0 - frac)
time -= self.set
if 0 <= time < self.off:
delay = self.off - time
alpha = self.low
time -= self.off
if 0 <= time < self.rise:
delay = 0
frac = time / self.rise
alpha = self.high * frac + self.low * (1.0 - frac)
rend = renpy.display.render.render(self.image, height, width, st)
if not renpy.display.module.can_map:
return rend
w, h = rend.get_size()
rv = renpy.display.render.Render(w, h)
if alpha:
oldsurf = rend.pygame_surface()
if not (oldsurf.get_masks()[3]):
oldsurf = oldsurf.convert_alpha()
newsurf = pygame.Surface(oldsurf.get_size(), oldsurf.get_flags(), oldsurf)
amap = renpy.display.im.ramp(0, int(alpha * 255.0))
identity = renpy.display.im.identity
renpy.display.module.map(oldsurf, newsurf,
identity, identity, identity, amap)
renpy.display.render.mutated_surface(newsurf)
rv.blit(newsurf, (0, 0))
rv.depends_on(rend)
renpy.display.render.redraw(self, delay)
return rv
-385
View File
@@ -1,385 +0,0 @@
# 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.
# 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.
# 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():
global mixer_works
global read_raw_volume
global compute_midi_msf
global set_music_volume
global playing_midi
if mixer_works is not None:
return
try:
pygame.mixer.music.get_volume()
mixer_works = True
except:
if renpy.config.debug_sound:
raise
else:
mixer_works = False
windows_magic = False
if hasattr(sys, 'winver') and mixer_works:
try:
from ctypes import windll, c_uint, byref
winmm = windll.winmm
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
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
return True
compute_midi_msf = _compute_midi_msf
# 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
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
global master_music_volume
master_music_volume = vol
try:
pygame.mixer.music.set_volume(vol)
except:
if renpy.config.debug_sound:
raise
set_music_volume = _set_music_volume
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.
"""
if not mixer_works:
return None
mo = pygame.mixer.music.get_pos()
if mo < 0:
return None
mo /= 1000.0
return offset - mo
def music_start(filename, loops=-1, startpos=0.0):
"""
This starts music playing. If a music track is already playing,
stops that track in favor of this one.
@param filename: The file that the music will be played from. This
is relative to the game directory, and must be a real file (so it
cannot be stored in an archive.)
@param loops: The number of times the music will loop after it
finishes playing. If negative, the music will loop indefinitely.
Please note that even once the song has finished, rollback or load
may cause it to start playing again. So it may not be safe to have
this set to a non-negative value.
@param startpos: The number of seconds into the music to start playing.
"""
if not mixer_works:
return
music_stop()
renpy.game.context().scene_lists.music = (filename, loops, startpos)
restore_music()
def music_stop():
"""
Stops the currently playing music track.
"""
if not mixer_works:
return
renpy.game.context().scene_lists.music = None
restore_music()
def restore_music():
"""
This makes sure that the current music matches the music found in
the context.
"""
if not mixer_works:
return
global current_music
global fading
compute_midi_msf()
set_music_volume(1.0)
new_music = renpy.game.context().scene_lists.music
if not renpy.game.preferences.music:
new_music = None
if current_music == new_music:
return
if not mixer_enabled:
return
# Usually, ignore errors.
try:
if current_music != new_music and current_music:
current_music = None
pygame.mixer.music.fadeout(int(renpy.config.fade_music * 1000))
fading = True
else:
if not pygame.mixer.music.get_busy():
fn, loops, startpos = new_music
fading = False
detect_midi(fn)
pygame.mixer.music.load(renpy.game.basepath + "/" + fn)
pygame.mixer.music.play(loops, startpos)
set_music_volume(master_music_volume)
current_music = new_music
except pygame.error, e:
if renpy.config.debug_sound:
raise
else:
print "Error while trying to play music:", str(e)
# Plays sounds.
def play(fn, loops=0):
"""
This plays the given sound. The sound must be in a wav file,
and expected to have a sample rate 44100hz (changable with
config.sound_sample_rate), 16 bit, stereo. These expectations may
be violated, but that may lead to conversion delays.
Once a sound has been started, there's no way to stop it.
@param fn: The name of the file that the sound is read from. This
file may be contained in a game directory or an archive.
@param loops: The number of extra times the sound will be
played. (The default, 0, will play the sound once.)
"""
if not mixer_works:
return
if not fn:
return
if not renpy.game.preferences.sound:
return
if not mixer_enabled:
return
try:
sound = pygame.mixer.Sound(renpy.loader.load(fn))
sound.play()
except:
if renpy.config.debug_sound:
raise
def pre_init():
try:
bufsize = 4096
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 disable_mixer():
"""
This function is called by the video code to disable the
pygame mixer.
"""
if not mixer_works:
return
global mixer_enabled
if mixer_enabled:
try:
pygame.mixer.quit()
except:
if renpy.config.debug_sound:
raise
mixer_enabled = False
def enable_mixer():
"""
This function is called by the video code to enable the
pygame mixer.
"""
if not mixer_works:
return
global mixer_enabled
if not mixer_enabled:
try:
pygame.mixer.init()
except:
if renpy.config.debug_sound:
raise
mixer_enabled = True
+175 -306
View File
@@ -79,10 +79,12 @@ def skipping(ev):
"""
if map_event(ev, "skip"):
renpy.config.skipping = True
renpy.config.skipping = "slow"
renpy.exports.restart_interaction()
if map_keyup(ev, "skip"):
renpy.config.skipping = False
renpy.config.skipping = None
renpy.exports.restart_interaction()
return
@@ -111,35 +113,6 @@ class Keymap(renpy.display.layout.Null):
# def render(self, width, height, st):
# return None
class KeymouseBehavior(renpy.display.layout.Null):
"""
This is a class that causes the keyboard to move the mouse. It's
useful on the game and key menus, as well as in imagemaps and the
like.
"""
def event(self, ev, x, y):
if ev.type == renpy.display.core.DISPLAYTIME:
pressed = pygame.key.get_pressed()
x, y = pygame.mouse.get_pos()
ox, oy = x, y
if is_pressed(pressed, "keymouse_left"):
x -= renpy.config.keymouse_distance
if is_pressed(pressed, "keymouse_right"):
x += renpy.config.keymouse_distance
if is_pressed(pressed, "keymouse_up"):
y -= renpy.config.keymouse_distance
if is_pressed(pressed, "keymouse_down"):
y += renpy.config.keymouse_distance
if (x, y) != (ox, oy):
pygame.mouse.set_pos((x, y))
return None
class PauseBehavior(renpy.display.layout.Null):
"""
This is a class implementing the Pause behavior, which is to
@@ -168,23 +141,48 @@ class SayBehavior(renpy.display.layout.Null):
mouse button.
"""
def __init__(self):
super(SayBehavior, self).__init__()
focusable = True
def __init__(self, default=True, afm=None, **properties):
super(SayBehavior, self).__init__(default=default, **properties)
if afm is not None:
self.afm_length = len(afm)
else:
self.afm_length = None
def set_afm_length(self, afm_length):
self.afm_length = afm_length
def event(self, ev, x, y):
if ev.type == renpy.display.core.DISPLAYTIME and \
renpy.config.allow_skipping and renpy.config.skipping and \
ev.duration > renpy.config.skip_delay / 1000.0:
renpy.config.allow_skipping and renpy.config.skipping and \
ev.duration > renpy.config.skip_delay / 1000.0:
if renpy.game.preferences.skip_unseen:
return True
elif renpy.config.skipping == "fast":
return True
elif renpy.game.context().seen_current(True):
return True
if ev.type == renpy.display.core.DISPLAYTIME and \
self.afm_length and renpy.game.preferences.afm_time:
afm_delay = ( 1.0 * ( renpy.config.afm_bonus + self.afm_length ) / renpy.config.afm_characters ) * renpy.game.preferences.afm_time
if map_event(ev, "dismiss"):
if renpy.game.preferences.text_cps:
afm_delay += 1.0 / renpy.game.preferences.text_cps * self.afm_length
if ev.duration > afm_delay:
if renpy.config.afm_callback:
if renpy.config.afm_callback():
return True
else:
return True
if map_event(ev, "dismiss") and self.is_focused():
return True
if map_event(ev, "rollforward"):
@@ -192,211 +190,83 @@ 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):
hovered=None, unhovered=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.unhovered = unhovered
self.focusable = clicked is not None
def render(self, width, height, st):
if self.activated:
self.set_style_prefix('activate_')
elif self.old_hover:
self.set_style_prefix('hover_')
else:
self.set_style_prefix('idle_')
rv = super(Button, self).render(width, height, st)
if self.clicked:
rv.add_focus(self,
None,
self.style.left_margin,
self.style.top_margin,
rv.width - self.style.right_margin,
rv.height - self.style.bottom_margin)
return rv
return super(Button, self).render(width, height, st)
def focus(self, default=False):
super(Button, self).focus(default)
def set_hover(self, hover):
"""
Called when we change from hovered to un-hovered, or
vice-versa.
"""
if self.hovered:
self.hovered()
# if hover:
# self.style.set_prefix('hover_')
# else:
# self.style.set_prefix('idle_')
renpy.display.render.redraw(self, 0)
def unfocus(self):
super(Button, self).unfocus()
if self.unhovered:
self.unhovered()
def event(self, ev, x, y):
# We deactivate on an event.
self.activated = False
if self.activated:
self.activated = False
inside = False
width, height = self.window_size
if x >= 0 and x < width and y >= 0 and y < height:
inside = True
if self.style.enable_hover and ev.type == MOUSEMOTION:
if self.old_hover != inside:
self.old_hover = inside
self.set_hover(inside)
if inside:
if self.hovered:
self.hovered()
renpy.display.audio.play(self.style.hover_sound)
if map_event(ev, "button_select"):
if inside and self.clicked:
renpy.display.audio.play(self.style.activate_sound)
self.activated = True
renpy.display.render.redraw(self, 0)
rv = self.clicked()
if rv is not None:
return rv
if self.focusable:
if self.is_focused():
self.set_style_prefix('hover_')
else:
raise renpy.display.core.IgnoreEvent()
self.set_style_prefix('idle_')
else:
self.set_style_prefix('insensitive_')
# If not focused, ignore all events.
if not self.is_focused():
return None
# If clicked,
if map_event(ev, "button_select") and self.clicked:
self.activated = True
self.set_style_prefix('activate_')
renpy.audio.sound.play(self.style.sound)
rv = self.clicked()
if rv is not None:
return rv
else:
raise renpy.display.core.IgnoreEvent()
return None
return super(Button, self).event(ev, x, y)
# Reimplementation of the TextButton widget as a Button and a Text
# widget.
def TextButton(text, style='button', text_style='button_text',
@@ -405,29 +275,6 @@ def TextButton(text, style='button', text_style='button_text',
text = renpy.display.text.Text(text, style=text_style)
return Button(text, style=style, clicked=clicked, **properties)
# class TextButton(Button):
# def __init__(self, text, style='button', text_style='button_text',
# clicked=None):
# self.text_widget = renpy.display.text.Text(text, style=text_style)
# super(TextButton, self).__init__(self.text_widget,
# style=style,
# clicked=clicked)
# self.text_widget.style.set_prefix('idle_')
# def set_hover(self, hover):
# super(TextButton, self).set_hover(hover)
# if hover:
# self.text_widget.style.set_prefix("hover_")
# else:
# self.text_widget.style.set_prefix("idle_")
class Input(renpy.display.text.Text):
"""
@@ -457,13 +304,12 @@ class Input(renpy.display.text.Text):
self.set_text(self.content.replace("{", "{{") + "_")
renpy.display.render.redraw(self, 0)
elif map_event(ev, "input_enter"):
return self.content
elif ev.type == KEYDOWN and ev.unicode:
if ord(ev.unicode[0]) < 32:
return None
return None
if self.length and len(self.content) >= self.length:
raise renpy.display.core.IgnoreEvent()
@@ -488,89 +334,112 @@ class Bar(renpy.display.core.Displayable):
to clicks on that value.
"""
def __init__(self, width, height, range, value, clicked=None,
style='bar', **properties):
def __init__(self, range, value, width=None, height=None,
changed=None, style='bar', **properties):
super(Bar, self).__init__()
if width is not None:
properties['xmaximum'] = width
self.style = renpy.style.Style(style, properties)
if height is not None:
properties['ymaximum'] = height
super(Bar, self).__init__(style=style, **properties)
self.width = width
self.height = height
self.range = range
self.value = value
self.changed = changed
self.focusable = changed is not None
self.clicked = clicked
def render(self, width, height, st):
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
# Store the width for the event function to use.
self.width = width
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
zone_width = width - lgutter - rgutter
# This makes it easier to select 100%.
x = x - lgutter
x = x + (barwidth / self.range // 2)
left_width = zone_width * self.value // self.range
right_width = zone_width - left_width
value = x * self.range / barwidth
value = max(value, 0)
rv = self.clicked(value)
if rv is not None:
return rv
else:
raise renpy.display.core.IgnoreEvent()
def render(self, width, height, st):
width = self.width
height = self.height
# The amount of space taken up by the bars.
if self.clicked:
lgutter = self.style.left_gutter
rgutter = self.style.right_gutter
else:
lgutter = 0
rgutter = 0
barwidth = width - lgutter - rgutter
left_width = barwidth * self.value // self.range
right_width = barwidth - left_width
left_width += lgutter
right_width += rgutter
rv = renpy.display.render.Render(width, height)
lsurf = render(self.style.left_bar, left_width, height, st)
rsurf = render(self.style.right_bar, right_width, height, st)
lsurf = render(self.style.left_bar, width, height, st)
rsurf = render(self.style.right_bar, width, height, st)
rv.blit(lsurf, (lgutter, 0))
rv.blit(rsurf, (lgutter + left_width, 0))
if self.style.thumb_shadow:
surf = render(self.style.thumb_shadow, width, height, st)
rv.blit(surf, (left_width + self.style.thumb_offset, 0))
rv.blit(lsurf.subsurface((0, 0, left_width, height)), (0, 0))
rv.blit(rsurf.subsurface((left_width, 0, right_width, height)),
(left_width, 0))
if self.style.thumb:
surf = render(self.style.thumb, width, height, st)
rv.blit(surf, (left_width + self.style.thumb_offset, 0))
if self.changed:
rv.add_focus(self, None, 0, 0, width, height)
return rv
def event(self, ev, x, y):
if not self.changed:
return
if not self.is_focused():
return
old_value = self.value
grabbed = (renpy.display.focus.get_grab() is self)
just_grabbed = False
if not grabbed and map_event(ev, "bar_activate"):
renpy.display.focus.set_grab(self)
just_grabbed = True
grabbed = True
if grabbed:
if map_event(ev, "bar_decrease"):
self.value -= 1
if map_event(ev, "bar_increase"):
self.value += 1
if ev.type in (MOUSEMOTION, MOUSEBUTTONUP, MOUSEBUTTONDOWN):
lgutter = self.style.left_gutter
rgutter = self.style.right_gutter
zone_width = self.width - lgutter - rgutter
self.value = (x - lgutter) * self.range // zone_width
if self.value < 0:
self.value = 0
if self.value > self.range:
self.value = self.range
if grabbed and not just_grabbed and map_event(ev, "bar_deactivate"):
renpy.display.focus.set_grab(None)
if self.value != old_value:
renpy.display.render.redraw(self, 0)
return self.changed(self.value)
return
def get_placement(self):
return self.style
class Conditional(renpy.display.layout.Container):
"""
@@ -587,7 +456,7 @@ class Conditional(renpy.display.layout.Container):
self.condition = condition
self.null = renpy.display.layout.Null()
self.state = eval(self.condition, renpy.game.store)
self.state = eval(self.condition, vars(renpy.store))
def render(self, width, height, st):
if self.state:
@@ -597,7 +466,7 @@ class Conditional(renpy.display.layout.Container):
def event(self, ev, x, y):
state = eval(self.condition, renpy.game.store)
state = eval(self.condition, vars(renpy.store))
if state != self.state:
renpy.display.render.redraw(self, 0)
+399 -150
View File
@@ -2,10 +2,11 @@
# window.
import renpy
from renpy.display.render import render
import pygame
from pygame.constants import *
import sys
import os
import time
import cStringIO
@@ -33,9 +34,40 @@ class Displayable(renpy.object.Object):
their fields.
"""
def __init__(self):
self.style = None
self.style_prefix = None
focusable = False
def __init__(self, focus=None, default=False, style='default', **properties):
self.style = renpy.style.Style(style, properties)
self.style_prefix = 'insensitive_'
self.focus_name = focus
self.default = default
def find_focusable(self, callback, focus_name):
if self.focusable:
callback(self, self.focus_name or focus_name)
def focus(self, default=False):
"""
Called to indicate that this widget has the focus.
"""
if self.style.enable_hover:
self.set_style_prefix("hover_")
if not default:
renpy.audio.sound.play(self.style.sound)
def unfocus(self):
"""
Called to indicate that this widget has become unfocused.
"""
if self.style.enable_hover:
self.set_style_prefix("idle_")
def is_focused(self):
return renpy.game.context().scene_lists.focused is self
def set_style_prefix(self, prefix):
"""
@@ -46,9 +78,7 @@ class Displayable(renpy.object.Object):
if prefix == self.style_prefix:
return
if self.style:
self.style.set_prefix(prefix)
self.style.set_prefix(prefix)
self.style_prefix = prefix
renpy.display.render.redraw(self, 0)
@@ -106,8 +136,8 @@ class Displayable(renpy.object.Object):
the images it may want to load.
"""
return
if self.style and self.style.background:
self.style.background.predict(callback)
def place(self, dest, x, y, width, height, surf):
"""
@@ -143,14 +173,20 @@ class Displayable(renpy.object.Object):
if isinstance(xoff, float):
xoff = int(xoff * width)
if style.xanchor == 'left':
xoff -= 0
elif style.xanchor == 'center':
xoff -= sw / 2
elif style.xanchor == 'right':
xoff -= sw
else:
raise Exception("xanchor '%s' is not known." % style.xanchor)
xanchor = style.xanchor
if xanchor == 'left':
xanchor = 0.0
elif xanchor == 'center':
xanchor = 0.5
elif xanchor == 'right':
xanchor = 1.0
elif not isinstance(xanchor, (float, int)):
raise Exception("xanchor %r is not known." % xanchor)
xoff -= int(sw * xanchor)
xoff += x
@@ -160,15 +196,18 @@ class Displayable(renpy.object.Object):
if isinstance(yoff, float):
yoff = int(yoff * height)
if style.yanchor == 'top':
yoff -= 0
elif style.yanchor == 'center':
yoff -= sh / 2
elif style.yanchor == 'bottom':
yoff -= sh
else:
raise Exception("yanchor '%s' is not known." % style.yanchor)
yanchor = style.yanchor
if yanchor == 'top':
yanchor = 0.0
elif yanchor == 'center':
yanchor = 0.5
elif style.yanchor == 'bottom':
yanchor = 1.0
elif not isinstance(yanchor, (float, int)):
raise Exception("yanchor %r is not known." % yanchor)
yoff -= int(sh * yanchor)
yoff += y
# print self, xoff, yoff
@@ -193,8 +232,7 @@ class SceneLists(object):
@ivar transient: The current transient display list.
@ivar overlay: The current overlay display list.
@ivar music: Opaque information about the music that is being played.
@ivar focused: The widget that is currently focused.
"""
def __init__(self, oldsl=None):
@@ -203,22 +241,26 @@ class SceneLists(object):
if oldsl:
for i in renpy.config.layers:
for i in renpy.config.layers + renpy.config.top_layers:
self.layers[i] = oldsl.layers[i][:]
for i in renpy.config.overlay_layers:
self.clear(i)
self.replace_transient()
self.music = oldsl.music
self.sticky_positions = oldsl.sticky_positions.copy()
self.movie = oldsl.movie
self.focused = None
else:
for i in renpy.config.layers:
for i in renpy.config.layers + renpy.config.top_layers:
self.layers[i] = [ ]
self.music = None
self.movie = None
self.sticky_positions = { }
self.focused = None
def rollback_copy(self):
"""
@@ -227,9 +269,7 @@ class SceneLists(object):
"""
rv = SceneLists(self)
for i in renpy.config.overlay_layers:
rv.layers[i] = [ ]
rv.focused = None
# rv.master = self.master[:]
# rv.transient = self.transient[:]
@@ -251,6 +291,21 @@ class SceneLists(object):
for i in renpy.config.transient_layers:
self.layers[i] = [ ]
def transient_is_empty(self):
"""
This returns True if all transient layers are empty. This is
used by the rollback code, as we can't start a new rollback
if there is something in a transient layer (as things in the
transient layer may contain objects that cannot be pickled,
like lambdas.)
"""
for i in renpy.config.transient_layers:
if self.layers[i]:
return False
return True
def add(self, layer, thing, key=None):
"""
This is called to add something to a layer. Layer is
@@ -317,9 +372,9 @@ class Display(object):
"""
This is responsible for managing the display window.
@ivar window: The window that is being presented to the user.
@ivar interface: The interface corresponding to this display.
@ivar buffer: A surface that buffers the window.
@ivar window: The window that is being presented to the user.
@ivar sample_surface: A sample surface that is optimized for fast
blitting to the window, with alpha. Used to create other surfaces
@@ -333,36 +388,55 @@ class Display(object):
@ivar mouse_location: The mouse location the last time it was
drawn, or None if it wasn't drawn the last time around.
@ivar mouse_backing: A backing store image holding the background
that goes behind the mouse.
@ivar mouse_backing_pos: The position of the upper-left hand
corner of the backing pos, relative to the window.
@ivar full_redraw: Force a full redraw.
@ivar next_frame: The time when the next frame should be drawn. In
ms returned from pygame.time.get_ticks().
"""
def __init__(self):
def __init__(self, interface):
self.interface = interface
# Ensure that we kill off the presplash.
renpy.display.presplash.end()
# Ensure that we kill off the movie when changing screen res.
renpy.display.video.movie_stop(clear=False)
renpy.display.audio.pre_init()
pygame.init()
renpy.display.audio.init()
pygame.display.init()
pygame.font.init()
renpy.audio.audio.init()
self.fullscreen = renpy.game.preferences.fullscreen
fsflag = 0
if self.fullscreen:
fullscreen = self.fullscreen and not os.environ.get('RENPY_DISABLE_FULLSCREEN', False)
if fullscreen:
fsflag = FULLSCREEN
# Pick an appropriate display mode. Prefer 32, but accept 24
# before letting SDL do conversions.
if 24 == pygame.display.mode_ok((renpy.config.screen_width,
renpy.config.screen_height),
fsflag, 32):
depth = 24
else:
depth = 32
# The window we display things in.
self.window = pygame.display.set_mode((renpy.config.screen_width,
renpy.config.screen_height),
fsflag)
# The mouse buffer.
if renpy.config.mouse:
self.buffer = pygame.Surface((renpy.config.screen_width,
renpy.config.screen_height))
fsflag, depth)
# Sample surface that all surfaces are created based on.
self.sample_surface = self.window.convert_alpha()
@@ -372,59 +446,151 @@ class Display(object):
pygame.display.set_caption(renpy.config.window_title)
if renpy.config.window_icon:
pygame.display.set_icon(renpy.display.image.cache.load_image(renpy.config.window_icon))
pygame.display.set_icon(renpy.display.im.load_image(renpy.config.window_icon))
# Load the mouse image, if any.
if renpy.config.mouse:
self.mouse = renpy.display.image.cache.load_image(renpy.config.mouse)
self.mouse = True
pygame.mouse.set_visible(False)
else:
self.mouse = None
pygame.mouse.set_visible(True)
self.mouse_location = None
self.suppress_mouse = False
self.mouse_backing = None
self.mouse_backing_pos = None
self.mouse_info = None
self.suppress_mouse = False
self.full_redraw = True
self.next_frame = 0
def can_redraw(self, first_pass):
"""
Uses the framerate to determine if we can and should redraw.
"""
framerate = renpy.config.framerate
if framerate is None:
return True
next_frame = self.next_frame
now = pygame.time.get_ticks()
frametime = 1000 / framerate
# Handle timer rollover.
if next_frame > now + frametime:
next_frame = now
# It's not yet time for the next frame.
if now < next_frame and not first_pass:
return False
# Otherwise, it is. Schedule the next frame.
if next_frame + frametime < now:
next_frame = now + frametime
else:
next_frame += frametime
self.next_frame = next_frame
return True
def show_mouse(self, pos, info):
"""
Actually shows the mouse.
"""
self.mouse_location = pos
self.mouse_info = info
img, mxo, myo = info
mouse = renpy.display.im.load_image(img)
mx, my = pos
mw, mh = mouse.get_size()
bx = mx - mxo
by = my - myo
self.mouse_backing_pos = (bx, by)
self.mouse_backing = pygame.Surface((mw, mh), self.window.get_flags(), self.window)
self.mouse_backing.blit(self.window, (0, 0), (bx, by, mw, mh))
self.window.blit(mouse, (bx, by))
return bx, by, mw, mh
def hide_mouse(self):
"""
Actually hides the mouse.
"""
size = self.mouse_backing.get_size()
self.window.blit(self.mouse_backing, self.mouse_backing_pos)
rv = self.mouse_backing_pos + size
self.mouse_backing = None
self.mouse_backing_pos = None
self.mouse_location = None
return rv
def draw_mouse(self, show_mouse=True):
"""
This draws the mouse to the screen, if necessary. It uses the
buffer to minimize the amount of the screen that needs to be
drawn, and only redraws if the mouse has actually been moved.
"""
if not self.mouse:
return
return [ ]
if self.suppress_mouse:
return
return [ ]
# Figure out the mouse animation.
if self.interface.mouse in renpy.config.mouse:
anim = renpy.config.mouse[self.interface.mouse]
else:
anim = renpy.config.mouse['default']
info = anim[self.interface.ticks % len(anim)]
mw, mh = self.mouse.get_size()
pos = pygame.mouse.get_pos()
if not pygame.mouse.get_focused():
pos = None
if pos == self.mouse_location and show_mouse and info == self.mouse_info:
return [ ]
if not show_mouse:
pos = None
if not pos and not show_mouse:
return [ ]
updates = [ ]
if self.mouse_location and self.mouse_location != pos:
ox, oy = self.mouse_location
self.window.blit(self.buffer, (ox, oy), (ox, oy, mw, mh))
updates.append((ox, oy, mw, mh))
if self.mouse_location:
updates.append(self.hide_mouse())
if pos and (pos != self.mouse_location):
self.window.blit(self.mouse, pos)
updates.append(pos + (mw, mh))
self.mouse_location = pos
pygame.display.update(updates)
if show_mouse:
updates.append(self.show_mouse(pos, info))
return updates
def update_mouse(self):
"""
Draws the mouse, and then updates the screen.
"""
updates = self.draw_mouse()
if updates:
pygame.display.update(updates)
def show(self, root_widget, suppress_blit):
"""
@@ -434,6 +600,13 @@ class Display(object):
relative to the screen.
"""
# if self.next_draw < pygame.time.get_ticks():
# self.next_draw = pygame.time.get_ticks()
# pygame.time.delay(self.next_draw - pygame.time.get_ticks())
# self.next_draw += 1000 / 30
surftree = renpy.display.render.render_screen(
root_widget,
renpy.config.screen_width,
@@ -442,25 +615,27 @@ class Display(object):
if not suppress_blit:
if self.mouse:
self.draw_mouse(False)
updates = [ ]
updates.extend(self.draw_mouse(False))
damage = renpy.display.render.screen_blit(surftree, self.full_redraw)
self.full_redraw = False
if self.mouse:
if damage:
self.buffer.blit(self.window, damage, damage)
self.draw_mouse(True)
if damage:
pygame.display.update(damage)
updates.append(damage)
self.full_redraw = False
updates.extend(self.draw_mouse(True))
pygame.display.update(updates)
else:
self.full_redraw = True
self.suppress_mouse = suppress_blit
renpy.display.focus.take_focuses(surftree.focuses)
def save_screenshot(self, filename):
"""
@@ -475,8 +650,8 @@ class Display(object):
contents of the window.
"""
surf = pygame.transform.scale(self.window, scale)
surf = renpy.display.module.scale(self.window, scale)
sio = cStringIO.StringIO()
pygame.image.save(surf, sio)
rv = sio.getvalue()
@@ -517,10 +692,14 @@ class Interface(object):
@ivar pushed_event: If not None, an event that was pushed back
onto the stack.
@ivar mouse: The name of the mouse cursor to use during the current
interaction.
@ivar ticks: The number of 20hz ticks.
"""
def __init__(self):
self.display = Display()
self.display = Display(self)
self.profile_time = time.time()
self.screenshot = None
self.old_scene = None
@@ -530,6 +709,8 @@ class Interface(object):
self.force_redraw = False
self.restart_interaction = False
self.pushed_event = None
self.ticks = 0
self.mouse = 'default'
def take_screenshot(self, scale):
"""
@@ -564,16 +745,16 @@ class Interface(object):
scene_lists = renpy.game.context().scene_lists
# We don't want the overlay to be clear here.
old_old_scene = self.old_scene
if not renpy.config.overlay_during_wait:
self.old_scene = self.compute_scene(scene_lists)
if renpy.config.overlay_during_with and old_old_scene:
for i in renpy.config.overlay_layers:
scene_lists.clear(i)
self.old_scene[i] = old_old_scene[i]
self.old_scene = self.compute_scene(scene_lists)
# self.old_scene.append_scene_list(scene_lists.master)
scene_lists = renpy.game.context().scene_lists
scene_lists.replace_transient()
def set_transition(self, transition, layer=None):
"""
@@ -583,6 +764,7 @@ class Interface(object):
self.transition[layer] = transition
def event_peek(self):
"""
This peeks the next event. It returns None if no event exists.
@@ -599,6 +781,21 @@ class Interface(object):
self.pushed_event = ev
return ev
def event_poll(self):
"""
Called to busy-wait for an event while we're waiting to
redraw a frame.
"""
if self.pushed_event:
rv = self.pushed_event
self.pushed_event = None
return rv
else:
return pygame.event.poll()
def event_wait(self):
"""
This is in its own function so that we can track in the
@@ -611,35 +808,32 @@ class Interface(object):
return rv
# We load at most one image per wait.
if renpy.display.image.cache.needs_preload():
ev = pygame.event.poll()
if ev.type != NOEVENT:
return ev
if renpy.display.im.cache.needs_preload():
ev = pygame.event.poll()
if ev.type != NOEVENT:
return ev
renpy.display.image.cache.preload()
return pygame.event.wait()
renpy.display.im.cache.preload()
ev = pygame.event.wait()
return ev
def compute_scene(self, scene_lists):
"""
This converts scene lists into a dictionary mapping layer
name to a Fixed containing that layer. None is mapped
to a fixed containing all of the layers.
name to a Fixed containing that layer.
"""
rv = { }
root = renpy.display.layout.Fixed()
for layer in renpy.config.layers + renpy.config.top_layers:
f = renpy.display.layout.Fixed(focus=layer)
for layer in renpy.config.layers:
f = renpy.display.layout.Fixed()
f.append_scene_list(scene_lists.layers[layer])
rv[layer] = f
root.add(f)
rv[None] = root
return rv
@@ -652,24 +846,34 @@ class Interface(object):
# These things can be done once per interaction.
repeat = True
while repeat:
repeat, rv = self.interact_core(**kwargs)
try:
# Clean out transient stuff at the end of an interaction.
scene_lists = renpy.game.context().scene_lists
scene_lists.replace_transient()
for i in renpy.config.start_interact_callbacks:
i()
return rv
repeat = True
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()
self.restart_interaction = True
def interact_core(self,
show_mouse=True,
trans_pause=False,
suppress_overlay=False,
suppress_underlay=False,
):
show_mouse=True,
trans_pause=False,
suppress_overlay=False,
suppress_underlay=False,
mouse='default',
):
"""
This handles one cycle of displaying an image to the user,
@@ -685,7 +889,7 @@ class Interface(object):
@param suppress_underlay: This suppresses the display of the underlay.
"""
self.suppress_transition |= renpy.config.skipping
self.suppress_transition = self.suppress_transition or renpy.config.skipping
## Safety condition, prevents deadlocks.
if trans_pause:
@@ -697,10 +901,18 @@ class Interface(object):
# We just restarted.
self.restart_interaction = False
# Setup the mouse.
self.mouse = mouse
# frames = 0
for i in renpy.config.interact_callbacks:
i()
renpy.audio.audio.interact()
# Tick time forward.
renpy.display.image.cache.tick()
renpy.display.im.cache.tick()
# Set up key repeats.
# pygame.time.set_timer(KEYREPEATEVENT, renpy.config.skip_delay)
@@ -711,17 +923,11 @@ class Interface(object):
# Clear some events.
pygame.event.clear((MOUSEMOTION, DISPLAYTIME,
MOUSEBUTTONUP, MOUSEBUTTONDOWN))
# Figure out the scene list we want to show.
start_time = time.time()
# Figure out the scene list we want to show.
scene_lists = renpy.game.context().scene_lists
# Figure out what the overlay layer should look like.
for i in renpy.config.overlay_layers:
scene_lists.clear(i)
renpy.ui.layer("overlay")
if not suppress_overlay:
@@ -739,26 +945,54 @@ class Interface(object):
# The root widget of everything that is displayed on the screen.
root_widget = renpy.display.layout.Fixed()
root_widget.append_scene_list(underlay)
root_widget.layers = { }
# 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()
layers_root.layers = { }
# Add layers (perhaps with transitions) to the layers root.
for layer in renpy.config.layers:
if layer in self.transition and self.old_scene and not self.suppress_transition:
def add_layer(where, layer):
if self.transition.get(layer, None) and self.old_scene and not self.suppress_transition:
trans = self.transition[layer](old_widget=self.old_scene[layer],
new_widget=scene[layer])
layers_root.add(trans, start_time)
where.add(trans, start_time)
where.layers[layer] = trans
else:
layers_root.add(scene[layer], start_time)
where.layers[layer] = scene[layer]
where.add(scene[layer], start_time)
# Add layers (perhaps with transitions) to the layers root.
for layer in renpy.config.layers:
add_layer(layers_root, layer)
# Add layers_root to root_widget, perhaps through a transition.
if None in self.transition and self.old_scene and not self.suppress_transition:
trans = self.transition[None](old_widget=self.old_scene[None],
# Compute what the old root should be.
old_root = renpy.display.layout.Fixed()
old_root.layers = { }
for layer in renpy.config.layers:
old_root.layers[layer] = self.old_scene[layer]
old_root.add(self.old_scene[layer], start_time)
trans = self.transition[None](old_widget=old_root,
new_widget=layers_root)
root_widget.add(trans, start_time)
@@ -774,6 +1008,11 @@ class Interface(object):
root_widget.add(layers_root, start_time)
# Add top_layers to the root_widget.
for layer in renpy.config.top_layers:
add_layer(root_widget, layer)
# Now, update various things regarding scenes and transitions,
# so we are ready for a new interaction or a restart.
self.old_scene = scene
@@ -784,13 +1023,15 @@ 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()))
# First pass through the while loop?
first_pass = True
# We only want to do prediction once, but we will defer it as
# long as possible.
@@ -806,7 +1047,7 @@ class Interface(object):
# Check for a change in fullscreen preference.
if self.display.fullscreen != renpy.game.preferences.fullscreen:
self.display = Display()
self.display = Display(self)
needs_redraw = True
# Check for a forced redraw.
@@ -815,8 +1056,9 @@ class Interface(object):
self.force_redraw = False
# Redraw the screen.
if needs_redraw:
if needs_redraw and self.display.can_redraw(first_pass):
needs_redraw = False
first_pass = False
# If we have a movie, start showing it.
suppress_blit = renpy.display.video.interact()
@@ -829,17 +1071,17 @@ class Interface(object):
renpy.config.frames += 1
# If profiling is enabled, report the profile time.
if renpy.config.profile:
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)
# Draw the mouse, if it needs drawing.
if show_mouse:
self.display.draw_mouse()
self.display.update_mouse()
# Determine if we need a redraw.
needs_redraw = renpy.display.render.process_redraws()
needs_redraw = needs_redraw or renpy.display.render.process_redraws()
# If we need to redraw again, do it if we don't have an
# event going on.
@@ -848,28 +1090,31 @@ class Interface(object):
# Predict images, if we haven't done so already.
if not did_prediction and not self.event_peek():
renpy.game.context().predict(renpy.display.image.cache.preload_image)
root_widget.predict(renpy.display.image.cache.preload_image)
root_widget.predict(renpy.display.im.cache.preload_image)
renpy.game.context().predict(renpy.display.im.cache.preload_image)
did_prediction = True
try:
ev = self.event_wait()
if needs_redraw:
ev = self.event_poll()
else:
ev = self.event_wait()
if ev.type == NOEVENT:
continue
self.profile_time = time.time()
if ev.type == DISPLAYTIME:
events = 1 + len(pygame.event.get([DISPLAYTIME]))
self.ticks += events
renpy.game.context().runtime += events * DISPLAYTIME_INTERVAL
renpy.audio.audio.periodic()
ev = pygame.event.Event(DISPLAYTIME, {},
duration=(time.time() - start_time))
# Update the playing music, if necessary.
# This needs to be here so that we eventually start a
# new song at the end of a fadeout.
renpy.display.audio.restore_music()
# Handle skipping.
renpy.display.behavior.skipping(ev)
@@ -890,8 +1135,9 @@ class Interface(object):
if len(evs):
ev = evs[-1]
# x, y = getattr(ev, 'pos', (0, 0))
renpy.display.focus.event_handler(ev)
# x, y = getattr(ev, 'pos', (0, 0))
x, y = pygame.mouse.get_pos()
rv = root_widget.event(ev, x, y)
@@ -914,6 +1160,10 @@ class Interface(object):
finally:
# Clean out the overlay layers.
for i in renpy.config.overlay_layers:
scene_lists.clear(i)
# pygame.time.set_timer(KEYREPEATEVENT, 0)
pygame.time.set_timer(DISPLAYTIME, 0)
@@ -922,4 +1172,3 @@ class Interface(object):
self.restart_interaction = True
# print "It took", frames, "frames."
+372
View File
@@ -0,0 +1,372 @@
# 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))
# The widget currently grabbing the input, if any.
grab = None
# 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
def set_grab(widget):
global grab
grab = widget
def get_grab():
return grab
# 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):
# Clear out an old grab.
global grab
grab = None
# 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 not current:
f.unfocus()
if current:
current.focus(default=True)
# This changes the focus to be the widget contained inside the new
# focus object.
def change_focus(newfocus):
if grab:
return
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)
+730
View File
@@ -0,0 +1,730 @@
# 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)
def predict_files(self):
"""
Returns a list of files that will be accessed when this image
operation is performed.
"""
return [ ]
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_masks()[3]:
im = im.convert_alpha()
else:
im = im.convert()
return im
def predict_files(self):
return [ self.filename ]
class Composite(ImageBase):
"""
This image manipulator composites one or more images together.
"""
def __init__(self, size, *args, **properties):
"""
This takes a variable number of arguments. The first argument
is size, which is either the desired size of the image (in
pixels), or None to indicate that the size should be the size of
the first image.
It then takes an even number of further arguments. (For an odd
number of total arguments.) The second and other even numbered
arguments contain position tuples, while the third and further
odd-numbered arguments give images (or image
manipulators). A position argument gives the position of the
image immediately following it, with the position expressed as
a tuple giving an offset from the upper-left corner of the
image. The images are composited in bottom-to-top order, with
the last image being closest to the user.
"""
super(Composite, self).__init__(size, *args, **properties)
if len(args) % 2 != 0:
raise Exception("Composite requires an odd 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
def predict_files(self):
rv = [ ]
for i in self.images:
rv.extend(i.predict_files())
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):
# 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
# Quick exit.
if sx0 == sx1 or sy0 == sy1:
return
# 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, **properties):
im = image(im)
super(Scale, self).__init__(im, width, height, **properties)
self.image = im
self.width = width
self.height = height
def load(self):
return pygame.transform.scale(cache.get(self.image),
(self.width, self.height))
def predict_files(self):
return self.image.predict_files()
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, **properties):
"""
@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, **properties)
self.image = im
self.angle = angle
self.zoom = zoom
def load(self):
rv = pygame.transform.rotozoom(cache.get(self.image),
self.angle, self.zoom)
return rv
def predict_files(self):
return self.image.predict_files()
class Crop(ImageBase):
"""
This crops the image that is its child.
"""
def __init__(self, im, x, y, w, h, **properties):
im = image(im)
super(Crop, self).__init__(im, x, y, w, h, **properties)
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 predict_files(self):
return self.image.predict_files()
def ramp(start, end):
"""
Returns a 256 character linear ramp, where the first character has
the value start and the last character has the value end. Such a
ramp can be used as a map argument of im.Map.
"""
chars = [ ]
for i in range(0, 256):
i = i / 255.0
chars.append(chr(int( end * i + start * (1.0 - i) ) ) )
return "".join(chars)
identity = ramp(0, 255)
class Map(ImageBase):
"""
This adjusts the colors of the image that is its child. It takes
as arguments 4 256 character strings. If a pixel channel has a
value of 192, then the value of the 192nd character in the string
is used for the mapped pixel component.
"""
def __init__(self, im, rmap=identity, gmap=identity, bmap=identity,
amap=identity, force_alpha=False, **properties):
im = image(im)
super(Map, self).__init__(im, rmap, gmap, bmap, amap, force_alpha, **properties)
self.image = im
self.rmap = rmap
self.gmap = gmap
self.bmap = bmap
self.amap = amap
self.force_alpha = force_alpha
def load(self):
surf = cache.get(self.image)
if not renpy.display.module.can_map:
return surf
if self.force_alpha and not (surf.get_masks()[3]):
surf = surf.convert_alpha()
rv = pygame.Surface(surf.get_size(), surf.get_flags(), surf)
renpy.display.module.map(surf, rv,
self.rmap, self.gmap, self.bmap, self.amap)
return rv
def predict_files(self):
return self.image.predict_files()
def Alpha(image, alpha, **properties):
"""
Returns an alpha-mapped version of the image. Alpha is the maximum
alpha that this image can have, a number between 0.0 (fully
transparent) and 1.0 (opaque).
If an image already has an alpha channel, values in that alpha
channel are reduced as appropriate.
"""
amap = ramp(0, int(255 * alpha))
return Map(image, identity, identity, identity, amap, force_alpha=True, **properties)
class Tile(ImageBase):
"""
This tiles the image, repeating it vertically and horizontally
until it is as large as the specified size. If no size is given,
then the size defaults to the size of the screen.
"""
def __init__(self, im, size=None, **properties):
im = image(im)
super(Tile, self).__init__(im, size, **properties)
self.image = im
self.size = size
def load(self):
size = self.size
if size is None:
size = (renpy.config.screen_width, renpy.config.screen_height)
surf = cache.get(self.image)
width, height = size
sw, sh = surf.get_size()
rv = pygame.Surface(size, 0,
renpy.game.interface.display.sample_surface)
for y in range(0, height, sh):
for x in range(0, width, sw):
rv.blit(surf, (x, y))
return rv
def predict_files(self):
return self.image.predict_files()
def image(arg, loose=False, **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 the loose argument is False, then this will report an error if an
arbitrary argument is given. If it's True, then the argument is passed
through unchanged.
"""
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)
if loose:
return arg
if 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))
+53 -408
View File
@@ -1,209 +1,14 @@
# This file contains some miscellanious displayables that involve images.
# Most of the guts of this file have been moved into im.py, with only some
# of the stuff thar uses images remaining.
import renpy
from renpy.display.render import render
import pygame
from pygame.constants import *
class ImageCache(object):
def __init__(self):
# A monotonically increasing time.
self.time = 0
# A map from image filename to surface.
self.surface_map = { }
# A map from image filename to last access time.
self.time_map = { }
# The list of things we want to preload.
self.preloads = [ ]
def tick(self):
self.time += 1
self.preloads = [ ]
def really_load_image(self, fn):
"""
This is called by load_image, and does the actual loading of
images. This may be a load of an image, or perhaps the
compositing of images if fn is a tuple.
"""
# If fn is not a single filename but a tuple, we composite the
# elements of the tuple.
if isinstance(fn, tuple):
if not tuple:
raise Exception("Trying to create a composite image from an empty tuple.")
base = self.load_image(fn[0])
rv = pygame.Surface(base.get_size(), 0,
renpy.game.interface.display.sample_surface)
rv.blit(base, (0, 0))
for i in fn[1:]:
layer = self.load_image(i)
rv.blit(layer, (0, 0))
renpy.display.render.mutated_surface(rv)
return rv
im = pygame.image.load(renpy.loader.load(fn), fn)
if im.get_flags() & SRCALPHA:
im = im.convert_alpha()
else:
im = im.convert()
renpy.display.render.mutated_surface(im)
return im
# Forces an image load, regardless of if the cache is full or not.
def load_image(self, fn):
self.time_map[fn] = self.time
if fn in self.surface_map:
return self.surface_map[fn]
if fn in self.preloads:
self.preloads.remove(fn)
# iw, ih = im.get_size()
# surf = renpy.display.render.Render(iw, ih)
# surf.blit(im, (0, 0))
im = self.really_load_image(fn)
self.surface_map[fn] = im
if renpy.config.debug_image_cache:
print "Image cache:", self.surface_map.keys()
return im
# Queues an image to be preloaded if not already loaded and there's
# room in the cache for it.
def preload_image(self, fn):
self.time_map[fn] = self.time
if fn in self.surface_map:
return
if fn not in self.preloads:
self.preloads.append(fn)
# This tries to ensure that there are n empty spaces in the image
# cache. Returns the number of empty spaces that are actually in
# the image cache. (A number that may be negative.)
def clear_image_cache(self, n):
rv = renpy.config.image_cache_size - len(self.surface_map)
if rv >= n:
return rv
# The number of images to remove. (This is the amount we are over
# the cache limit + the number of images we have been requested to
# pull.)
num_to_remove = len(self.surface_map) - renpy.config.image_cache_size + n
time_files = [ (self.time_map[fn], fn) for fn in self.surface_map ]
time_files = [ (time, fn) for time, fn in time_files if time != self.time ]
time_files.sort()
time_files = time_files[:num_to_remove]
for time, fn in time_files:
del self.surface_map[fn]
del self.time_map[fn]
if renpy.config.debug_image_cache:
print "Image cache:", self.surface_map.keys()
rv = renpy.config.image_cache_size - len(self.surface_map)
return rv
def needs_preload(self):
"""
Returns True if calling preload would do anything.
"""
return self.preloads and True
def preload(self):
# If we have nothing to preload, bail early.
if not self.preloads:
return
# Try to clear up enough space for the preloads.
avail = self.clear_image_cache(len(self.preloads))
if avail < 0:
avail = 0
self.preloads = self.preloads[:avail]
# If no space is available, bail here.
if not self.preloads:
return
# Get the first thing to preload.
fn = self.preloads[0]
# Actually load the image.
try:
self.load_image(fn)
except:
if renpy.config.debug:
raise
cache = ImageCache()
class Image(renpy.display.core.Displayable):
"""
Returns a Displayable that is an image that is loaded from a file
on disk.
"""
def __init__(self, filename, style='image_placement', **properties):
"""
@param filename: The filename that the image is loaded
from. Many common file formats are supported.
If the filename is not a single string but instead a tuple of
strings, the image is considered to be"layered". In this case,
the image will be the size of the first image in the tuple, and
other images will be aligned with the upper-left corner of the
image.
"""
super(Image, self).__init__()
self.filename = filename
self.style = renpy.style.Style(style, properties)
def render(self, w, h, st):
im = cache.load_image(self.filename)
w, h = im.get_size()
rv = renpy.display.render.Render(w, h)
rv.blit(im, (0, 0))
return rv
def get_placement(self):
return self.style
def predict(self, callback):
callback(self.filename)
Image = renpy.display.im.image
class UncachedImage(renpy.display.core.Displayable):
"""
@@ -241,19 +46,18 @@ class UncachedImage(renpy.display.core.Displayable):
class ImageReference(renpy.display.core.Displayable):
"""
This is a reference to an image or animation that is kept
in exports.images.
@ivar name: The name of the image.
Not serialized:
@ivar target: If defined, a pointer to the thing that name resolves to.
ImageReference objects are used to reference images by their name,
which is a tuple of strings corresponding to the name used to define
the image in an image statment.
"""
nosave = [ 'target' ]
def __init__(self, name):
"""
@param name: A tuple of strings, the name of the image.
"""
super(ImageReference, self).__init__()
self.name = name
@@ -265,8 +69,7 @@ class ImageReference(renpy.display.core.Displayable):
parameters = [ ]
def error(msg):
self.target = renpy.display.text.Text(msg,
color=(255, 0, 0, 255))
self.target = renpy.display.text.Text(msg, color=(255, 0, 0, 255))
if renpy.config.debug:
raise Exception(msg)
@@ -323,7 +126,8 @@ class Solid(renpy.display.core.Displayable):
def __init__(self, color):
"""
@param color: An RGBA tuple, giving the color that the display will be filled with.
@param color: An RGBA tuple, giving the color that the display
will be filled with.
"""
super(Solid, self).__init__()
@@ -331,10 +135,11 @@ class Solid(renpy.display.core.Displayable):
def render(self, width, height, st):
rv = renpy.display.render.Render(width, height)
rv.fill(self.color)
si = renpy.display.im.SolidImage(self.color,
width,
height)
return rv
return render(si, width, height, st)
class Frame(renpy.display.core.Displayable):
"""
@@ -348,11 +153,10 @@ class Frame(renpy.display.core.Displayable):
the center of the image is scaled in both x and y directions.
"""
nosave = [ 'cache' ]
def __init__(self, filename, xborder, yborder):
def __init__(self, image, xborder, yborder):
"""
@param filename: The file that the original image will be read from.
@param image: The image (which may be a filename or image
object) that will be scaled.
@param xborder: The number of pixels in the x direction to use as
a border.
@@ -360,107 +164,32 @@ class Frame(renpy.display.core.Displayable):
@param yborder: The number of pixels in the y direction to use as
a border.
For better performance, have the image file share a dimension
For better performance, have the image share a dimension
length in common with the size the frame will be rendered
at. We detect this and avoid scaling if possible.
"""
super(Frame, self).__init__()
self.filename = filename
self.image = Image(image)
self.xborder = xborder
self.yborder = yborder
def render(self, width, height, st):
dest = renpy.display.render.Render(width, height)
dw, dh = width, height
source = cache.load_image(self.filename)
sw, sh = source.get_size()
fi = renpy.display.im.FrameImage(self.image,
self.xborder,
self.yborder,
width,
height)
def draw(x0, x1, y0, y1):
# Quick exit.
if x0 == x1 or y0 == y1:
return
# Compute the coordinates of the left, right, top, and
# bottom sides of the region, for both the source and
# destination surfaces.
# left side.
if x0 >= 0:
dx0 = x0
sx0 = x0
else:
dx0 = dw + x0
sx0 = sw + x0
# right side.
if x1 > 0:
dx1 = x1
sx1 = x1
else:
dx1 = dw + x1
sx1 = sw + x1
# top side.
if y0 >= 0:
dy0 = y0
sy0 = y0
else:
dy0 = dh + y0
sy0 = sh + y0
# bottom side
if y1 > 0:
dy1 = y1
sy1 = y1
else:
dy1 = dh + y1
sy1 = sh + y1
# Compute sizes.
srcsize = (sx1 - sx0, sy1 - sy0)
dstsize = (dx1 - dx0, dy1 - dy0)
# Get a subsurface.
surf = source.subsurface((sx0, sy0, srcsize[0], srcsize[1]))
# Scale if we have to.
if dstsize != srcsize:
surf = pygame.transform.scale(surf, dstsize)
# Blit.
dest.blit(surf, (dx0, dy0))
xb = self.xborder
yb = self.yborder
# Top row.
draw(0, xb, 0, yb)
draw(xb, -xb, 0, yb)
draw(-xb, 0, 0, yb)
# Middle row.
draw(0, xb, yb, -yb)
draw(xb, -xb, yb, -yb)
draw(-xb, 0, yb, -yb)
# Bottom row.
draw(0, xb, -yb, 0)
draw(xb, -xb, -yb, 0)
draw(-xb, 0, -yb, 0)
# And, finish up.
return dest
return render(fi, width, height, st)
def predict(self, callback):
callback(self.filename)
class Animation(renpy.display.core.Displayable):
self.image.predict(callback)
# This class has been replaced with a function in anim.
class OldAnimation(renpy.display.core.Displayable):
"""
A Displayable that draws an animation, which is a series of images
that are displayed with time delays between them.
@@ -476,7 +205,7 @@ class Animation(renpy.display.core.Displayable):
animation will restart after the final delay time.
"""
super(Animation, self).__init__()
super(Animation, self).__init__(style='image_placement')
self.images = [ ]
self.delays = [ ]
@@ -484,7 +213,7 @@ class Animation(renpy.display.core.Displayable):
for i, arg in enumerate(args):
if i % 2 == 0:
self.images.append(arg)
self.images.append(Image(arg))
else:
self.delays.append(arg)
@@ -499,7 +228,7 @@ class Animation(renpy.display.core.Displayable):
if t < delay:
renpy.display.render.redraw(self, delay - t)
im = cache.load_image(image)
im = render(image, width, height, st)
width, height = im.get_size()
rv = renpy.display.render.Render(width, height)
rv.blit(im, (0, 0))
@@ -511,105 +240,21 @@ class Animation(renpy.display.core.Displayable):
def predict(self, callback):
for i in self.images:
callback(i)
i.predict(callback)
def get_placement(self):
return renpy.game.style.image_placement
class ImageMap(renpy.display.core.Displayable):
"""
The displayable that implements renpy.imagemap.
"""
def __init__(self, ground, selected, hotspots, unselected=None,
style='imagemap', **properties):
super(ImageMap, self).__init__()
self.ground = ground
self.selected = selected
self.hotspots = hotspots
if not unselected:
self.unselected = self.ground
else:
self.unselected = unselected
self.active = None
self.last_active = None
self.style = renpy.style.Style(style, properties)
def get_placement(self):
return self.style
# This doesn't do anything quite yet.
def predict(self, callback):
callback(self.ground)
callback(self.selected)
callback(self.unselected)
def render(self, width, height, st):
ground = cache.load_image(self.ground)
selected = cache.load_image(self.selected)
unselected = cache.load_image(self.unselected)
width, height = ground.get_size()
rv = renpy.display.render.Render(width, height)
rv.blit(ground, (0, 0))
for i, hotspot in enumerate(self.hotspots):
x0, y0, x1, y1, result = hotspot
if i == self.active:
source = selected
else:
source = unselected
subsurface = source.subsurface((x0, y0, x1-x0, y1-y0))
renpy.display.render.mutated_surface(subsurface)
rv.blit(subsurface, (x0, y0))
return rv
def event(self, ev, x, y):
old_active = self.active
active = None
for i, (x0, y0, x1, y1, result) in enumerate(self.hotspots):
if x >= x0 and x <= x1 and y >= y0 and y <= y1:
active = i
break
# result stays set.
if old_active != active:
self.active = active
renpy.display.render.redraw(self, 0)
if active is not None:
renpy.display.audio.play(self.style.hover_sound)
if active is None:
return None
if renpy.display.behavior.map_event(ev, "imagemap_select"):
renpy.display.audio.play(self.style.activate_sound)
return result
return None
class ImageButton(renpy.display.behavior.Button):
"""
Used to implement the guts of an image button.
"""
def __init__(self, idle_image, hover_image, style='image_button',
def __init__(self, idle_image, hover_image,
style='image_button',
image_style='image_button_image',
clicked=None, hovered=None):
clicked=None, hovered=None, **properties):
self.idle_image = Image(idle_image, style=image_style)
self.idle_image.style.set_prefix("idle_")
@@ -619,18 +264,18 @@ class ImageButton(renpy.display.behavior.Button):
super(ImageButton, self).__init__(self.idle_image,
style=style,
clicked=clicked,
hovered=hovered)
hovered=hovered,
**properties)
def predict(self, callback):
self.idle_image.predict(callback)
self.hover_image.predict(callback)
def set_hover(self, hover):
super(ImageButton, self).set_hover(hover)
def focus(self, default=False):
self.child = self.hover_image
super(ImageButton, self).focus(default=default)
if hover:
self.child = self.hover_image
else:
self.child = self.idle_image
def unfocus(self):
self.child = self.idle_image
super(ImageButton, self).unfocus()
+477 -223
View File
@@ -27,9 +27,7 @@ class Null(renpy.display.core.Displayable):
"""
def __init__(self, width=0, height=0, style='default', **properties):
super(Null, self).__init__()
self.style = renpy.style.Style(style, properties)
super(Null, self).__init__(style=style, **properties)
self.width = width
self.height = height
@@ -37,7 +35,12 @@ class Null(renpy.display.core.Displayable):
return self.style
def render(self, width, height, st):
return renpy.display.render.Render(self.width, self.height)
rv = renpy.display.render.Render(self.width, self.height)
if self.focusable:
rv.add_focus(self, None, None, None, None, None)
return rv
class Container(renpy.display.core.Displayable):
@@ -60,16 +63,24 @@ class Container(renpy.display.core.Displayable):
"""
def __init__(self, *args):
def __init__(self, *args, **properties):
super(Container, self).__init__()
self.children = []
self.child = None
for i in args:
self.add(i)
super(Container, self).__init__(**properties)
def find_focusable(self, callback, focus_name):
super(Container, self).find_focusable(callback, focus_name)
for i in self.children:
i.find_focusable(callback, self.focus_name or focus_name)
def set_style_prefix(self, prefix):
super(Container, self).set_style_prefix(prefix)
@@ -132,6 +143,8 @@ class Container(renpy.display.core.Displayable):
def predict(self, callback):
super(Container, self).predict(callback)
for i in self.children:
i.predict(callback)
@@ -147,13 +160,21 @@ class Fixed(Container):
Fixed is used by the display core to render scene lists, and to
pass them off to transitions.
"""
def __init__(self, style='default', **properties):
super(Fixed, self).__init__()
self.style = renpy.style.Style(style, properties)
super(Fixed, self).__init__(style=style, **properties)
self.times = [ ]
# A map from layer name to the widget corresponding to
# that layer.
self.layers = None
# The scene list for this widget.
self.scene_list = [ ]
def add(self, widget, time=None):
super(Fixed, self).add(widget)
self.times.append(time)
@@ -162,6 +183,8 @@ class Fixed(Container):
for tag, time, d in l:
self.add(d, time)
self.scene_list.extend(l)
def get_widget_time_list(self):
return zip(self.children, self.times)
@@ -196,6 +219,48 @@ class Fixed(Container):
return rv
def LiveComposite(size, *args, **properties):
"""
This is similar to im.Composite, but can be used with displayables
instead of images. This allows it to be used to composite, for
example, an animation on top of the image.
This is less efficient then im.Composite, as it needs to draw all
of the displayables on the screen. On the other hand, it allows
displayables to change while they are on the screen, which is
necessary for animation.
This takes a variable number of arguments. The first argument is
size, which must be a tuple giving the width and height of the
composited widgets, for layout purposes.
It then takes an even number of further arguments. (For an odd
number of total arguments.) The second and other even numbered
arguments contain position tuples, while the third and further
odd-numbered arguments give displayables. A position argument
gives the position of the displayable immediately following it,
with the position expressed as a tuple giving an offset from the
upper-left corner of the LiveComposite. The displayables are
drawn in bottom-to-top order, with the last being closest to the
user.
"""
properties.setdefault('style', 'image_placement')
width, height = size
rv = Fixed(xmaximum=width, ymaximum=height, **properties)
if len(args) % 2 != 0:
raise Exception("LiveComposite requires an odd number of arguments.")
for pos, widget in zip(args[0::2], args[1::2]):
xpos, ypos = pos
rv.add(Position(renpy.display.im.image(widget, loose=True),
xpos=xpos, xanchor='left', ypos=ypos, yanchor='top'))
return rv
class Position(Container):
"""
Controls the placement of a displayable on the screen, using
@@ -214,9 +279,7 @@ class Position(Container):
child of this widget is placed.
"""
super(Position, self).__init__()
self.style = renpy.style.Style(style, properties)
super(Position, self).__init__(style=style, **properties)
self.add(child)
def render(self, width, height, st):
@@ -240,11 +303,14 @@ class Grid(Container):
"""
def __init__(self, cols, rows, padding=0,
transpose=False,
style='default', **properties):
"""
@param cols: The number of columns in this widget.
@params rows: The number of rows in this widget.
@params transpose: True if the grid should be transposed.
"""
super(Grid, self).__init__()
@@ -255,6 +321,10 @@ class Grid(Container):
self.rows = rows
self.padding = padding
self.transpose = transpose
def get_placement(self):
return self.style
def render(self, width, height, st):
@@ -266,7 +336,28 @@ class Grid(Container):
if len(self.children) != cols * rows:
raise Exception("Grid not completely full.")
renders = [ render(i, width, height, st) for i in self.children ]
# If necessary, transpose the grid (kinda hacky, but it works here.)
if self.transpose:
self.transpose = False
old_children = self.children[:]
for y in range(0, rows):
for x in range(0, cols):
self.children[x + y * cols] = old_children[ y + x * rows ]
# Now, start the actual rendering.
renwidth = width
renheight = height
if self.style.xfill:
renwidth = (width - (cols - 1) * padding) / cols
if self.style.yfill:
renheight = (height - (rows - 1) * padding) / rows
renders = [ render(i, renwidth, renheight, st) for i in self.children ]
self.sizes = [ i.get_size() for i in renders ]
cwidth = 0
@@ -277,10 +368,10 @@ class Grid(Container):
cheight = max(cheight, h)
if self.style.xfill:
cwidth = (width - (cols - 1) * padding) / cols
cwidth = renwidth
if self.style.yfill:
cheight = (height - (rows - 1) * padding) / rows
cheight = renheight
width = cwidth * cols + padding * (cols - 1)
height = cheight * rows + padding * (rows - 1)
@@ -303,139 +394,253 @@ class Grid(Container):
return rv
class HBox(Container):
"""
A box where things are aligned horizontally. The height of the box
is equal to the height of the largest thing in the box, or minheight,
whichever is larger. For each child that is smaller than the height of the
box, alignment * (empty space) is placed above the child, with the rest
of the empty space being placed below. (So 0.0 for top, 0.5 for center,
and 1.0 for bottom alignment.)
class MultiBox(Container):
If Full, the end result uses all of the width available to it.
def __init__(self, spacing=None, layout=None, style='default', **properties):
if spacing is not None:
properties['box_spacing'] = spacing
super(MultiBox, self).__init__(style=style, **properties)
self.default_layout = layout
"""
def __init__(self, padding=0, style='default', **properties):
super(HBox, self).__init__()
self.padding = padding
self.style = renpy.style.Style(style, properties)
def get_placement(self):
return self.style
def render(self, width, height, st):
self.offsets = [ ]
self.sizes = [ ]
layout = self.style.box_layout
padding = self.style.box_spacing
surfaces = [ ]
xoffsets = [ ]
if layout is None:
layout = self.default_layout
remwidth = width
xo = 0
if layout == "horizontal":
myheight = 0
# This is the horizontal path.
for i in self.children:
self.offsets = [ ]
self.sizes = [ ]
xoffsets.append(xo)
surf = render(i, remwidth, height, st)
surfaces = [ ]
xoffsets = [ ]
sw, sh = surf.get_size()
remwidth = width
xo = 0
remwidth -= sw
remwidth -= self.padding
myheight = 0
xo += sw + self.padding
for i in self.children:
myheight = max(sh, myheight)
xoffsets.append(xo)
surf = render(i, remwidth, height, st)
surfaces.append(surf)
self.sizes.append((sw, sh))
sw, sh = surf.get_size()
remwidth -= sw
remwidth -= padding
xo += sw + padding
myheight = max(sh, myheight)
surfaces.append(surf)
self.sizes.append((sw, sh))
width = xo - self.padding
width = xo - padding
rv = renpy.display.render.Render(width, myheight)
for surf, child, xo in zip(surfaces, self.children, xoffsets):
sw, sh = surf.get_size()
offset = child.place(rv, xo, 0, sw, myheight, surf)
self.offsets.append(offset)
return rv
rv = renpy.display.render.Render(width, myheight)
else:
self.offsets = [ ]
self.sizes = [ ]
for surf, child, xo in zip(surfaces, self.children, xoffsets):
sw, sh = surf.get_size()
surfaces = [ ]
yoffsets = [ ]
offset = child.place(rv, xo, 0, sw, myheight, surf)
self.offsets.append(offset)
remheight = height
yo = 0
return rv
mywidth = 0
for i in self.children:
yoffsets.append(yo)
surf = render(i, width, remheight, st)
sw, sh = surf.get_size()
remheight -= sh
remheight -= padding
yo += sh + padding
mywidth = max(sw, mywidth)
surfaces.append(surf)
self.sizes.append((sw, sh))
height = yo - padding
rv = renpy.display.render.Render(mywidth, height)
for surf, child, yo in zip(surfaces, self.children, yoffsets):
sw, sh = surf.get_size()
offset = child.place(rv, 0, yo, mywidth, sh, surf)
self.offsets.append(offset)
return rv
# class HBox(Container):
# """
# A box where things are aligned horizontally. The height of the box
# is equal to the height of the largest thing in the box, or minheight,
# whichever is larger. For each child that is smaller than the height of the
# box, alignment * (empty space) is placed above the child, with the rest
# of the empty space being placed below. (So 0.0 for top, 0.5 for center,
# and 1.0 for bottom alignment.)
# If Full, the end result uses all of the width available to it.
# """
# def __init__(self, padding=0, style='default', **properties):
# super(HBox, self).__init__()
# self.padding = padding
# self.style = renpy.style.Style(style, properties)
# def get_placement(self):
# return self.style
# def render(self, width, height, st):
# self.offsets = [ ]
# self.sizes = [ ]
# surfaces = [ ]
# xoffsets = [ ]
# remwidth = width
# xo = 0
# myheight = 0
# for i in self.children:
# xoffsets.append(xo)
# surf = render(i, remwidth, height, st)
# sw, sh = surf.get_size()
# remwidth -= sw
# remwidth -= self.padding
# xo += sw + self.padding
# myheight = max(sh, myheight)
# surfaces.append(surf)
# self.sizes.append((sw, sh))
# width = xo - self.padding
# rv = renpy.display.render.Render(width, myheight)
# for surf, child, xo in zip(surfaces, self.children, xoffsets):
# sw, sh = surf.get_size()
# offset = child.place(rv, xo, 0, sw, myheight, surf)
# self.offsets.append(offset)
# return rv
class VBox(Container):
"""
This is a box that lines displayables up vertically. The width of
the box is equal to the width of the widest displayable, or the
minwidth, whichever is smaller. Algnment * (empty space) of the
empty space is placed to the left of each displayable, with the
rest being placed to the right. Padding pixels of space are placed
between displayables.
# class VBox(Container):
# """
# This is a box that lines displayables up vertically. The width of
# the box is equal to the width of the widest displayable, or the
# minwidth, whichever is smaller. Algnment * (empty space) of the
# empty space is placed to the left of each displayable, with the
# rest being placed to the right. Padding pixels of space are placed
# between displayables.
If full is given, then the height of the returned surface is the
full height allocated.
"""
# If full is given, then the height of the returned surface is the
# full height allocated.
# """
def __init__(self, padding=0, style='default', **properties):
super(VBox, self).__init__()
# def __init__(self, padding=0, style='default', **properties):
# super(VBox, self).__init__()
self.padding = padding
self.style = renpy.style.Style(style, properties)
# self.padding = padding
# self.style = renpy.style.Style(style, properties)
def get_placement(self):
return self.style
# def get_placement(self):
# return self.style
def render(self, width, height, st):
# def render(self, width, height, st):
self.offsets = [ ]
self.sizes = [ ]
# self.offsets = [ ]
# self.sizes = [ ]
surfaces = [ ]
yoffsets = [ ]
# surfaces = [ ]
# yoffsets = [ ]
remheight = height
yo = 0
# remheight = height
# yo = 0
mywidth = 0
# mywidth = 0
for i in self.children:
# for i in self.children:
yoffsets.append(yo)
# yoffsets.append(yo)
surf = render(i, width, remheight, st)
# surf = render(i, width, remheight, st)
sw, sh = surf.get_size()
# sw, sh = surf.get_size()
remheight -= sh
remheight -= self.padding
# remheight -= sh
# remheight -= self.padding
yo += sh + self.padding
# yo += sh + self.padding
mywidth = max(sw, mywidth)
# mywidth = max(sw, mywidth)
surfaces.append(surf)
self.sizes.append((sw, sh))
# surfaces.append(surf)
# self.sizes.append((sw, sh))
height = yo - self.padding
# height = yo - self.padding
rv = renpy.display.render.Render(mywidth, height)
# rv = renpy.display.render.Render(mywidth, height)
for surf, child, yo in zip(surfaces, self.children, yoffsets):
# for surf, child, yo in zip(surfaces, self.children, yoffsets):
sw, sh = surf.get_size()
# sw, sh = surf.get_size()
offset = child.place(rv, 0, yo, mywidth, sh, surf)
# offset = child.place(rv, 0, yo, mywidth, sh, surf)
self.offsets.append(offset)
# self.offsets.append(offset)
return rv
# return rv
class Window(Container):
@@ -456,17 +661,15 @@ class Window(Container):
def __init__(self, child, style='window', **properties):
super(Window, self).__init__()
super(Window, self).__init__(style=style, **properties)
self.add(child)
self.style = renpy.style.Style(style, properties)
def get_placement(self):
return self.style
def render(self, width, height, st):
# save typing and screen space.
# save some typing.
style = self.style
xminimum = scale(style.xminimum, width)
@@ -536,163 +739,214 @@ class Window(Container):
return rv
class Pan(Container):
class Motion(Container):
"""
This is used to pan over a child displayable, which is almost
always an image. It works by interpolating the placement of the
upper-left corner of the image, over time. It's only really
suitable for use with images that are larger than the screen, as
we don't do any cropping on the image.
This is used to move a child displayable around the screen. It
works by supplying a time value to a user-supplied function,
which is in turn expected to return a pair giving the x and y
location of the upper-left-hand corner of the child, or a
4-tuple giving that and the xanchor and yanchor of the child.
The time value is a floating point number that ranges from 0 to
1. If repeat is True, then the motion repeats every period
sections. (Otherwise, it stops.) If bounce is true, the
time value varies from 0 to 1 to 0 again.
The function supplied needs to be pickleable, which means it needs
to be defined as a name in an init block. It cannot be a lambda or
anonymous inner function. If you can get away with using Pan or
Move, use them instead.
Please note that floats and ints are interpreted as for xpos and
ypos, with floats being considered fractions of the screen.
"""
def __init__(self, startpos, endpos, time, child,
style='image_placement', **properties):
def __init__(self, function, period, child=None, new_widget=None, old_widget=None, repeat=False, bounce=False, delay=None, style='default', **properties):
"""
@param child: The child displayable.
@param startpos: The initial coordinates of the upper-left
corner of the screen, relative to the image.
@param new_widget: If child is None, it is set to new_widget,
so that we can speak the transition protocol.
@param endpos: The coordinates of the upper-left corner of the
screen, relative to the image, after time has elapsed.
@param old_widget: Ignored, for compatibility with the transition protocol.
@param time: The time it takes to pan from startpos to endpos.
@param function: A function that takes a floating point value and returns
an xpos, ypos tuple.
@param period: The amount of time it takes to go through one cycle, in seconds.
@param repeat: Should we repeat after a period is up?
@param bounce: Should we bounce?
@param delay: How long this motion should take. If repeat is None, defaults to period.
This can also be used as a transition. When used as a
transition, the motion is applied to the new_widget for delay
seconds.
"""
super(Pan, self).__init__()
self.add(child)
if child is None:
child = new_widget
self.startpos = startpos
self.endpos = endpos
self.time = time
self.style = renpy.style.Style(style, properties)
if delay is None and not repeat:
delay = period
super(Motion, self).__init__(style=style, **properties)
self.child = child
self.children = [ child ]
self.function = function
self.period = period
self.repeat = repeat
self.bounce = bounce
self.delay = delay
def get_placement(self):
return self.style
def render(self, width, height, st):
surf = render(self.child, width, height, st)
self.sizes = [ surf.get_size() ]
x0, y0 = self.startpos
x1, y1 = self.endpos
if self.time > 0:
tfrac = (st / self.time)
else:
tfrac = 1.0
if tfrac > 1.0:
tfrac = 1.0
xo = int(x0 * (1.0 - tfrac) + x1 * tfrac)
yo = int(y0 * (1.0 - tfrac) + y1 * tfrac)
self.offsets = [ (-xo, -yo) ]
rv = renpy.display.render.Render(width, height)
# print surf
subsurf = surf.subsurface((xo, yo, width, height))
rv.blit(subsurf, (0, 0))
# rv.blit(surf, (-xo, -yo))
if st < self.time:
if self.delay and st >= self.delay:
st = self.delay
if self.repeat:
st = st % self.period
elif self.repeat:
st = st % self.period
renpy.display.render.redraw(self, 0)
return rv
class Move(Container):
"""
This moves a child relative to the thing containing it. This
motion is done by manipulating the xpos and ypos properties in a
placement style.
"""
def __init__(self, startpos, endpos, time, child,
style='default', **properties):
super(Move, self).__init__()
self.add(child)
self.startpos = startpos
self.endpos = endpos
self.time = time
self.st = 0.0
self.style = renpy.style.Style(style, properties)
def get_placement(self):
st = self.st
x0, y0 = self.startpos
x1, y1 = self.endpos
if self.time > 0:
tfrac = (st / self.time)
else:
tfrac = 1.0
if st > self.period:
st = self.period
else:
renpy.display.render.redraw(self, 0)
st /= self.period
if tfrac > 1.0:
tfrac = 1.0
if self.bounce:
st = st * 2
if st > 1.0:
st = 2.0 - st
xo = x0 * (1.0 - tfrac) + x1 * tfrac
yo = y0 * (1.0 - tfrac) + y1 * tfrac
res = self.function(st)
if isinstance(x1, int):
xo = int(xo)
if len(res) == 2:
self.style.xpos, self.style.ypos = res
else:
self.style.xpos, self.style.ypos, self.style.xanchor, self.style.yanchor = res
if isinstance(y1, int):
yo = int(yo)
child = render(self.child, width, height, st)
cw, ch = child.get_size()
self.style.xpos = xo
self.style.ypos = yo
rv = renpy.display.render.Render(cw, ch)
rv.blit(child, (0, 0))
return self.style
def render(self, width, height, st):
self.st = st
rv = render(self.child, width, height, st)
self.sizes = [ rv.get_size() ]
self.sizes = [ child.get_size() ]
self.offsets = [ (0, 0) ]
if st < self.time:
renpy.display.render.redraw(self, 0)
return rv
class Interpolate(object):
anchors = {
'top' : 0.0,
'center' : 0.5,
'bottom' : 1.0,
'left' : 0.0,
'right' : 1.0,
}
def __init__(self, start, end):
if len(start) != len(end):
raise Exception("The start and end must have the same number of arguments.")
self.start = [ self.anchors.get(i, i) for i in start ]
self.end = [ self.anchors.get(i, i) for i in end ]
def __call__(self, t):
def interp(a, b):
rv = (1.0 - t) * a + t * b
if isinstance(a, int) and isinstance(b, int):
return int(rv)
else:
return rv
return [ interp(a, b) for a, b in zip(self.start, self.end) ]
def Pan(startpos, endpos, time, child=None, repeat=False, bounce=False,
style='default', **properties):
"""
This is used to pan over a child displayable, which is almost
always an image. It works by interpolating the placement of the
upper-left corner of the screen, over time. It's only really
suitable for use with images that are larger than the screen,
and we don't do any cropping on the image.
@param startpos: The initial coordinates of the upper-left
corner of the screen, relative to the image.
@param endpos: The coordinates of the upper-left corner of the
screen, relative to the image, after time has elapsed.
class Sizer(Container):
"""
This is a widget that can change the size allocated to the widget that
it contains. Please note that it can only shrink the widget, and that
not all widgets respond well to having their areas shrunk. (For example,
this has no effect on an image.)
@param time: The time it takes to pan from startpos to endpos.
@param child: The child displayable.
@param repeat: True if we should repeat this forever.
@param bounce: True if we should bounce from the start to the end
to the start.
This can be used as a transition. See Motion for details.
"""
def __init__(self, maxwidth, maxheight, child,
style='default', **properties):
x0, y0 = startpos
x1, y1 = endpos
return Motion(Interpolate((-x0, -y0), (-x1, -y1)),
time,
child,
repeat=repeat,
bounce=bounce,
style=style,
**properties)
super(Sizer, self).__init__()
self.add(child)
def Move(startpos, endpos, time, child=None, repeat=False, bounce=False,
style='default', **properties):
"""
This is used to pan over a child displayable relative to
the containing area. It works by interpolating the placement of the
the child, over time.
self.maxwidth = maxwidth
self.maxheight = maxheight
@param startpos: The initial coordinates of the child
relative to the containing area.
self.style = renpy.style.Style(style, properties)
@param endpos: The coordinates of the child at the end of the
move.
@param time: The time it takes to move from startpos to endpos.
def render(self, width, height, st):
@param child: The child displayable.
if self.maxwidth:
width = min(width, self.maxwidth)
@param repeat: True if we should repeat this forever.
@param bounce: True if we should bounce from the start to the end
to the start.
This can be used as a transition. See Motion for details.
"""
return Motion(Interpolate(startpos, endpos),
time,
child,
repeat=repeat,
bounce=bounce,
style=style,
**properties)
if self.maxheight:
height = min(height, self.maxheight)
return super(Sizer, self).render(width, height, st)
+204
View File
@@ -0,0 +1,204 @@
# This file mediates access to the _renpy module, which is a C module that
# allows us to enhance the feature set of pygame in a renpy specific way.
import pygame
from pygame.constants import *
import sys
try:
import _renpy
version = _renpy.version()
except:
# If for any reason we can't import the module, we have a version
# number of 0.
print >>sys.stderr, "The _renpy module was not found. Please read module/README.txt for"
print >>sys.stderr, "more information."
version = 0
def convert_and_call(function, src, dst, *args):
"""
This calls the function with the source and destination
surface. The surfaces must have the same alpha.
If the surfaces are not 24 or 32 bits per pixel, or don't have the
same format, they are converted and then converted back.
"""
if (dst.get_masks()[3] != 0) != (src.get_masks()[3] != 0):
raise Exception("Surface alphas do not match.")
dstsize = dst.get_bitsize()
if dst.get_bitsize() in (24, 32):
target = dst
else:
if dst.get_masks()[3]:
target = pygame.Surface(dst.get_size(), SRCALPHA, 32)
else:
target = pygame.Surface(dst.get_size(), 0, 24)
if src.get_bitsize() == target.get_bitsize():
source = src
else:
source = src.convert(target)
function(source, target, *args)
if target is not dst:
dst.blit(target, (0, 0))
if version >= 4008002:
can_pixellate = True
def pixellate(src, dst, avgwidth, avgheight, outwidth, outheight):
"""
This pixellates the source surface. First, every pixel in the
source surface is projected onto a virtual surface, such that
the average value of every avgwidth x avgheight pixels becomes
one virtual pixel. It then gets projected back onto the
destination surface at a ratio of one virtual pixel to every
outwidth x outheight destination pixels.
If either src or dst is not a 24 or 32 bit surface, they are
converted... but that may be a significant performance hit.
The two surfaces must either have the same alpha or no alpha.
"""
convert_and_call(_renpy.pixellate,
src, dst,
avgwidth, avgheight,
outwidth, outheight)
def scale(s, size):
"""
Scales down the supplied pygame surface by the given X and Y
factors.
Always works, but may not be high quality.
"""
width, height = s.get_size()
dx, dy = size
if s.get_flags() & SRCALPHA:
d = pygame.Surface((dx, dy), SRCALPHA)
else:
d = pygame.Surface((dx, dy), 0)
pixellate(s, d, width / dx, height / dy, 1, 1)
return d
else:
can_pixellate = False
def scale(s, size):
return pygame.transform.scale(s, size)
def slow_endian_order(shifts, masks, r, g, b, a):
has_alpha = masks[3]
if not has_alpha:
l = zip(shifts, (r, g, b))
l.sort()
if sys.byteorder == 'big':
l.reverse()
return [ j for i, j in l] + [ a ]
l = zip(shifts, (r, g, b, a))
l.sort()
if sys.byteorder == 'big':
l.reverse()
return [ j for i, j in l]
endian_order_cache = { }
def endian_order(src, r, g, b, a):
"""
Returns the four arguments, in endian-order.
"""
shifts = src.get_shifts()
try:
func = endian_order_cache[shifts]
except KeyError:
masks = src.get_masks()
order = slow_endian_order(shifts, masks, 'r', 'g', 'b', 'a')
func = eval( "lambda r, g, b, a : (" + ", ".join(order) + ")")
endian_order_cache[shifts] = func
return func(r, g, b, a)
if version >= 4008005:
can_map = True
def map(src, dst, rmap, gmap, bmap, amap):
"""
This maps the colors between two surfaces. The various map
parameters must be 256 character long strings, with the value
of a character at a given offset being what a particular pixel
component value is mapped to.
"""
convert_and_call(_renpy.map,
src, dst,
*endian_order(dst, rmap, gmap, bmap, amap))
else:
can_map = False
if version >= 4008007:
can_munge = True
def alpha_munge(src, dst, amap):
"""
This samples the red channel from src, maps it through amap, and
place it into the alpha channel of amap.
"""
if src.get_size() != dst.get_size():
return
red = list(endian_order(src, 1, 2, 3, 4)).index(1)
alpha = list(endian_order(dst, 1, 2, 3, 4)).index(4)
_renpy.alpha_munge(src, dst, red, alpha, amap)
else:
can_munge = False
def alpha_munge(src, dst, amap):
return
+56
View File
@@ -0,0 +1,56 @@
# Pre-splash code. The goal of this code is to try to get a pre-splash
# screen up as soon as possible, to let the user know something is
# going on.
import os
import os.path
import pygame.display
import pygame.constants
# The directory from which presplash images are loaded.
gamedir = None
# Are we actually using presplash?
active = False
# Called at the start of the presplash process. This determines if
# we're even doing presplash, and if so what will be shown to the
# user.
#
# As this is called before any of the renpy modules are even loaded,
# we need to be careful.
def start(_gamedir):
global gamedir
global active
gamedir = _gamedir
if not os.path.exists(gamedir + "/presplash.png"):
return
active = True
os.environ['SDL_VIDEO_CENTERED'] = "1"
pygame.display.init()
img = pygame.image.load(gamedir + "/presplash.png")
screen = pygame.display.set_mode(img.get_size(), pygame.constants.NOFRAME)
screen.blit(img, (0, 0))
pygame.display.update()
# Called just before we initialize the display for real, to
# hide the splash, and terminate window centering.
def end():
global active
if not active:
return
active = False
del os.environ['SDL_VIDEO_CENTERED']
pygame.display.quit()
+83 -7
View File
@@ -62,6 +62,12 @@ def render(widget, width, height, st):
Renders a widget on the screen.
"""
if widget.style.xmaximum is not None:
width = min(widget.style.xmaximum, width)
if widget.style.ymaximum is not None:
height = min(widget.style.ymaximum, height)
if (widget, width, height) in old_renders:
rv = old_renders[widget, width, height]
@@ -72,7 +78,6 @@ def render(widget, width, height, st):
return rv
rv = widget.render(width, height, st)
rv.render_of.append((widget, width, height))
@@ -132,7 +137,6 @@ def render_screen(widget, width, height, st):
global new_renders
global mutated_surfaces
redraw_queue = [ ]
mutated_surfaces = { }
rv = render(widget, width, height, st)
@@ -144,11 +148,23 @@ def render_screen(widget, width, height, st):
dead_render_set = old_render_set - new_render_set
for r in dead_render_set:
render_of = r.render_of
r.kill()
assert not rv.dead, render_of
old_renders.update(new_renders)
new_renders.clear()
# Figure out which widgets are still alive.
live_widgets = sets.Set()
for widget, height, width in old_renders:
live_widgets.add(widget)
# Filter dead widgets from the redraw queue.
redraw_queue = [ (when, widget) for when, widget in redraw_queue if
widget in live_widgets ]
return rv
old_blits = [ ]
@@ -295,9 +311,11 @@ class Render(object):
# entries in old_renders that this render is in.
self.render_of = [ ]
# The width and height of this render.
self.width = width
self.height = height
# The parents of this render.
self.parents = [ ]
self.blittables = [ ]
@@ -310,6 +328,10 @@ class Render(object):
self.subsurfaces = { }
# The list of focusable widgets collected from this render
# and the children of this render. A list of (widget, arg, x, y, w, h,)
self.focuses = [ ]
# def __del__(self):
# Render.renders -= 1
# print "Render del", Render.renders, Render.liverenders, self
@@ -329,6 +351,9 @@ class Render(object):
i.keep_alive()
for i in self.depends:
i.keep_alive()
def kill(self):
"""
Calling this marks the render and all of its parents dead. It also
@@ -371,8 +396,9 @@ class Render(object):
# Removes cycles.
self.render_of = [ ]
self.focuses = [ ]
def blit(self, source, (x, y)):
def blit(self, source, (xo, yo), focus=True):
"""
Adds the source to the list of things that need to be blitted
to the screen. The source should be either a pygame.Surface,
@@ -385,8 +411,18 @@ class Render(object):
source.parents.append(self)
self.children.append(source)
self.blittables.append((x, y, source))
if focus and xo == 0 and yo == 0:
self.focuses.extend(source.focuses)
elif focus:
for widget, arg, x, y, w, h in source.focuses:
if x is not None:
x += xo
y += yo
self.add_focus(widget, arg, x, y, w, h)
self.blittables.append((xo, yo, source))
def blit_to(self, dest, x, y):
@@ -459,7 +495,7 @@ class Render(object):
return rv
def subsurface(self, pos):
def subsurface(self, pos, focus=False):
"""
Returns a subsurface of this render.
"""
@@ -477,6 +513,26 @@ class Render(object):
rv = Render(width, height)
rv.children.append(self)
if focus:
for fwidget, farg, fx, fy, fw, fh in self.focuses:
if fx is not None:
fx -= x
fx = max(fx, 0)
fy -= y
fy = max(fy, 0)
fw -= x
fw = min(fw, width)
fh -= y
fh = min(fh, height)
if fw <= 0 or fh <= 0:
continue
rv.add_focus(fwidget, farg, fx, fy, fw, fh)
for xo, yo, source in self.blittables:
# ulx, uly -- the coordinates of the upper-left hand corner of
@@ -539,4 +595,24 @@ class Render(object):
self.depends.append(child)
child.parents.append(self)
def add_focus(self, widget, arg=None, x=0, y=0, w=None, h=None):
"""
This is called to indicate a region of the screen that can be
focused.
@param widget: The widget that will be focused.
@param arg: A focus argument, which can be checked by the widget.
The rest of the parameters are a rectangle giving the portion of
this region corresponding to the focus. If they are all None, than
this focus is assumed to be the singular full-screen focus.
"""
if x is not None:
if w is None:
w = self.width
if h is None:
h = self.height
self.focuses.append(renpy.display.focus.Focus(widget, arg, x, y, w, h))
+464 -73
View File
@@ -11,6 +11,9 @@ _font_cache = { }
def get_font(fn, size, bold=False, italics=False, underline=False):
from renpy.loader import transfn
if (fn, bold, italics) in renpy.config.font_replacement_map:
fn, bold, italics = renpy.config.font_replacement_map[fn, bold, italics]
if (fn, size, bold, italics, underline) in _font_cache:
return _font_cache[(fn, size, bold, italics, underline)]
@@ -19,7 +22,28 @@ def get_font(fn, size, bold=False, italics=False, underline=False):
rv.set_bold(bold)
rv.set_italic(italics)
except:
rv = pygame.font.SysFont(fn, size, bold, italics)
# Let's try to find the font on our own.
fonts = [ i.strip().lower() for i in fn.split(",") ]
pygame.sysfont.initsysfonts()
rv = None
for k, v in pygame.sysfont.Sysfonts.iteritems():
for flags, ffn in v.iteritems():
for i in fonts:
if ffn.lower().endswith(i):
rv = pygame.font.Font(ffn, size)
rv.set_bold(bold)
rv.set_italic(italics)
break
if rv:
break
if rv:
break
else:
# Let pygame try to find the font for us.
rv = pygame.font.SysFont(fn, size, bold, italics)
rv.set_underline(underline)
@@ -89,28 +113,165 @@ class TextStyle(object):
# 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):
def render(self, text, antialias, color, use_colors, time):
if use_colors and self.color:
color = self.color
r, g, b, a = color
color = (r, g, b, 255)
font = self.get_font()
rv = font.render(text, antialias, color)
surf = font.render(text, antialias, color)
if a != 255 and renpy.display.module.can_map:
if not surf.get_masks()[3]:
surf = surf.convert_alpha()
rv = pygame.Surface(surf.get_size(), surf.get_flags(), surf)
alpha = renpy.display.im.ramp(0, a)
identity = renpy.display.im.identity
renpy.display.module.map(surf, rv, identity, identity, identity, alpha)
else:
rv = surf
renpy.display.render.mutated_surface(rv)
return rv
return rv, rv.get_size()
def length(self, text):
return len(text)
class WidgetStyle(object):
"""
Represents the style of a widget.
"""
def __init__(self, ts, widget, width, time):
self.height = ts.sizes(" ")[1]
self.ascent = ts.get_ascent()
# The widget we will render.
self.widget = widget
self.owidth = width
surf = renpy.display.render.render(widget, self.height, width, time)
self.width, _ = surf.get_size()
def get_ascent(self):
return self.ascent
def sizes(self, widget):
return self.width, self.height
def render(self, widget, antialias, color, foreground, time):
# If in the foreground
if foreground:
return renpy.display.render.render(widget, self.owidth, self.height, time), (self.width, self.height)
else:
return None, (self.width, self.height)
def length(self, text):
return 1
text_regexp = re.compile(ur"""(?x)
(?P<space>[ \u200b])
| \{(?P<tag>[^{}]+)\}
| (?P<untag>\{\{)
| (?P<newline>\n)
| (?P<word>[^ \n\{]+)
""")
def text_tokenizer(s, style):
"""
This functions is used to tokenize text. It's called when laying
out a Text widget, and is given the string that is the text of the
widget, and the style associated with the widget.
It's expected to yield some number of pairs. In each pair, the
first element is the kind of token found, and the second element
is the text corresponding to that token. The following token
types are defined:
"newline" -- A newline, which when encountered starts a new line.
"word" -- A word of text. A line will never be broken inside of
a word.
"space" -- A space. Spaces are always placed on the current line,
and will never be placed as the start of a line.
"tag" -- A text tag. If encountered, the second element should be
the name of the tag, without any enclosing braces.
"""
for m in text_regexp.finditer(s):
if m.group('space'):
yield 'space', m.group('space')
elif m.group('word'):
yield 'word', m.group('word')
elif m.group('tag'):
yield 'tag', m.group('tag')
elif m.group('untag'):
yield 'word', '{'
elif m.group('newline'):
yield 'newline', m.group('newline')
def input_tokenizer(l, style, pauses=None):
"""
This tokenizes the input into a list of lists of tokens, where
each token is a pair giving the type of token and the text of the
token.
"""
if isinstance(l, basestring):
l = [ l ]
rv = [ ]
for s in l:
if isinstance(s, basestring):
sl = [ ]
for type, text in renpy.config.text_tokenizer(s, style):
if type == "tag" and text == "p":
sl.append(("tag", "w"))
sl.append(("newline", "\n"))
else:
sl.append((type, text))
rv.append(sl)
elif isinstance(s, renpy.display.core.Displayable):
rv.append([ ("widget", s) ])
else:
raise Exception("Couldn't figure out how to tokenize " + repr(s))
return rv
class Text(renpy.display.core.Displayable):
"""
A displayable that can format and display text on the screen.
"""
nosave = [ 'laidout', 'laidout_lineheights', 'laidout_width', 'laidout_height', 'width' ]
nosave = [ 'laidout', 'laidout_lineheights', 'laidout_linewidths',
'laidout_width', 'laidout_height', 'laidout_start',
'laidout_length', 'width', 'tokens' ]
def after_setstate(self):
self.laidout = None
self._update()
def __init__(self, text, slow=False, style='default', **properties):
def __init__(self, text, slow=False, slow_done=None,
slow_speed=None, slow_start=0, slow_abortable=False,
pause=None, style='default', **properties):
"""
@param text: The text that will be displayed on the screen.
@@ -119,13 +280,34 @@ class Text(renpy.display.core.Displayable):
@param style: A style that will be applied to the text.
@param properties: Additional properties that are applied to the text.
@param pause: If not None, then we display up to the pauseth pause (0-numbered.)
@param slow_done: A callback that occurs when slow text is done.
@param slow_speed: The speed of slow text. If none, it's taken from
the preferences.
@param slow_offset: The offset into the text to start the slow text.
@param slow_abortable: If True, clicking aborts the slow text.
"""
super(Text, self).__init__()
self.text = text
self.text = text
self.style = renpy.style.Style(style, properties)
self.pause = pause
self.keep_pausing = False
self._update(redraw=False)
self.slow = slow
self.slow_done = slow_done
self.slow_start = slow_start
self.slow_speed = slow_speed
self.laidout = None
@@ -147,15 +329,40 @@ class Text(renpy.display.core.Displayable):
self.style = style
self._update()
def _update(self):
def _update(self, redraw=True):
"""
This is called after this widget has been updated by
set_text or set_style.
"""
self.laidout = None
renpy.display.render.redraw(self, 0)
if self.text:
text = self.text
else:
text = " "
self.tokens = input_tokenizer(text, self.style)
if self.pause is not None:
pause = self.pause
l = [ ]
for i in self.tokens[0]:
l.append(i)
if i == ("tag", "w"):
if pause == 0:
self.keep_pausing |= True
break
else:
pause -= 1
self.tokens[0] = l
if redraw:
renpy.display.render.redraw(self, 0)
def event(self, ev, x, y):
"""
@@ -165,12 +372,15 @@ class Text(renpy.display.core.Displayable):
if not self.slow:
return None
if not self.slow_abortable:
return None
if renpy.display.behavior.map_event(ev, "dismiss"):
self.slow = False
raise renpy.display.core.IgnoreEvent()
def layout(self, width):
def layout(self, width, time):
"""
This lays out the text of this widget. It sets self.laidout,
self.laidout_lineheights, self.laidout_width, and
@@ -211,9 +421,18 @@ class Text(renpy.display.core.Displayable):
# The width of the current line.
linewidth = 0
# A list of the same.
linewidths = [ ]
# The maximum linewidth.
maxwidth = 0
# The length of the laidout text.
laidout_length = 0
# Where we should start the slow effect from on the laidout text.
laidout_start = 0
# The current text.
cur = ""
@@ -224,10 +443,22 @@ class Text(renpy.display.core.Displayable):
# cur.
remwidth = width - indent()
for i in re.split(r'( |\{[^{]+\}|\{\{|\n)', self.text):
if not self.text:
text = " "
else:
text = self.text
# for i in re.split(r'( |\{[^{}]+\}|\{\{|\n)', text):
tokens = [ ]
for l in self.tokens:
tokens.extend(l)
for kind, i in tokens:
# Newline.
if i == "\n":
if kind == "newline":
if cur:
line.append((TextStyle(tsl[-1]), cur))
maxwidth = max(maxwidth, linewidth + curwidth)
@@ -235,6 +466,7 @@ class Text(renpy.display.core.Displayable):
lines.append(line)
lineheights.append(lineheight)
linewidths.append(curwidth)
line = [ ]
linewidth = 0
@@ -243,14 +475,10 @@ class Text(renpy.display.core.Displayable):
continue
elif i == "{{":
i = "{"
# We want to render this like a word, so no continue.
elif i.startswith("{"):
elif kind == "tag":
# Are we closing a tag?
if i.startswith("{/"):
if i.startswith("/"):
if cur:
line.append((TextStyle(tsl[-1]), cur))
cur = ""
@@ -277,23 +505,41 @@ class Text(renpy.display.core.Displayable):
tsl.append(TextStyle(tsl[-1]))
if i == "{b}":
if i == "w":
# Automatically closes.
tsl.pop()
elif i == "fast":
# Automatically closes.
tsl.pop()
laidout_start = laidout_length
elif i == "b":
tsl[-1].bold = True
elif i == "{i}":
elif i == "i":
tsl[-1].italic = True
elif i == "{u}":
elif i == "u":
tsl[-1].underline = True
elif i == "{plain}":
elif i == "plain":
tsl[-1].bold = False
tsl[-1].italic = False
tsl[-1].underline = False
elif i.startswith("{size"):
elif i.startswith("font"):
m = re.match(r'font=(.*)', i)
m = re.match(r'\{size=(\+|-|)(\d+)\}', i)
if not m:
raise Exception('Font tag %s could not be parsed.' % i)
tsl[-1].font = m.group(1)
elif i.startswith("size"):
m = re.match(r'size=(\+|-|)(\d+)', i)
if not m:
raise Exception('Size tag %s could not be parsed.' % i)
@@ -305,21 +551,36 @@ class Text(renpy.display.core.Displayable):
else:
tsl[-1].size = int(m.group(2))
elif i.startswith("{color"):
elif i.startswith("color"):
m = re.match(r'\{color=(\#?[a-fA-F0-9]+)\}', i)
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))
elif i.startswith("image"):
m = re.match(r'image=(.*)', i)
if not m:
raise Exception('Image tag %s could not be parsed.' % i)
kind = "widget"
i = renpy.display.im.image(m.group(1))
# The tag automatically closes.
tsl.pop()
else:
raise Exception("Text tag %s was not recognized. Case and spacing matter here.")
raise Exception("Text tag %s was not recognized. Case and spacing matter here." % i)
continue
# Since the kind can change.
if kind == "tag":
continue
elif i == ' ':
elif kind == "space":
# 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.
@@ -327,37 +588,93 @@ class Text(renpy.display.core.Displayable):
cur += i
curwidth, lh = tsl[-1].sizes(cur)
lineheight = max(lh, lineheight)
laidout_length += len(i)
continue
# If we made it here, then we have normal text.
elif kind == "word":
# If we made it here, then we have normal text.
laidout_length += len(i)
# 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?
oldcurwidth = curwidth
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)
linewidths.append(width + oldcurwidth - remwidth)
cur = i
curwidth, lineheight = tsl[-1].sizes(cur)
remwidth = width - indent()
linewidth = 0
else:
cur = cur + i
lineheight = max(lh, lineheight)
# 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
elif kind == "widget":
pass
else:
cur = cur + i
lineheight = max(lh, lineheight)
raise Exception("Unknown text token kind %s." % kind)
if kind == "widget":
laidout_length += 1
# Here, we have a widget that we can render.
# Close off an existing line, if it's open.
if cur:
line.append((TextStyle(tsl[-1]), cur))
cur = ""
remwidth -= curwidth
linewidth += curwidth
curwidth = 0
wstyle = WidgetStyle(tsl[-1], i, width, time)
wwidth, wheight = wstyle.sizes(i)
# If we're bigger then the remaining width on a non-empty line.
if line and wwidth > remwidth:
lines.append(line)
maxwidth = max(maxwidth, linewidth)
lineheights.append(lineheight)
linewidths.append(width - remwidth)
line = [ (wstyle, i) ]
remwidth = width - indent() - wwidth
linewidth = wwidth
lineheight = wheight
else:
line.append((wstyle, i))
linewidth -= wwidth
lineheight = max(lineheight, wheight)
# We're done. Let's close up.
@@ -371,14 +688,64 @@ class Text(renpy.display.core.Displayable):
if line:
lines.append(line)
lineheights.append(lineheight)
linewidths.append(linewidth + curwidth)
laidout_length += len(lines)
self.laidout = lines
self.laidout_lineheights = lineheights
self.laidout_width = max(maxwidth, self.style.minwidth)
self.laidout_linewidths = linewidths
self.laidout_width = max(max(linewidths), self.style.minwidth)
self.laidout_height = sum(lineheights) + len(lineheights) * self.style.line_spacing
self.laidout_length = laidout_length
self.laidout_start = laidout_start
def render_pass(self, r, xo, yo, color, user_colors, length):
def get_simple_length(self):
"""
Returns a simple length of the text in the first segment of
the tokens. Doesn't use the same algorithm as get_laidout_length,
so isn't suitable for slow_start.
"""
rv = 0
for tl in self.tokens:
for type, text in tl:
if type == "newline":
rv += len(text)
elif type == "word":
rv += len(text)
elif type == "space":
rv += len(text)
elif type == "widget":
rv += 1
return rv
def get_keep_pausing(self):
"""
If true, we have text beyond the pause number indicated.
"""
return self.keep_pausing
def get_laidout_length(self):
"""
The (reasonably arbitrary) length this text field was laidout
to. This can only be called after the text field was drawn
(that is, after it has been on the screen for an interaction
with the user. Otherwise, it returns sys.maxint.
"""
if not self.laidout:
import sys
return sys.maxint
return self.laidout_length
def render_pass(self, r, xo, yo, color, user_colors, length, time):
"""
Renders the text to r at xo, yo. Color is the base color,
and user_colors controls if the user can override those colors.
@@ -393,8 +760,8 @@ class Text(renpy.display.core.Displayable):
antialias = self.style.antialias
line_spacing = self.style.line_spacing
for line, line_height in zip(self.laidout, self.laidout_lineheights):
x = xo + indent
for line, line_height, line_width in zip(self.laidout, self.laidout_lineheights, self.laidout_linewidths):
x = xo + indent + self.style.textalign * (self.laidout_width - line_width)
indent = rest_indent
max_ascent = 0
@@ -404,31 +771,32 @@ class Text(renpy.display.core.Displayable):
for ts, text in line:
length -= len(text)
length -= ts.length(text)
if length < 0:
text = text[:length]
surf = ts.render(text, antialias, color, user_colors)
sw, sh = surf.get_size()
surf, (sw, sh) = ts.render(text, antialias, color, user_colors, time)
# sw, sh = surf.get_size()
r.blit(surf, (x, y + max_ascent - ts.get_ascent()))
if surf:
r.blit(surf, (x, y + max_ascent - ts.get_ascent()))
x = x + sw
if length <= 0:
return False
length -= 1
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
speed = self.slow_speed or renpy.game.preferences.text_cps
if self.style.drop_shadow:
dsxo, dsyo = self.style.drop_shadow
@@ -449,15 +817,37 @@ class Text(renpy.display.core.Displayable):
dsyo = 0
else:
yo = 0
else:
absxo = 0
absyo = 0
dsxo = 0
dsyo = 0
xo = 0
yo = 0
self.layout(width - absxo)
self.layout(width - absxo, st)
rv = renpy.display.render.Render(self.laidout_width + absxo, self.laidout_height + absxo)
if self.slow and speed:
start = max(self.slow_start, self.laidout_start)
length = start + int(st * speed)
else:
length = sys.maxint
self.slow = False
if self.slow_done:
self.slow_done()
rv = renpy.display.render.Render(self.laidout_width + absxo, self.laidout_height + absyo)
if self.style.drop_shadow:
self.render_pass(rv, dsxo, dsyo, self.style.drop_shadow_color, False, length)
self.render_pass(rv, dsxo, dsyo, self.style.drop_shadow_color, False, length, st)
self.slow = not self.render_pass(rv, xo, yo, self.style.color, True, length)
if self.render_pass(rv, xo, yo, self.style.color, True, length, st):
if self.slow:
self.slow = False
if self.slow_done:
self.slow_done()
if self.slow:
renpy.display.render.redraw(self, 0)
@@ -499,4 +889,5 @@ class ParameterizedText(object):
return Text(string, style=self.style, **self.properties)
def predict(self, callback):
return
+529 -69
View File
@@ -1,8 +1,24 @@
# This file contains code contributed by Brian Turcotte, the copyright
# assignment follows:
#
# Copyright for nonlinear ramp code added to ImageDissolve is hereby
# assigned to PyTom <pytom@bishoujo.us>.
#
# And feel free to change, improve, fold, spindle, or mutilate it,
# obviously.
#
# Cheers, Brian Turcotte (shaja).
import renpy
from renpy.display.render import render
import pygame
from pygame.constants import *
# We used time too many other times. :-(
from time import time as now
# This is a utility function that attempts to refactor an old and a new
# Fixed into four Fixeds: below, old, new, and above. Since only the
# old and new need transitions, this can be a significant win.
@@ -55,13 +71,13 @@ def refactor_fixed(in_old, in_new):
class Transition(renpy.display.core.Displayable):
"""
This is the base class of all transitions. It takes care of event
dispatching (primarily by passing all events off to a SayBehavior.)
This is the base class of most transitions. It takes care of event
dispatching.
"""
def __init__(self, delay):
super(Transition, self).__init__()
self.delay = delay
self.offsets = [ ]
self.events = True
def event(self, ev, x, y):
@@ -70,87 +86,160 @@ class Transition(renpy.display.core.Displayable):
else:
return None
class Fade(Transition):
def find_focusable(self, callback, focus_name):
self.new_widget.find_focusable(callback, focus_name)
class NoTransition(Transition):
"""
This is a transition that doesn't do anything, and simply displays
the new_widget for a specified amount of time. It's almost
certainly not interesting by itself, but it may come in quite
handy as part of a MultipleTransition.
"""
def __init__(self, delay, old_widget=None, new_widget=None):
super(NoTransition, self).__init__(delay)
self.old_widget = old_widget
self.new_widget = new_widget
self.events = True
def render(self, width, height, st):
rv = renpy.display.render.Render(width, height)
rv.blit(renpy.display.render.render(self.new_widget,
width,
height,
st), (0, 0))
return rv
class MultipleTransition(Transition):
"""
This is a transition that can sequence between multiple screens,
showing a different transition between each.
This must be supplied with a tuple containing an odd number of
components. The first, third, and so on components are interpreted
as screens that can be shown to the user, while the even components
are transitions between those screens.
A screen can be any displayable, but normally an Image or Solid is
most appropriate. An screen can also be False to represent the screen
we are transitioning from, or True to represent the screen we are
transitioning to. Almost always, the first argument will be False
and the last will be True.
"""
def __init__(self, args, old_widget=None, new_widget=None):
if len(args) % 2 != 1 or len(args) < 3:
raise Exception("MultipleTransition requires an odd number of arguments, and at least 3 arguments.")
self.transitions = [ ]
def oldnew(w):
if w is False:
return old_widget
if w is True:
return new_widget
return w
for old, trans, new in zip(args[0::2], args[1::2], args[2::2]):
old = oldnew(old)
new = oldnew(new)
self.transitions.append(trans(old_widget=old, new_widget=new))
super(MultipleTransition, self).__init__(sum([i.delay for i in self.transitions]))
self.event_target = None
self.time_offset = 0
self.new_widget = self.transitions[-1]
self.events = False
def render(self, width, height, st):
while True:
trans = self.transitions[0]
stoff = st - self.time_offset
if stoff < trans.delay:
break
if len(self.transitions) == 1:
break
self.time_offset += trans.delay
self.transitions.pop(0)
if len(self.transitions) == 1:
self.events = True
self.event_target = trans
rv = renpy.display.render.Render(width, height)
rv.blit(renpy.display.render.render(trans, width, height, stoff), (0,0))
if stoff > 0:
renpy.display.render.redraw(self, stoff)
return rv
def Fade(out_time, hold_time, in_time,
old_widget=None, new_widget=None,
color=None,
widget=None,
):
"""
This returns an object that can be used as an argument to a with
statement to fade the old scene into a solid color, waits for a
given amount of time, and then fades from the solid color into
the new scene.
@param in_time: The amount of time that will be spent
@param out_time: The amount of time that will be spent
fading from the old scene to the solid color. A float, given as
seconds.
@param hold_time: The amount of time that will be spent
displaying the solid color. A float, given as seconds.
@param out_time: The amount of time that will be spent
@param in_time: The amount of time that will be spent
fading from the solid color to the new scene. A float, given as
seconds.
@param color: The solid color that will be faded
to. This is an RGB triple, where each element is in the range 0
to 255. This defaults to black.
@param color: The solid color that will be fade to. A tuple containing
three components, each between 0 or 255. This can also be None.
@param widget: This is a widget that will be faded to, if color
is None. This allows a fade to be to an image rather than just
a solid color.
If both color and widget are None, then the fade is to black.
"""
def __init__(self, out_time, hold_time, in_time,
old_widget=None, new_widget=None, color=(0, 0, 0)):
dissolve = renpy.curry.curry(Dissolve)
notrans = renpy.curry.curry(NoTransition)
if color:
widget = renpy.display.image.Solid(color)
super(Fade, self).__init__(out_time + hold_time + in_time)
if not widget:
widget = renpy.display.image.Solid((0, 0, 0, 255))
self.out_time = out_time
self.hold_time = hold_time
self.in_time = in_time
self.old_widget = old_widget
self.new_widget = new_widget
self.color = color
args = [ False, dissolve(out_time), widget ]
# self.frames = 0
if hold_time:
args.extend([ notrans(hold_time), widget, ])
# def __del__(self):
# print "Faded using", self.frames, "frames."
args.extend([dissolve(in_time), True ])
def render(self, width, height, st):
return MultipleTransition(args, old_widget=old_widget, new_widget=new_widget)
# self.frames += 1
rv = renpy.display.render.Render(width, height)
events = False
if st < self.out_time:
widget = self.old_widget
alpha = int(255 * (st / self.out_time))
elif st < self.out_time + self.hold_time:
widget = None
alpha = 255
else:
widget = self.new_widget
alpha = 255 - int(255 * ((st - self.out_time - self.hold_time) / self.in_time))
events = True
if widget:
surf = render(widget, width, height, st)
rv.blit(surf, (0, 0))
self.events = events
# Just to be sure.
if alpha < 0:
alpha = 0
if alpha > 255:
alpha = 255
rv.fill(self.color[:3] + (alpha,))
if st < self.in_time + self.hold_time + self.out_time:
renpy.display.render.redraw(self, 0)
return rv
# This was a nifty idea that just didn't work out, since we can't vary
# the alpha on an image with an alpha channel. Too bad.
@@ -212,6 +301,75 @@ class Fade(Transition):
# return rv
class Pixellate(Transition):
"""
This pixellates out the old scene, and then pixellates in the new
scene, taking the given amount of time and the given number of pixellate
steps in each direction.
"""
def __init__(self, time, steps, old_widget=None, new_widget=None):
time = float(time)
if not renpy.display.module.can_pixellate:
time = 0
super(Pixellate, self).__init__(time)
self.time = time
self.steps = steps
self.old_widget = old_widget
self.new_widget = new_widget
self.surface = None
self.surface_size = None
self.events = False
self.quantum = time / ( 2 * steps )
def render(self, width, height, st):
if st >= self.time:
self.events = True
return render(self.new_widget, width, height, st)
step = st // self.quantum + 1
visible = self.old_widget
if step > self.steps:
step = (self.steps * 2) - step + 1
visible = self.new_widget
self.events = True
rv = renpy.display.render.Render(width, height)
rdr = render(visible, width, height, st)
# No alpha support.
surf = rdr.pygame_surface(False)
if surf.get_size() != self.surface_size:
self.surface_size = surf.get_size()
self.surface = pygame.Surface(self.surface_size, surf.get_flags(), surf)
px = 2 ** step
renpy.display.module.pixellate(surf, self.surface, px, px, px, px)
renpy.display.render.mutated_surface(self.surface)
rv.blit(self.surface, (0, 0))
if self.events:
rv.focuses.extend(rdr.focuses)
# renpy.display.render.redraw(self, self.quantum - st % self.quantum)
renpy.display.render.redraw(self, 0)
return rv
class Dissolve(Transition):
"""
@@ -219,7 +377,7 @@ class Dissolve(Transition):
overlaying the new scene on top of the old scene and varying its
alpha from 0 to 255.
@param delay: The amount of time the dissolve will take.
@param time: The amount of time the dissolve will take.
"""
def __init__(self, time, old_widget=None, new_widget=None):
@@ -253,9 +411,10 @@ class Dissolve(Transition):
surf = top.pygame_surface(False)
renpy.display.render.mutated_surface(surf)
if id(top) == self.old_top and id(bottom) == self.old_bottom:
rv.focuses.extend(top.focuses)
# Fast rendering path.
if renpy.config.enable_fast_dissolve and id(top) == self.old_top and id(bottom) == self.old_bottom and hasattr(self.new_widget, 'layers'):
# Fast rendering path. Only used for full-screen, top-level, renders.
alpha = alpha / 255.0
change = ( alpha - self.old_alpha) / ( 1.0 - self.old_alpha)
@@ -271,7 +430,7 @@ class Dissolve(Transition):
# Complete rendering path.
rv.blit(bottom, (0, 0))
rv.blit(bottom, (0, 0), focus=False)
surf.set_alpha(alpha, RLEACCEL)
rv.blit(surf, (0, 0))
@@ -352,7 +511,7 @@ class CropMove(Transition):
image. Otherwise, the top layer contains the old image.
"""
super(CropMove, self).__init__(time)
self.time = time
if mode == "wiperight":
@@ -503,13 +662,314 @@ class CropMove(Transition):
rv = renpy.display.render.Render(width, height)
rv.blit(render(self.bottom, width, height, st), (0, 0))
rv.blit(render(self.bottom, width, height, st), (0, 0), focus=not self.topnew)
top = render(self.top, width, height, st)
ss = top.subsurface(crop)
rv.blit(ss, pos)
ss = top.subsurface(crop, focus=self.topnew)
rv.blit(ss, pos, focus=self.topnew)
renpy.display.render.redraw(self, 0)
return rv
def MoveTransition(delay, old_widget=None, new_widget=None):
"""
This transition attempts to find images that have changed
position, and moves them from the old position to the new
transition, taking delay seconds to complete the move.
Images are considered to be the same if they have the same tag, in
the same way that the tag is used to determine which image to
replace or to hide.
If you use this transition to slide an image off the side of the
screen, remember to hide it when you are done.
"""
def position(d):
placement = d.get_placement()
xpos = placement.xpos
ypos = placement.ypos
if isinstance(xpos, float):
xpos = int(renpy.config.screen_width * xpos)
if isinstance(ypos, float):
ypos = int(renpy.config.screen_height * ypos)
return xpos, ypos, placement.xanchor, placement.yanchor
def merge_slide(old, new):
# If new does not have .layers or .scene_list, then we simply
# insert a move from the old position to the new position.
if not hasattr(new, 'layers') and not hasattr(new, 'scene_list'):
return renpy.display.layout.Move(position(old),
position(new),
delay,
new,
)
# If we're in the root widget, merge the child widgets for
# each layer.
if new.layers:
assert old.layers
rv = renpy.display.layout.Fixed()
rv.layers = { }
for layer in renpy.config.layers:
f = new.layers[layer]
if isinstance(f, renpy.display.layout.Fixed) and f.scene_list:
f = merge_slide(old.layers[layer], new.layers[layer])
rv.layers[layer] = f
rv.add(f)
return rv
# Otherwise, we recompute the scene list for the two widgets, merging
# as appropriate.
tags = { }
for tag, time, d in old.scene_list:
if tag is None:
continue
tags[tag] = d
newsl = [ ]
for tag, time, d in new.scene_list:
if tag is None or tag not in tags:
newsl.append((tag, time, d))
continue
oldpos = position(tags[tag])
newpos = position(d)
if oldpos == newpos:
newsl.append((tag, time, d))
continue
move = renpy.display.layout.Move(position(tags[tag]),
position(d),
delay,
d,
)
newsl.append((tag, now(), move))
rv = renpy.display.layout.Fixed()
rv.append_scene_list(newsl)
return rv
rv = merge_slide(old_widget, new_widget)
rv.delay = delay
return rv
class ImageDissolve(Transition):
"""
This dissolves the old scene into the new scene, using an image
to control the dissolve process.
A list of values is used to control this mapping. This list of
values consists 256 fully transparent values, a ramp (of a
specified number of steps) from full transparency to full opacity,
and 256 fully opaque values. A 256 entry window is slid over this
list, and the values found within are used to map the red channel
of the image onto the opacity of the new scene.
Basically, this means that while pixels come in first, black last,
and the ramp controls the sharpness of the transition.
@param image: The image that will be used to control this
transition. The image should be the same size as the scene being
dissolved.
@param time: The amount of time the dissolve will take.
@param ramplen: The number of pixels of ramp to use. This defaults
to 8.
@param ramptype: Type of alpha ramp. Possible types are: linear, cube,
dcube, mcube. Default is linear. Non-linear types must have
ramplen >= 8. "cube": Ease in, sharp out. "dcube": Sharp in, sharp out.
"mcube": Ease in, ease out.
@param reverse: This reverses the ramp and the direction of the window
slide. When True, black pixels dissolve in first, and while pixels come
in last.
"""
def generate_ramp(self, ramplen, ramptype, reverse):
"""
Precomputes the ramp.
"""
ramp = '\x00' * 256
if ramptype == 'cube':
# make sure ramplen is big enough, to avoid div-by-0 errors
# Not much point in nonlinear if the size is that small, anyway
if ramplen >= 8:
table = []
for i in range(ramplen):
table.append(i * i * i)
scale = max(table) / 255.0
for i in range(ramplen):
#print i, table[i], table[i] / scale
ramp += chr(int(table[i] / scale))
else:
ramptype = 'linear'
elif ramptype == 'dcube':
if ramplen >= 8:
table = []
for i in range(ramplen / 2 - ramplen, ramplen / 2):
table.append(i * i * i)
adj = abs(min(table))
for i in range(len(table)):
table[i] += adj
scale = max(table) / 255.0
#print "scale:", scale
for i in range(ramplen):
#print i, table[i], table[i] / scale
ramp += chr(int(table[i] / scale))
else:
ramptype = 'linear'
elif ramptype == 'mcube':
if ramplen >= 8:
ramplen = (ramplen / 2) * 2 # make sure it's even
table = []
for i in range(ramplen / 2):
table.append(i * i * i)
adj = table[-1]
tmptable = []
for i in table:
tmptable.append(i)
for i in range(1, len(table) + 1):
tmptable.append(abs(table[len(table) - i] - adj) + adj)
table = tmptable
scale = max(table) / 255.0
#print "scale:", scale
for i in range(ramplen):
#print i, table[i], table[i] / scale
ramp += chr(int(table[i] / scale))
else:
ramptype = 'linear'
if ramptype == 'linear':
for i in range(ramplen):
ramp += chr(255 * i / ramplen)
ramp += '\xff' * 256
if reverse:
ramp = list(ramp)
ramp.reverse()
ramp = ''.join(ramp)
return ramp
def __init__(self, image, time, ramplen=8, ramptype='linear', reverse=False,
old_widget=None, new_widget=None):
super(ImageDissolve, self).__init__(time)
self.time = time
self.old_widget = old_widget
self.new_widget = new_widget
self.events = False
self.old_bottom = None
self.old_top = None
self.old_ramp = '\x00' * 256
self.image = renpy.display.im.load_image(image)
self.ramp = self.generate_ramp(ramplen, ramptype, reverse)
self.steps = ramplen + 256
self.reverse = reverse
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)
step = int(self.steps * st / self.time)
if self.reverse:
step = self.steps - step
ramp = self.ramp[step:step+256]
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(True)
renpy.display.render.mutated_surface(surf)
rv.focuses.extend(top.focuses)
if renpy.config.enable_fast_dissolve and id(top) == self.old_top and id(bottom) == self.old_bottom and hasattr(self.new_widget, 'layers'):
# Fast rendering path. Only used for full-screen, top-level, renders.
fast_ramp = [ ]
for new, old in zip(ramp, self.old_ramp):
new = ord(new)
old = ord(old)
if new >= 255:
fast_ramp.append('\xff')
continue
change = 255 * ( new - old ) / ( 255 - old )
fast_ramp.append(chr(int(change)))
renpy.display.module.alpha_munge(self.image, surf,
''.join(fast_ramp))
rv.blit(surf, (0, 0))
else:
# Complete rendering path.
rv.blit(bottom, (0, 0), focus=False)
renpy.display.module.alpha_munge(self.image, surf, ramp)
rv.blit(surf, (0, 0))
self.old_ramp = ramp
self.old_top = id(top)
self.old_bottom = id(bottom)
return rv
+3 -4
View File
@@ -38,7 +38,7 @@ def movie_stop(clear=True):
surface = None
loops = 0
renpy.display.audio.enable_mixer()
renpy.audio.audio.init()
if clear:
renpy.game.context().scene_lists.movie = None
@@ -104,7 +104,7 @@ def interact():
if not movie:
# Needed so we get movie sound.
renpy.display.audio.disable_mixer()
renpy.audio.audio.quit()
m = pygame.movie.Movie(renpy.loader.transfn(info.filename))
@@ -140,7 +140,7 @@ def interact():
if renpy.config.debug_sound:
raise
else:
renpy.display.audio.enable_mixer()
renpy.audio.audio.enable_mixer()
return False
@@ -160,7 +160,6 @@ class Movie(renpy.display.layout.Null):
def render(self, width, height, st):
renpy.display.render.redraw(self, 0)
if surface:
renpy.display.render.mutated_surface(surface)
+32 -7
View File
@@ -22,6 +22,9 @@ class Context(object):
@ivar rollback: True if this context participates in rollbacks.
@ivar runtime: The time spent in this context, in milliseconds.
@ivar info: An object that is made available to user code. This object
does participates in rollback.
"""
def __init__(self, rollback, context=None):
@@ -30,11 +33,14 @@ class Context(object):
self.return_stack = [ ]
self.rollback = rollback
self.runtime = 0
self.info = renpy.python.RevertableObject()
self.seen = False
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)
@@ -64,25 +70,37 @@ class Context(object):
if self.rollback and renpy.game.log:
renpy.game.log.begin()
self.seen = False
try:
node = node.execute()
except renpy.game.JumpException, e:
node = renpy.game.script.lookup(e.args[0])
renpy.game.seen_ever[self.current] = True
renpy.game.seen_session[self.current] = True
if self.seen:
renpy.game.seen_ever[self.current] = True
renpy.game.seen_session[self.current] = True
if self.rollback and renpy.game.log:
renpy.game.log.complete()
def mark_seen(self):
"""
Marks the current statement as one that has been seen by the user.
"""
self.seen = True
def call(self, label, return_site=None):
"""
Calls the named label.
"""
if not self.current:
raise "Context not capable of executing Ren'Py code."
if return_site is None:
return_site is self.current
return_site = self.current
self.return_stack.append(return_site)
self.current = label
@@ -115,7 +133,8 @@ 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
def predict(self, callback):
@@ -125,6 +144,9 @@ class Context(object):
they will be potentially loaded.
"""
if not self.current:
return
nodes = [ renpy.game.script.lookup(self.current) ]
for i in range(0, renpy.config.predict_statements):
@@ -154,6 +176,9 @@ class Context(object):
we've finished this statement in the current session.
"""
if not self.current:
return False
if ever:
seen = renpy.game.seen_ever
else:
+382 -83
View File
@@ -6,19 +6,22 @@
import renpy
# Many of these shouldn't be used directly.
from renpy.display.layout import *
from renpy.display.text import *
from renpy.display.behavior import *
from renpy.display.image import *
# from renpy.display.layout import *
from renpy.display.text import ParameterizedText
from renpy.display.behavior import Keymap
# from renpy.display.image import *
from renpy.curry import curry
from renpy.display.audio import music_start, music_stop
from renpy.display.audio import play
# from renpy.display.audio import music_start, music_stop
from renpy.audio.sound import play
from renpy.display.video import movie_start_fullscreen, movie_start_displayable, movie_stop
from renpy.loadsave import *
from renpy.loadsave import load, save, saved_games, can_load
from renpy.python import py_eval as eval
from renpy.python import rng as random
import renpy.audio.sound as sound
import renpy.audio.music as music
import time
# This is a map from image name to a Displayable object corresponding
@@ -27,12 +30,23 @@ images = { }
def checkpoint():
"""
Marks the current statement as a checkpoint, which is a place
where rolling back can stop when the user asks for a rollback.
This creates a checkpoint that the user can rollback to. The
checkpoint is placed at the statement after the last statement
that interacted with the user. Once this function has been called,
there should be no more interaction with the user in the current
statement.
"""
renpy.game.log.checkpoint()
def block_rollback():
"""
Prevents the game from rolling back to before the current
statement.
"""
renpy.game.log.block()
# def interact(**kwargs):
# return renpy.game.interface.interact(**kwargs)
@@ -44,62 +58,97 @@ def scene_lists(index=-1):
return renpy.game.context(index).scene_lists
def scene_list_add(listname, displayable, key=None):
def image(name, img):
"""
Adds the displayable to the named scene list, with the
given key if one is provided.
This is used to execute the image statment. It takes as arguments
an image name and an image object, and associates the image name
with the image object.
@param listname: One of 'master' or 'transient'.
Like the image statment, this function should only be executed
in init blocks.
@param name: The image name, a tuple of strings.
@param img: The displayable that is associated with that name. If this
is a string or tuple, it is interpreted as an argument to Image.
"""
if listname not in ('master', 'transient'):
raise Exception("Scene list '%s' doesn't exist." % listname)
if not renpy.game.init_phase:
raise Exception("Images may only be declared inside init blocks.")
scene_lists().add(listname, displayable, key)
img = renpy.display.im.image(img, loose=True)
def show(at_disp, with_disp=None, key=None):
images[name] = img
def show(name, at_list=[ ], layer='master'):
"""
Shows the displayable, as if it was added to a scene list with the show command.
This is used to execute the show statement, adding the named image
to the screen as part of the master layer.
@param at_disp: The displayable that is added to the screen as if
it was the result of the at clause in the show statement.
@param name: The name of the image to add to the screen. This is a tuple
of strings, one string for each component of the image name.
@param with_displ: The displayable that is added to the screen as
if it was the result of the with clause in the show statement. If
None, then this is taken from the at command.
@param key: The key that is used to remove this displayable from
the scene list, or None if there is no key.
"""
if not with_disp:
with_disp = at_disp
scene_list_add('master', at_disp, key)
scene_list_add('transient', at_disp, key)
def hide(key):
"""
Removes items named with the given key from the scene lists.
@param at_list: The at list, a list of functions that are applied
to the image when shown. The members of the at list need to
be pickleable if sticky_positions is True.
"""
sls = scene_lists()
sls.remove('master', key)
sls.remove('transient', key)
key = name[0]
def scene():
if renpy.config.sticky_positions:
if not at_list and key in sls.sticky_positions:
at_list = sls.sticky_positions[key]
sls.sticky_positions[key] = at_list
img = renpy.display.image.ImageReference(name)
for i in at_list:
img = i(img)
# Update the list of images we have ever seen.
renpy.game.persistent._seen_images[tuple(name)] = True
sls.add(layer, img, key)
def hide(name, layer='master'):
"""
This clears the scene lists, as if the scene statement executed.
This finds items in the master layer that have the same name
as the first component of the given name, and removes them
from the master layer. This is used to execute the hide
statement.
@param name: The name of an image. A tuple of strings, but only
the first component of this tuple is ever accessed.
"""
sls = scene_lists()
sls.clear('master')
sls.clear('transient')
key = name[0]
sls.remove(layer, key)
if key in sls.sticky_positions:
del sls.sticky_positions[key]
def scene(layer='master'):
"""
This clears out the master layer. This is used in the execution of
the scene statment, but only to clear out the layer. If you want
to then add something new, call renpy.show after this.
"""
sls = scene_lists()
sls.clear(layer)
sls.sticky_positions.clear()
def watch(expression, style='default', **properties):
"""
This watches the given python expression, by displaying it in the
upper-right corner of the screen (although position properties
upper-left corner of the screen (although position properties
can change that). The expression should always be
defined, never throwing an exception.
@@ -107,8 +156,8 @@ def watch(expression, style='default', **properties):
"""
def overlay_func():
return [ renpy.display.text.Text(renpy.python.py_eval(expression),
style=style, **properties) ]
renpy.ui.text(unicode(renpy.python.py_eval(expression)),
style=style, **properties)
renpy.config.overlay_functions.append(overlay_func)
@@ -139,7 +188,7 @@ def input(prompt, default='', allow=None, exclude='{}', length=None):
renpy.ui.close()
return renpy.ui.interact()
return renpy.ui.interact(mouse='prompt')
def menu(items, set_expr):
"""
@@ -149,7 +198,7 @@ def menu(items, set_expr):
# Filter the list of items to only include ones for which the
# condition is true.
items = [ (label, value)
items = [ (label % tag_quoting_dict, value)
for label, condition, value in items
if renpy.python.py_eval(condition) ]
@@ -180,7 +229,18 @@ def menu(items, set_expr):
return rv
def display_menu(items, window_style='menu_window'):
def choice_for_skipping():
"""
This is called to indicate to the skipping code that we have
reached a choice. If we're skipping, and if the skip after
choices preference is not True, then this disables skipping.
"""
if renpy.config.skipping and not renpy.game.preferences.skip_after_choices:
renpy.config.skipping = False
def display_menu(items, window_style='menu_window', interact=True):
"""
Displays a menu containing the given items, returning the value of
the item the user selects.
@@ -190,27 +250,66 @@ def display_menu(items, window_style='menu_window'):
for this menuitem. The second element is the value to be returned
if this item is selected, or None if this item is a non-selectable
caption.
@param interact: If True, then an interaction occurs. If False, no suc
interaction occurs, and the user should call ui.interact() manually.
"""
choice_for_skipping()
if renpy.config.auto_choice_delay:
choices = [ val for label, val in items ]
while None in choices:
choices.remove(None)
renpy.ui.pausebehavior(renpy.config.auto_choice_delay,
random.choice(choices))
renpy.ui.window(style=window_style)
renpy.ui.menu(items)
rv = renpy.ui.interact()
checkpoint()
for label, val in items:
if val:
log("Choice: " + label)
else:
log(label)
return rv
log("")
if interact:
rv = renpy.ui.interact(mouse='menu')
for label, val in items:
if rv == val:
log("User chose: " + label)
break
else:
log("No choice chosen.")
log("")
checkpoint()
return rv
return None
class TagQuotingDict(object):
def __getitem__(self, key):
if key in renpy.game.store:
rv = renpy.game.store[key]
store = vars(renpy.store)
if key in store:
rv = store[key]
if isinstance(rv, (str, unicode)):
rv = rv.replace("{", "{{")
return rv
else:
raise Exception("During an interpolation, '%s' was not found as a variable." % key)
if renpy.config.debug:
raise Exception("During an interpolation, '%s' was not found as a variable." % key)
return "<" + key + " unbound>"
tag_quoting_dict = TagQuotingDict()
@@ -241,6 +340,15 @@ def display_say(who, what, who_style='say_label',
what_suffix='',
interact=True,
slow=True,
slow_speed=None,
slow_abortable=True,
image=False,
afm=True,
ctc=None,
ctc_position="nestled",
all_at_once=False,
what_properties={},
window_properties={},
**properties):
"""
@param who: Who is saying the dialogue, or None if it's not being
@@ -248,31 +356,97 @@ def display_say(who, what, who_style='say_label',
@param what: What is being said.
@param afm: If True, the auto-forwarding mode is enabled. If False,
it is disabled.
@param all_at_once: If True, then the text is displayed all at once. (This is forced to true if interact=False.)
For documentation of the various prefixes, suffixes, and styles,
please read the documentation for Character.
"""
# If we're going to do an interaction, then saybehavior needs
# to be here.
if interact:
renpy.ui.saybehavior()
if who is not None:
who = who_prefix + who + who_suffix
# If we're in fast skipping mode, don't bother with say
# statements at all.
if renpy.config.skipping == "fast":
# Clears out transients.
with(None)
return
what = what_prefix + what + what_suffix
renpy.ui.window(style=window_style)
renpy.ui.vbox(padding=10)
if not interact:
all_at_once = True
if who is not None:
renpy.ui.text(who, style=who_style, **properties)
if all_at_once:
pause = None
else:
pause = 0
renpy.ui.text(what, style=what_style, slow=slow)
renpy.ui.close()
keep_interacting = True
slow_start = 0
while keep_interacting:
# If we're going to do an interaction, then saybehavior needs
# to be here.
if interact:
behavior = renpy.ui.saybehavior()
else:
behavior = None
renpy.ui.window(style=window_style, **window_properties)
renpy.ui.vbox(padding=10)
if who is not None and not image:
whotext = who_prefix + who + who_suffix
renpy.ui.text(whotext, style=who_style, **properties)
elif who is not None and image:
renpy.ui.image(who, style=who_style, **properties)
# Code to support ctc.
ctcwhat = [ what ]
if ctc and ctc_position == "nestled":
ctcwhat.extend([ " ", ctc ])
slow_done = None
if ctc and ctc_position == "fixed":
def slow_done():
renpy.ui.add(ctc)
restart_interaction()
what_text = renpy.ui.text(ctcwhat,
style=what_style,
slow=slow,
slow_done=slow_done,
slow_abortable=slow_abortable,
slow_start=slow_start,
pause=pause,
slow_speed = None,
**what_properties)
if behavior and afm:
behavior.set_afm_length(what_text.get_simple_length() - slow_start)
renpy.ui.close()
if interact:
renpy.ui.interact(mouse='say')
keep_interacting = what_text.get_keep_pausing()
if keep_interacting:
slow_start = what_text.get_laidout_length()
pause += 1
if who:
log(who)
log(what)
log("")
if interact:
renpy.ui.interact()
checkpoint()
def imagemap(ground, selected, hotspots, unselected=None, overlays=False,
@@ -310,9 +484,7 @@ 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))
rv = renpy.ui.interact(suppress_overlay=(not overlays), mouse='imagemap')
checkpoint()
return rv
@@ -326,18 +498,21 @@ def pause(delay=None, music=None):
@param delay: The number of seconds to delay.
@param music: If supplied, this gives the number of seconds into
the background music that we will delay until. If music is
playing, this takes precedence, otherwise the delay parameter
take precedence.
@param music: If supplied, and music is playing, this takes
precedence over the delay parameter. It gives a time, in seconds,
into the playing music track. Ren'Py will pause until the music
has played up to that point..
Returns True if the pause was interrupted by the user hitting a key
or clicking a mouse, or False if the pause was ended by the appointed
time being reached.
"""
if renpy.config.skipping == "fast":
return True
if music is not None:
newdelay = renpy.display.audio.music_delay(music)
newdelay = renpy.audio.music.get_delay(music)
if newdelay is not None:
delay = newdelay
@@ -347,7 +522,7 @@ def pause(delay=None, music=None):
if delay:
renpy.ui.pausebehavior(delay, False)
return renpy.ui.interact()
return renpy.ui.interact(mouse='pause')
def movie_cutscene(filename, delay, loops=0):
"""
@@ -379,12 +554,17 @@ def movie_cutscene(filename, delay, loops=0):
def with(trans):
"""
Behaves identically to a with statement. The only reason to use this
over a Ren'Py with statement is to get at the return code, which is
True if the transition was interrupted, or False otherwise.
Implements the with statement. One reason to use this over a
Ren'Py with statement is to get at the return code, which is True
if the transition was interrupted, or False otherwise.
"""
# Code basically copied from ast.With.execute.
if renpy.config.skipping:
trans = None
if renpy.config.with_callback:
renpy.config.with_callback(trans)
if not trans:
renpy.game.interface.with_none()
return False
@@ -393,7 +573,8 @@ def with(trans):
renpy.game.interface.set_transition(trans)
return renpy.game.interface.interact(show_mouse=False,
trans_pause=True,
suppress_overlay=not renpy.config.overlay_during_wait)
suppress_overlay=not renpy.config.overlay_during_with,
mouse='with')
else:
return False
@@ -461,6 +642,15 @@ def jump(label):
raise renpy.game.JumpException(label)
def jump_out_of_context(label):
"""
Causes control to leave the current context, and then to be
transferred in the parent context to the given label.
"""
raise renpy.game.JumpOutException(label)
def screenshot(filename):
"""
Saves a screenshot in the named filename.
@@ -485,6 +675,14 @@ def version():
return renpy.version
def module_version():
"""
Returns a number corresponding to the current version of the Ren'Py module,
or 0 if the module wasn't loaded.
"""
return renpy.display.module.version
def transition(trans, layer=None):
"""
Sets the transition that will be used for the next
@@ -524,6 +722,15 @@ 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.
"""
return renpy.loader.loadable(filename)
def exists(filename):
"""
Returns true if the given filename can be found in the
@@ -549,7 +756,99 @@ 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
def music_start(filename, loops=True, fadeout=None):
"""
Deprecated music start function, retained for compatibility. Use
renpy.music.play() or .queue() instead.
"""
renpy.audio.music.play(filename, loop=loops, fadeout=fadeout)
def music_stop(fadeout=None):
"""
Deprecated music start function, retained for compatibility. Use
renpy.music.play() or .queue() instead.
"""
renpy.audio.music.stop(fadeout=fadeout)
def get_filename_line():
"""
Returns a pair giving the filename and line number of the current
statement.
"""
n = renpy.game.script.namemap[renpy.game.context().current]
return n.filename, n.linenumber
def launch_editor():
"""
This causes an editor to be launched at the location of the current
statement.
"""
import renpy.subprocess as subprocess
if not renpy.config.editor:
return
filename, line = get_filename_line()
subs = dict(filename=filename, line=line)
cmd = renpy.config.editor % subs
try:
subprocess.Popen(cmd, shell=True)
except:
if renpy.config.debug:
raise
# A file that log logs to.
logfile = None
def log(msg):
"""
If config.log is not set, this does nothing. Otherwise, it opens
the logfile (if not already open), formats the message to 70
columns, and prints it to the logfile.
"""
global logfile
if not renpy.config.log:
return
if msg is None:
return
if not logfile:
import codecs
logfile = file(renpy.config.log, "a")
if not logfile.tell():
logfile.write(codecs.BOM_UTF8)
import textwrap
print >>logfile, textwrap.fill(msg).encode("utf-8")
logfile.flush()
def force_full_redraw():
"""
Forces the screen to be redrawn in full. Call this after using pygame
to redraw the screen directly.
"""
renpy.game.interface.display.full_redraw = True
call_in_new_context = renpy.game.call_in_new_context
curried_call_in_new_context = renpy.curry.curry(renpy.game.call_in_new_context)
invoke_in_new_context = renpy.game.invoke_in_new_context
+64 -11
View File
@@ -18,14 +18,12 @@ basepath = None
# everything that can be loaded, before archives are used.
searchpath = [ ]
# The options that were read off the command line.
options = None
# 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.
@@ -79,16 +77,28 @@ class Preferences(object):
"""
def reinit(self):
self.fullscreen = False
self.skip_unseen = False
self.text_cps = 0
self.afm_time = 0
# These will be going away soon.
self.sound = True
self.music = True
self.skip_unseen = False
self.fast_text = False
# 2 - All transitions.
# 1 - Only non-default transitions.
# 0 - No transitions.
self.transitions = 2
self.skip_after_choices = False
# Mixer channel info.
self.volumes = { }
self.mute = { }
def __setstate__(self, state):
self.reinit()
vars(self).update(state)
@@ -105,6 +115,10 @@ class RestartException(Exception):
been changed, and therefore execution needs to be restarted.
"""
def __init__(self, contexts, label):
self.contexts = contexts
self.label = label
class FullRestartException(Exception):
"""
An exception of this type forces a hard restart, completely
@@ -124,6 +138,12 @@ class JumpException(Exception):
to the named label.
"""
class JumpOutException(Exception):
"""
This should be raised with a label as the only argument. This exits
the current context, and then raises a JumpException.
"""
def context(index=-1):
"""
Return the current execution context, or the context at the
@@ -132,19 +152,52 @@ def context(index=-1):
return contexts[index]
def invoke_in_new_context(callable):
"""
This pushes the current context, and invokes the given python
function in a new context. When that function returns or raises an
exception, it removes the new context, and restores the current
context.
Please note that the context so created cannot execute renpy
code. So exceptions that change the flow of renpy code (like
the one created by renpy.jump) cause this context to terminate,
and are handled by the next higher context.
If you want to execute renpy code from the function, you can call
it with renpy.call_in_new_context.
Use this to begin a second interaction with the user while
inside an interaction.
"""
context = renpy.execution.Context(False, contexts[-1])
contexts.append(context)
try:
return callable()
finally:
contexts.pop()
def call_in_new_context(label):
"""
This code creates a new context, and starts executing code from
that label in the new context. Rollback is disabled in the
new context. (Actually, it will just bring you back to the
real context.)
Use this to begin a second interaction with the user while
inside an interaction.
"""
context = renpy.execution.Context(False, contexts[-1])
contexts.append(context)
context.goto_label(label)
context.run()
try:
context.goto_label(label)
context.run()
contexts.pop()
except renpy.game.JumpOutException, e:
contexts.pop()
raise renpy.game.JumpException(e.args[0])
contexts.pop()
interface.force_redraw = True
+267
View File
@@ -0,0 +1,267 @@
import renpy
import sys
import codecs
import os
import time
# Things to check in lint.
#
# Image files exist, and are of the right case.
# Jump/Call targets defined.
# Say whos can evaluate.
# Call followed by say.
# Show/Scene valid.
# At valid.
# With valid.
# Hide maybe valid.
# Expressions can compile.
# Reports a message to the user.
def report(node, msg, *args):
out = "%s:%d " % (node.filename, node.linenumber)
out += msg % args
print
print out.encode('utf-8')
added = { }
# Reports additional information about a message, the first time it
# occurs.
def add(msg):
if not msg in added:
added[msg] = True
print unicode(msg).encode('utf-8')
# Trys to evaluate an expression, announcing an error if it fails.
def try_eval(node, where, expr, additional=None):
try:
renpy.python.py_eval(expr)
except:
report(node, "Could not evaluate '%s', in %s.", expr, where)
if additional:
add(additional)
# Returns True of the expression can be compiled as python, False
# otherwise.
def try_compile(node, where, expr):
try:
renpy.python.py_compile_eval_bytecode(expr)
except:
report(node, "'%s' could not be compiled as a python expression, %s.", expr, where)
# This reports an error if we're sure that the image with the given name
# does not exist.
def image_exists(node, name):
name = list(name)
names = " ".join(name)
while name:
if tuple(name) in renpy.exports.images:
return
name.pop()
report(node, "The image named '%s' was not declared.", names)
# Lints ast.Image nodes.
def check_image(node):
name = " ".join(node.imgname)
files = [ ]
def files_callback(img):
files.extend(img.predict_files())
renpy.exports.images[node.imgname].predict(files_callback)
for fn in files:
if not renpy.loader.loadable(fn):
report(node, "Image '%s' uses file '%s', which is not loadable.", name, fn)
continue
try:
renpy.loader.transfn(fn)
except:
continue
if renpy.loader.transfn(fn) and \
fn.lower() in filenames and \
fn != filenames[fn.lower()]:
report(node, "Filename case mismatch for image '%s'. '%s' was used in the script, but '%s' was found on disk.", name, fn, filenames[fn.lower()])
add("Case mismatches can lead to problems on Mac, Linux/Unix, and when archiving images. To fix them, either rename the file on disk, or the filename use in the script.")
continue
# Lints ast.Show and ast.Scene nodets.
def check_show(node):
# A Scene may have an empty imspec.
if not node.imspec:
return
name, at_list, layer = node.imspec
if layer not in renpy.config.layers:
report(node, "Uses layer '%s', which is not in config.layers.", layer)
image_exists(node, name)
for i in at_list:
try_eval(node, "the at list of a scene or show statment", i, "Perhaps you forgot to declare, or misspelled, a position?")
# Lints ast.Hide.
def check_hide(node):
name, at_list, layer = node.imspec
if layer not in renpy.config.layers:
report(node, "Uses layer '%s', which is not in config.layers.", layer)
if name[0] not in image_prefixes:
report(node, "The image prefix '%s' is not the prefix of any declared image.", name[0])
for i in at_list:
try_eval(node, "at list of hide statment", i)
def check_with(node):
try_eval(node, "a with statement or clause", node.expr, "Perhaps you forgot to declare, or misspelled, a transition?")
def check_say(node):
if node.who:
try_eval(node, "the who part of a say statement", node.who, "Perhaps you forgot to declare a character?")
if node.with:
try_eval(node, "the with clause of a say statement", node.with, "Perhaps you forgot to declare, or misspelled, a transition?")
def check_menu(node):
if node.with:
try_eval(node, "the with clause of a menu statement", node.with, "Perhaps you forgot to declare, or misspelled, a transition?")
if not [ (l, c, b) for l, c, b in node.items if l ]:
report(node, "The menu does not contain any selectable choices.")
for l, c, b in node.items:
if c:
try_compile(node, "in the if clause of a menuitem", c)
def check_jump(node):
if node.expression:
return
if not renpy.game.script.has_label(node.target):
report(node, "The jump is to notexistent label '%s'.", node.target)
def check_call(node):
if not isinstance(node.next.name, basestring):
report(node, "The call does not have a from clause associated with it.")
add("You can add from clauses to calls automatically by running the add_from program.")
add("This is necessary to ensure saves can be loaded even when the script changes.")
if node.expression:
return
if not renpy.game.script.has_label(node.label):
report(node, "The call is to notexistent label '%s'.", node.label)
def check_while(node):
try_compile(node, "in the condition of the while statement", node.condition)
def check_if(node):
for condition, block in node.entries:
try_compile(node, "in a condition of the if statement", condition)
def lint():
"""
The master lint function, that's responsible for staging all of the
other checks.
"""
print codecs.BOM_UTF8
print unicode(renpy.version + " lint report, generated at: " + time.ctime()).encode("utf-8")
# This is used to support the check_image.
global filenames
filenames = { }
for d in renpy.config.searchpath:
for fn in os.listdir(d):
filenames[fn.lower()] = fn
# This supports check_hide.
global image_prefixes
image_prefixes = { }
for k in renpy.exports.images:
image_prefixes[k[0]] = True
# Iterate through every statement in the program, processing
# them. We sort them in filename, linenumber order.
all_stmts = [ (i.filename, i.linenumber, i) for i in renpy.game.script.all_stmts ]
all_stmts.sort()
say_words = 0
say_count = 0
menu_count = 0
for fn, ln, node in all_stmts:
if isinstance(node, renpy.ast.Image):
check_image(node)
elif isinstance(node, renpy.ast.Show):
check_show(node)
elif isinstance(node, renpy.ast.Scene):
check_show(node)
elif isinstance(node, renpy.ast.Hide):
check_hide(node)
elif isinstance(node, renpy.ast.With):
check_with(node)
elif isinstance(node, renpy.ast.Say):
check_say(node)
say_count += 1
say_words += len(node.what.split())
elif isinstance(node, renpy.ast.Menu):
check_menu(node)
menu_count += 1
elif isinstance(node, renpy.ast.Jump):
check_jump(node)
elif isinstance(node, renpy.ast.Call):
check_call(node)
elif isinstance(node, renpy.ast.While):
check_while(node)
elif isinstance(node, renpy.ast.If):
check_if(node)
print
print
print "The game contains", say_count, "lines of dialogue containing a total of", say_words, "words, and", menu_count, "menus."
+101
View File
@@ -1,7 +1,13 @@
import renpy
import os
import os.path
from pickle import loads
from cStringIO import StringIO
import sys
import types
# Files on disk should be checked before archives. Otherwise, among
# other things, using a new version of bytecode.rpb will break.
archives = [ ]
@@ -15,13 +21,52 @@ def index_archives():
for prefix in renpy.config.archives:
fn = transfn(prefix + ".rpa")
try:
fn = transfn(prefix + ".rpa")
f = file(fn, "rb")
l = f.readline()
# 2.0 Branch.
if l.startswith("RPA-2.0 "):
offset = int(l[8:], 16)
f.seek(offset)
index = loads(f.read().decode("zlib"))
archives.append((prefix, index))
f.close()
continue
# 1.0 Branch.
f.close()
fn = transfn(prefix + ".rpi")
index = loads(file(fn, "rb").read().decode("zlib"))
archives.append((prefix, index))
except:
if renpy.config.debug:
raise
def listdirfiles():
"""
Returns a list of directory, file tuples known to the system. If
the file is in an archive, the directory is None.
"""
rv = [ ]
for i in renpy.config.searchpath:
for j in os.listdir(i):
rv.append((i, j))
for prefix, index in archives:
for j in index.iterkeys():
rv.append((None, j))
return rv
def load(name):
"""
@@ -57,6 +102,24 @@ def load(name):
raise Exception("Couldn't find file '%s'." % name)
def loadable(name):
"""
Returns True if the name is loadable with load, False if it is not.
"""
try:
transfn(name)
return True
except:
pass
for prefix, index in archives:
if name in index:
return True
return False
def transfn(name):
"""
Tries to translate the name to a file that exists in one of the
@@ -68,3 +131,41 @@ def transfn(name):
return d + "/" + name
raise Exception("Couldn't find file '%s'." % name)
class RenpyImporter(object):
"""
An importer, that tries to load modules from the places where Ren'Py
searches for data files.
"""
def translate(self, fullname):
fn = fullname.replace(".", "/") + ".py"
if loadable(fn):
return fn
fn = fullname.replace(".", "/") + "/__init__.py"
if loadable(fn):
return fn
return None
def find_module(self, fullname, path=None):
if self.translate(fullname):
return self
def load_module(self, fullname):
filename = self.translate(fullname)
mod = sys.modules.setdefault(fullname, types.ModuleType(fullname))
mod.__file__ = filename
mod.__loader__ = self
mod.__path__ = [ ]
exec load(filename) in mod.__dict__
return mod
def get_data(self, filename):
return load(filename).read()
sys.meta_path.append(RenpyImporter())
+26 -4
View File
@@ -5,6 +5,7 @@ import cStringIO
import zipfile
import time
import os
import re
import renpy
@@ -20,7 +21,7 @@ def debug_dump(prefix, o, seen):
return
if id(o) in seen:
print prefix, "@%x" % id(o)
print prefix, "@%x" % id(o), type(o)
return
seen[id(o)] = True
@@ -44,6 +45,8 @@ def debug_dump(prefix, o, seen):
debug_dump(prefix + " ", v, seen)
print prefix, "}"
elif isinstance(o, renpy.style.Style):
print "<style>"
elif hasattr(o, "__dict__"):
@@ -96,6 +99,9 @@ def save(filename, extra_info=''):
# print
# print "Debug Dump!"
# if os.environ['RENPY_DEBUG_DUMP']:
# renpy.config.debug = True
# debug_dump("", renpy.game.log, { })
# The actual game.
@@ -107,7 +113,7 @@ def save(filename, extra_info=''):
def saved_games():
def saved_games(regexp=r'.'):
"""
This scans the savegames that we know about and returns
information about them. Specifically, it returns tuple containing
@@ -117,11 +123,13 @@ def saved_games():
The savelist, in turn, is a list of tuples, with each tuple containing
the filename of the saved game, a Displayable containing a screenshot,
and a string giving the extra data of that save.
The regexp matches at the start of the filename, and filters the list.
"""
files = os.listdir(renpy.config.savedir)
files.sort()
files = [ i for i in files if i.endswith(savegame_suffix) ]
files = [ i for i in files if i.endswith(savegame_suffix) and re.match(regexp, i) ]
if not files:
newest = None
@@ -156,7 +164,20 @@ def saved_games():
newest = None
return saveinfo, newest
def can_load(filename):
"""
Returns true if we can load the given savegame file, False otherwise.
"""
try:
zf = zipfile.ZipFile(renpy.config.savedir + "/" + filename + savegame_suffix, "r")
zf.close()
return True
except:
return False
def load(filename):
"""
Loads the game from the given file. This function never returns.
@@ -165,4 +186,5 @@ def load(filename):
zf = zipfile.ZipFile(renpy.config.savedir + "/" + filename + savegame_suffix, "r")
log = loads(zf.read("log"))
zf.close()
log.unfreeze()
log.unfreeze(label="after_load")
+75 -9
View File
@@ -12,6 +12,7 @@
import renpy
import renpy.game as game
import os
import time
from pickle import loads, dumps, HIGHEST_PROTOCOL
def run(restart=False):
@@ -54,6 +55,10 @@ def run(restart=False):
game.persistent._seen_ever = { }
game.seen_ever = game.persistent._seen_ever
# Initialize the set of images seen ever.
if not game.persistent._seen_images:
game.persistent._seen_images = { }
# Clear the list of seen statements in this game.
game.seen_session = { }
@@ -66,11 +71,11 @@ def run(restart=False):
# Initialize the store.
renpy.store.store = renpy.store
game.store = vars(renpy.store)
renpy.store.persistent = game.persistent
renpy.store._preferences = game.preferences
# Set up styles.
renpy.style.reset()
game.style = renpy.style.StyleManager()
renpy.store.style = game.style
@@ -88,14 +93,21 @@ def run(restart=False):
renpy.game.exception_info = 'After initialization, but before game start.'
# Rebuild the various style caches.
game.style._build_style_caches()
renpy.style.build_styles()
# Index the archive files. We should not have loaded an image
# before this point. (As pygame will not have been initialized.)
renpy.loader.index_archives()
# Make a clean copy of the store.
game.clean_store = game.store.copy()
game.clean_store = vars(renpy.store).copy()
if renpy.game.options.lint:
renpy.lint.lint()
return
# Remove the list of all statements from the script.
game.script.all_stmts = None
# Re-Initialize the log.
game.log = renpy.python.RollbackLog()
@@ -109,13 +121,26 @@ def run(restart=False):
# Jump to an appropriate start label.
if game.script.has_label("_start"):
game.context().goto_label('_start')
start_label = '_start'
else:
game.context().goto_label('start')
start_label = 'start'
game.context().goto_label(start_label)
# Perhaps warp.
if renpy.game.options.warp:
label = renpy.warp.warp(renpy.game.options.warp)
game.context().goto_label(label)
if game.script.has_label('after_warp'):
game.context().call('after_warp')
try:
while True:
renpy.exports.log("--- " + time.ctime())
renpy.exports.log("")
# We first try running the script.
try:
game.context().run()
@@ -124,7 +149,15 @@ def run(restart=False):
# We get this when the context has changed, and so we go and
# start running from the new context.
except game.RestartException, e:
pass
renpy.game.contexts = e.contexts
label = e.label
if label:
if game.script.has_label(label):
game.context().call(label)
continue
except game.QuitException, e:
break
@@ -136,15 +169,48 @@ def run(restart=False):
# And, we're done.
def main(basepath):
def main(basename):
renpy.game.exception_info = 'While loading the script.'
game.basepath = basepath
renpy.config.searchpath = [ "common", basepath ]
if os.path.isdir(basename):
basepath = basename
elif os.path.isdir("game"):
basepath = "game"
elif os.path.isdir("data"):
basepath = "data"
else:
basepath = "."
game.basepath = basepath
renpy.config.searchpath = [ basepath ]
if os.path.isdir("common"):
renpy.config.searchpath.append("common")
renpy.config.archives = [ ]
for i in basename, "game", "data":
if i in renpy.config.archives:
continue
try:
renpy.loader.transfn(i + ".rpa")
renpy.config.archives.append(i)
continue
except:
continue
if renpy.game.options.profile:
renpy.config.profile = True
# Backup the configuration.
renpy.config.backup()
# Initialize archives.
renpy.loader.index_archives()
# Load the script.
game.script = renpy.script.load_script()
+71 -29
View File
@@ -143,7 +143,7 @@ def list_logical_lines(filename):
if line != "":
raise ParseError(filename, number, "is not terminated with a newline.")
raise ParseError(filename, start_number, "is not terminated with a newline (check quotes and parenthesis).")
return rv
@@ -172,13 +172,11 @@ def group_logical_lines(lines):
if l[index] == '\t':
index += 1
depth = depth + 8 - (16 % 8)
depth = depth + 8 - (depth % 8)
continue
break
# TODO: Fix to handle tabs properly. Or else update the docs to
# forbid tabs entirely.
return depth, l[depth:]
# i, min_depth -> block, new_i
@@ -235,6 +233,7 @@ class Lexer(object):
'init',
'jump',
'menu',
'onlayer',
'python',
'return',
'scene',
@@ -395,30 +394,38 @@ class Lexer(object):
self.skip_whitespace()
s = self.match(r'"([^\\"]|\\.)*"')
s = self.match(r'r?"([^\\"]|\\.)*"')
if s is None:
s = self.match(r"'([^\\']|\\.)*'")
s = self.match(r"r?'([^\\']|\\.)*'")
if s is None:
return None
if s[0] == 'r':
raw = True
s = s[1:]
else:
raw = False
# Strip off delimiters.
s = s[1:-1]
# Collapse runs of whitespace into single spaces.
s = re.sub(r'\s+', ' ', s)
if not raw:
# Expand out backslash escapes.
def unescape(m):
c = m.group(1)
# Collapse runs of whitespace into single spaces.
s = re.sub(r'\s+', ' ', s)
if c == 'n':
return '\n'
# Expand out backslash escapes.
def unescape(m):
c = m.group(1)
return c
if c == 'n':
return '\n'
s = re.sub(r'\\(.)', unescape, s)
return c
s = re.sub(r'\\(.)', unescape, s)
return s
@@ -441,7 +448,7 @@ class Lexer(object):
oldpos = self.pos
rv = self.match(r'[a-zA-Z_]\w*')
rv = self.match(ur'[a-zA-Z_\u00a0-\ufffd][0-9a-zA-Z_\u00a0-\ufffd]*')
if rv in self.keywords:
self.pos = oldpos
@@ -467,12 +474,17 @@ class Lexer(object):
if c == 'u':
self.pos += 1
if self.eol():
if self.pos == len(self.text):
self.pos -= 1
return False
c = self.text[self.pos]
if c not in ('"', "'"):
if c not in ('"', "'"):
self.pos -= 1
return False
elif c not in ('"', "'"):
return False
delim = c
@@ -514,7 +526,7 @@ class Lexer(object):
while self.match(r'\.'):
n = self.name()
if not n:
self.parse_error('expecting name.')
self.error('expecting name.')
rv += "." + n
@@ -656,7 +668,7 @@ class Lexer(object):
def get_location(self):
"""
Returns a (filename, line number) tuple representing the current
Returns a (filename, line number, file version) tuple representing the current
physical location of the start of the current logical line.
"""
@@ -699,20 +711,37 @@ class Lexer(object):
def python_block(self):
"""
Returns the subblock of this code, and subblocks of that
subblock, as indented python code.
subblock, as indented python code. This tries to insert
whitespace to ensure line numbers match up.
"""
rv = [ ]
# Something to hold the expected line number.
class Object(object):
pass
o = Object()
o.line = self.number
def process(block, indent):
for fn, ln, text, subblock in block:
rv.append(indent + text + '\n')
if o.line > ln:
assert False
while o.line < ln:
rv.append(indent + '\n')
o.line += 1
linetext = indent + text + '\n'
rv.append(linetext)
o.line += linetext.count('\n')
process(subblock, indent + ' ')
process(self.subblock, '')
return ''.join(rv)
def parse_image_name(l):
@@ -758,23 +787,29 @@ def parse_image_specifier(l):
"""
This parses an image specifier. An image specifier looks like:
<image name> [ "at" <at list> ]
<image name> [ "onlayer" <layer> ] [ "at" <at list> ]
The general idea is that the image name tells us which image to show,
and the at list tells us where it should be shown.
This returns a tuple containing a tuple of strings, and a list
of expressions (which may be empty if the at part is omitted).
This returns a tuple containing a tuple of strings, a list
of expressions (which may be empty if the at part is omitted), and
a layer name.
"""
image_name = parse_image_name(l)
if l.keyword('onlayer'):
layer = l.require(l.name)
else:
layer = 'master'
if l.keyword('at'):
at_list = parse_simple_expression_list(l)
else:
at_list = [ ]
return image_name, at_list
return image_name, at_list, layer
def parse_with(l, node):
"""
@@ -815,6 +850,8 @@ def parse_menu(l, loc):
l.expect_noblock('with clause')
l.advance()
continue
if l.keyword('set'):
set = l.require(l.simple_expression)
l.expect_eol()
@@ -1009,13 +1046,18 @@ def parse_statement(l):
if l.keyword('scene'):
l.expect_noblock('scene statement')
if l.keyword('onlayer'):
layer = l.require(l.name)
else:
layer = "master"
# Empty.
if l.eol():
l.advance()
return ast.Scene(loc, None)
return ast.Scene(loc, None, layer)
imspec = parse_image_specifier(l)
rv = parse_with(l, ast.Scene(loc, imspec))
rv = parse_with(l, ast.Scene(loc, imspec, imspec[2]))
l.expect_eol()
l.advance()
+89 -39
View File
@@ -1,4 +1,3 @@
# This file contains code that handles the execution of python code
# contained within the script file. It also handles rolling back the
# game state to some time in the past.
@@ -6,7 +5,7 @@
from compiler import parse
from compiler.pycodegen import ModuleCodeGenerator, ExpressionCodeGenerator
from compiler.misc import set_filename
# from compiler.misc import set_filename
import compiler.ast as ast
import marshal
@@ -35,7 +34,7 @@ def reached(obj, path, reachable):
try:
# Treat as fields, indexed by strings.
for k, v in vars(obj.iteritems()):
for k, v in vars(obj).iteritems():
reached(v, path + "." + k, reachable)
except:
pass
@@ -62,7 +61,7 @@ def reached(obj, path, reachable):
def reached_vars(store, reachable):
"""
Marks everything reachable from the variables in the store
as reachable.
or from the context info objects as reachable.
@param store: A map from variable name to variable value.
@param reachable: A dictionary mapping reached object ids to
@@ -72,7 +71,9 @@ def reached_vars(store, reachable):
for k, v in store.iteritems():
reached(v, k, reachable)
for c in renpy.game.contexts:
reached(c.info, "#context", reachable)
##### Code that replaces literals will calls to magic constructors.
@@ -116,33 +117,56 @@ def recursively_replace(o, func):
return o
def py_compile(source, mode):
def set_filename(filename, offset, tree):
"""Set the filename attribute to filename on every node in tree"""
worklist = [tree]
while worklist:
node = worklist.pop(0)
node.filename = filename
lineno = getattr(node, 'lineno', None)
if lineno is not None:
node.lineno = lineno + offset
worklist.extend(node.getChildNodes())
def py_compile(source, mode, filename='<none>', lineno=1):
"""
Compiles the given source code using the supplied codegenerator.
Lists, List Comprehensions, and Dictionaries are wrapped when
appropriate.
@param source: The sourccode, as a string.
@param mode: 'exec' or 'eval'.
@param filename: The filename that the source code is taken from.
@param lineno: The line number of the first line of the source code.
"""
source = source.encode('raw_unicode_escape')
tree = parse(source, mode)
recursively_replace(tree, wrap_node)
if mode == 'exec':
set_filename("<none>", tree)
set_filename(filename, lineno - 1, tree)
cg = ModuleCodeGenerator(tree)
else:
set_filename("<none>", tree)
set_filename(filename, lineno - 1, tree)
cg = ExpressionCodeGenerator(tree)
return cg.getCode()
def py_compile_exec_bytecode(source):
code = py_compile(source, 'exec')
def py_compile_exec_bytecode(source, **kwargs):
code = py_compile(source, 'exec', **kwargs)
return marshal.dumps(code)
def py_compile_eval_bytecode(source):
def py_compile_eval_bytecode(source, **kwargs):
source = source.strip()
code = py_compile(source, 'exec')
code = py_compile(source, 'eval', **kwargs)
return marshal.dumps(code)
@@ -158,8 +182,7 @@ def mutator(method):
if id(self) not in mutated:
mutated[id(self)] = ( weakref.ref(self), self.get_rollback())
return method(self, *args, **kwargs)
return do_mutation
@@ -211,10 +234,10 @@ class RevertableDict(dict):
class RevertableObject(object):
def __setattr__(self, attr, value):
self.__dict__[attr] = value
object.__setattr__(self, attr, value)
def __delattr__(self, attr):
del self.__dict__[attr]
object.__delattr__(self, attr)
__setattr__ = mutator(__setattr__)
__delattr__ = mutator(__delattr__)
@@ -294,7 +317,6 @@ class Rollback(renpy.object.Object):
@ivar random: A list of random numbers that were generated during the
execution of this element.
"""
def __init__(self):
@@ -356,10 +378,10 @@ class Rollback(renpy.object.Object):
for t in self.store:
if len(t) == 2:
k, v = t
renpy.game.store[k] = v
vars(renpy.store)[k] = v
else:
k, = t
del renpy.game.store[k]
del vars(renpy.store)[k]
renpy.game.contexts = [ self.context ]
rng.pushback(self.random)
@@ -415,6 +437,12 @@ class RollbackLog(renpy.object.Object):
state needs to be saved for rollbacking.
"""
# If the transient scene list is not empty, then we do
# not begin a new rollback, as the TSL will be purged
# after a rollback is complete.
if not renpy.game.contexts[0].scene_lists.transient_is_empty():
return
# If the log is too long, try pruning it to a label.
if len(self.log) > renpy.config.rollback_length:
rb = self.log[-renpy.config.rollback_length]
@@ -428,7 +456,7 @@ class RollbackLog(renpy.object.Object):
self.log.append(self.current)
self.mutated = { }
self.old_store = renpy.game.store.copy()
self.old_store = vars(renpy.store).copy()
def complete(self):
"""
@@ -439,7 +467,7 @@ class RollbackLog(renpy.object.Object):
occurs.
"""
new_store = renpy.game.store
new_store = vars(renpy.store)
store = [ ]
@@ -484,9 +512,11 @@ class RollbackLog(renpy.object.Object):
rv = { }
store = vars(renpy.store)
for k in self.ever_been_changed.keys():
if k in renpy.game.store:
rv[k] = renpy.game.store[k]
if k in store:
rv[k] = store[k]
return rv
@@ -523,7 +553,15 @@ class RollbackLog(renpy.object.Object):
self.current.checkpoint = True
def rollback(self, checkpoints, force=False):
def block(self):
"""
Called to indicate that the user should not be able to rollback
through this checkpoint.
"""
self.rollback_limit = 0
def rollback(self, checkpoints, force=False, label=None):
"""
This rolls the system back to the first valid rollback point
after having rolled back past the specified number of checkpoints.
@@ -534,6 +572,9 @@ class RollbackLog(renpy.object.Object):
force makes us throw an exception if we can't find a place to stop
rolling back, otherwise if we run out of log this call has no
effect.
@param label: The label that is jumped to in the game script
after rollback has finished, if it exists.
"""
# If we have exceeded the rollback limit, and don't have force,
@@ -581,7 +622,7 @@ class RollbackLog(renpy.object.Object):
rng.reset()
# Restart the game with the new state.
raise renpy.game.RestartException()
raise renpy.game.RestartException(renpy.game.contexts[:], label)
def freeze(self):
"""
@@ -606,59 +647,68 @@ class RollbackLog(renpy.object.Object):
# We need to do this to counteract the effects of self.purge_unreachable
self.current.purged = False
def unfreeze(self):
def unfreeze(self, label=None):
"""
Used to unfreeze the game state after a load of this log
object. This call will always throw an exception. If we're
lucky, it's the one that indicates load was successful.
@param label: The label that is jumped to in the game script
after rollback has finished, if it exists.
"""
# Set us up as the game log.
renpy.game.log = self
# Restore the store.
renpy.game.store.clear()
renpy.game.store.update(renpy.game.clean_store)
store = vars(renpy.store)
store.clear()
store.update(renpy.game.clean_store)
for k in self.ever_been_changed:
if k in renpy.game.store:
del renpy.game.store[k]
if k in store:
del store[k]
renpy.game.store.update(self.frozen_roots)
store.update(self.frozen_roots)
self.frozen_roots = None
# Now, rollback to an acceptable point.
self.rollback(0, force=True)
self.rollback(0, force=True, label=label)
# We never make it this far.
def py_exec_bytecode(bytecode, hide=False):
store = vars(renpy.store)
if hide:
locals = { }
else:
locals = renpy.game.store
locals = store
exec marshal.loads(bytecode) in renpy.game.store, locals
exec marshal.loads(bytecode) in store, locals
def py_exec(source, hide=False):
def py_exec(source, hide=False, store=None):
if store is None:
store = vars(renpy.store)
if hide:
locals = { }
else:
locals = renpy.game.store
locals = store
exec py_compile(source, 'exec') in renpy.game.store, locals
exec py_compile(source, 'exec') in store, locals
def py_eval_bytecode(bytecode):
return eval(marshal.loads(bytecode), renpy.game.store)
return eval(marshal.loads(bytecode), vars(renpy.store))
def py_eval(source):
source = source.strip()
return eval(py_compile(source, 'eval'),
renpy.game.store)
vars(renpy.store))
+129 -25
View File
@@ -5,6 +5,7 @@ import renpy
import os.path
import os
import imp
from pickle import loads, dumps
@@ -33,6 +34,10 @@ class Script(object):
@ivar initcode: A list of priority, Node tuples that should be
executed in ascending priority order at init time.
@ivar all_stmts: A list of all statements, that have been found
in every file. Useful for lint, but tossed if lint is not performed
to save memory.
"""
def __init__(self, node_callback=None):
@@ -41,22 +46,24 @@ class Script(object):
walking the various ASTs to initialize this Script object.
"""
self.key = None
self.namemap = { }
self.initcode = [ ]
self.all_stmts = [ ]
# A list of all files in the search directories.
dirlist = [ ]
dirlist = renpy.loader.listdirfiles()
for dirname in renpy.config.searchpath:
for fn in os.listdir(dirname):
dirlist.append(dirname + "/" + fn)
# for dirname in renpy.config.searchpath:
# for fn in os.listdir(dirname):
# dirlist.append(dirname + "/" + fn)
# Files to ensure (because they are the alts of files that
# have been processed.)
ignore = [ ]
for fn in dirlist:
for dir, fn in dirlist:
if not (fn.endswith('.rpyc') or fn.endswith('.rpy')):
continue
@@ -69,23 +76,27 @@ class Script(object):
else:
alt = fn + 'c'
if os.path.exists(alt):
fntime = os.stat(fn).st_mtime
alttime = os.stat(alt).st_mtime
if dir:
fullfn = dir + "/" + fn
fullalt = dir + "/" + alt
if alttime > fntime:
continue
if os.path.exists(fullfn) and os.path.exists(fullalt):
fntime = os.stat(fullfn).st_mtime
alttime = os.stat(fullalt).st_mtime
if alttime > fntime:
continue
ignore.append(alt)
# print "Loading", fn
if self.load_file(fn, node_callback):
if self.load_file(dir, fn, node_callback):
continue
print "Couldn't load %s, trying %s instead." % (fn, alt)
if self.load_file(alt, node_callback):
if self.load_file(dir, alt, node_callback):
continue
raise Exception("Could not load %s or %s." % (fn, alt))
@@ -99,32 +110,61 @@ class Script(object):
self.initcode = [ (prio, code) for prio, index, code in initcode ]
# Ensure that all of the python code found in the script has been
# compiled into bytecode.
self.update_bytecode()
# Do some generic init here.
def load_file(self, fn, node_callback):
def load_file(self, dir, fn, node_callback):
if fn.endswith(".rpy"):
stmts = renpy.parser.parse(fn)
f = file(fn + "c", "wb")
f.write(dumps((script_version, stmts)).encode('zlib'))
f.close()
elif fn.endswith(".rpyc"):
f = file(fn, "rb")
if not dir:
raise Exception("Cannot load rpy file %s from inside an archive." % fn)
stmts = renpy.parser.parse(dir + "/" + fn)
data = { }
data['version'] = script_version
data['key'] = renpy.game.options.lock or 'unlocked'
try:
version, stmts = loads(f.read().decode('zlib'))
except ValueError:
return False
f = file(dir + "/" + fn + "c", "wb")
f.write(dumps((data, stmts), 2).encode('zlib'))
f.close()
except:
pass
if version != script_version:
elif fn.endswith(".rpyc"):
# When locking, regenerate all files.
if renpy.game.options.lock:
return False
f = renpy.loader.load(fn)
try:
data, stmts = loads(f.read().decode('zlib'))
except:
return False
if not isinstance(data, dict):
return False
if data['version'] != script_version:
return False
f.close()
else:
return False
if self.key is None:
self.key = data['key']
elif self.key != data['key']:
raise Exception( fn + " does not share a key with at least one .rpyc file. To fix, delete all .rpyc files, or rerun Ren'Py with the --lock option.")
# All of the statements found in file, regardless of nesting
# depth.
all_stmts = [ ]
@@ -165,8 +205,72 @@ class Script(object):
if init:
self.initcode.append(init)
self.all_stmts.extend(all_stmts)
return True
def update_bytecode(self):
"""
Updates the bytecode for all the bytecode objects in renpy.PyCode.extent.
"""
VERSION = 1
oldcache = { }
newcache = { }
magic = imp.get_magic()
dirty = False
# Load the oldcache.
try:
version, cache = loads(renpy.loader.load("bytecode.rpyb").read().decode("zlib"))
if version == VERSION:
oldcache = cache
except:
pass
# Update all of the PyCode objects in the system with the loaded
# bytecode.
for i in renpy.ast.PyCode.extent:
codes = oldcache.get(i.location, { })
if magic in codes:
code = codes[magic]
else:
dirty = True
old_ei = renpy.game.exception_info
renpy.game.exception_info = "While compiling python block starting at line %d of %s." % (i.location[1], i.location[0])
if i.mode == 'exec':
code = renpy.python.py_compile_exec_bytecode(i.source, filename=i.location[0], lineno=i.location[1])
elif i.mode == 'eval':
code = renpy.python.py_compile_eval_bytecode(i.source, filename=i.location[0], lineno=i.location[1])
renpy.game.exception_info = old_ei
codes[magic] = code
i.source = None
i.bytecode = code
newcache[i.location] = codes
if dirty:
try:
data = (VERSION, newcache)
f = file(os.path.join(renpy.config.searchpath[0], "bytecode.rpyb"), "w")
f.write(dumps(data, 2).encode("zlib"))
f.close()
except:
pass
renpy.ast.PyCode.extent = None
def lookup(self, label):
"""
Looks up the given label in the game. If the label is not found,
+124 -46
View File
@@ -6,6 +6,8 @@
import renpy
import renpy.ui as ui
import renpy.display.im as im
import renpy.display.anim as anim
from renpy.python import RevertableList as __renpy__list__
list = __renpy__list__
@@ -19,17 +21,25 @@ from renpy.python import RevertableObject as object
config = renpy.config
Image = renpy.display.image.Image
ImageReference = renpy.display.image.ImageReference
Solid = renpy.display.image.Solid
Frame = renpy.display.image.Frame
Animation = renpy.display.image.Animation
Null = renpy.display.layout.Null
LiveComposite = renpy.display.layout.LiveComposite
# Animation = renpy.display.image.Animation
Animation = anim.Animation
Movie = renpy.display.video.Movie
Position = renpy.curry.curry(renpy.display.layout.Position)
Pan = renpy.curry.curry(renpy.display.layout.Pan)
Move = renpy.curry.curry(renpy.display.layout.Move)
Motion = renpy.curry.curry(renpy.display.layout.Motion)
Fade = renpy.curry.curry(renpy.display.transition.Fade)
Dissolve = renpy.curry.curry(renpy.display.transition.Dissolve)
ImageDissolve = renpy.curry.curry(renpy.display.transition.ImageDissolve)
CropMove = renpy.curry.curry(renpy.display.transition.CropMove)
Pixellate = renpy.curry.curry(renpy.display.transition.Pixellate)
MoveTransition = renpy.curry.curry(renpy.display.transition.MoveTransition)
def _return(v):
"""
@@ -42,8 +52,6 @@ def _return(v):
_return = renpy.curry.curry(_return)
# Note that this is really a RevertableObject.
# TODO: Move this someplace saner. Like perhaps to .exports. But
# be sure to change the base class after the move!
class Character(object):
"""
@@ -56,10 +64,20 @@ class Character(object):
import renpy.config as config
# Properties beginning with what or window that are treated
# specially.
special_properties = [
'what_prefix',
'what_suffix',
]
def __init__(self, name,
who_style='say_label',
what_style='say_dialogue',
window_style='say_window',
function = renpy.exports.display_say,
condition=None,
dynamic=False,
**properties):
"""
@param name: The name of the character, as shown to the user.
@@ -74,12 +92,6 @@ class Character(object):
@param window_style: The name of the style of the window
containing all the dialogue.
@param properties: Additional style properties, that are
applied to the label containing the character's name.
In addition to the parameters given above, there are also a
few other keyword parameters:
@param who_prefix: A prefix that is prepended to the name.
@param who_suffix: A suffix that is appended to the name. (Defaults to ':')
@@ -88,10 +100,46 @@ class Character(object):
@param what_suffix: A suffix that is appended to the text body.
@param function: The function that is called to actually display
this dialogue. This should either be renpy.display_say, or a function
with the same signature as it.
@param condition: A string containing a python expression, or
None. If not None, the condition is evaluated when each line
of dialogue is said. If it evaluates to False, the dialogue is
not shown to the user.
@param interact: If True (the default), then each line said
through this character causes an interaction. If False, then
the window is added to the screen, but control immediately
proceeds. You'll need to call ui.interact yourself to show it.
@param properties: Additional style properties, that are
applied to the label containing the character's name.
@param dynamic: If true, the name is interpreted as a python
expression, which is evaluated to get the name that will be
used by the rest of the code.
@param image: If true, the name is considered to be the name
of an image, which is rendered in place of the who label.
@param ctc: If present, this is interpreted as a widget that
is displayed when all text is shown to the user, prompting the
user to click to continue. Animation or anim.Blink is a good
choice for this sort of widget, as is Image.
@param ctc_position: If "nestled", the ctc widget is
displayed nestled in with the end of the text. If
"fixed", the ctc widget is displayed directly on the screen,
with its various position properties determining where it is
actually shown.
In addition, Character objects also take properties. If a
property is prefixed with window_, it is applied to the
window. If prefixed with what_, it is applied to the text
being spoken. Unprefixed properties are applied to the who
label, the name of the character speaking.
"""
self.name = name
@@ -99,54 +147,84 @@ class Character(object):
self.what_style = what_style
self.window_style = window_style
self.properties = properties
self.what_properties = { }
self.window_properties = { }
self.function = function
self.condition = condition
self.dynamic = dynamic
for k in list(self.properties):
if k in self.special_properties:
continue
if k.startswith("what_"):
self.what_properties[k[len("what_"):]] = self.properties[k]
del self.properties[k]
continue
if k.startswith("window_"):
self.window_properties[k[len("window_"):]] = self.properties[k]
del self.properties[k]
continue
def check_condition(self):
"""
Returns true if we should show this line of dialogue.
"""
if self.condition is None:
return True
import renpy.python as python
return python.py_eval(self.condition)
def store_readback(self, who, what):
"""
This is called when a say occurs, to store the information
about what is said into the readback buffers.
"""
return
def __call__(self, what, interact=True):
renpy.display_say(self.name, what,
who_style=self.who_style,
what_style=self.what_style,
window_style=self.window_style,
interact=interact,
**self.properties)
class DynamicCharacter(object):
if not self.check_condition():
return
name = self.name
if self.dynamic:
import renpy.python as python
name = python.py_eval(name)
self.function(name, what,
who_style=self.who_style,
what_style=self.what_style,
window_style=self.window_style,
interact=interact,
what_properties=self.what_properties,
window_properties=self.window_properties,
**self.properties)
self.store_readback(name, what)
def DynamicCharacter(name_expr, **properties):
"""
A DynamicCharacter is similar to a Character, except that instead
of having a fixed name, it has an expression that is evaluated to
produce a name before each line of dialogue is displayed. This allows
one to have a character with a name that is read from the user, as
may be the case for the POV character.
This is now exactly the same as constructing a character with
dynamic=True.
"""
import renpy.config as config
def __init__(self, name_expr,
who_style='say_label',
what_style='say_dialogue',
window_style='say_window',
**properties):
"""
@param name_expr: An expression that, when evaluated, should yield
the name of the character, as a string.
All other parameters are as for Character.
"""
self.name_expr = name_expr
self.who_style = who_style
self.what_style = what_style
self.window_style = window_style
self.properties = properties
def __call__(self, what, interact=True):
import renpy.python as python
renpy.display_say(python.py_eval(self.name_expr),
what,
who_style=self.who_style,
what_style=self.what_style,
window_style=self.window_style,
interact=interact,
**self.properties)
return Character(name_expr, dynamic=True, **properties)
# The color function. (Moved, since text needs it, too.)
color = renpy.display.text.color
+196 -114
View File
@@ -1,158 +1,240 @@
import renpy
# A list of style prefixes we care about, including no prefix.
prefixes = [ 'hover_', 'idle_', 'activate_', '' ]
prefixes = [ 'hover_', 'idle_', 'activate_' , 'insensitive_' ]
def startswith_prefix(s):
for i in prefixes:
if s.startswith(i):
return True
else:
return False
substitutes = dict(
xmargin = [ 'left_margin', 'right_margin' ],
ymargin = [ 'top_margin', 'bottom_margin' ],
xpadding = [ 'left_padding', 'right_padding' ],
ypadding = [ 'top_padding', 'bottom_padding' ],
)
# Expand out substitutes:
for k in substitutes.keys():
for p in prefixes:
substitutes[p + k] = [ p + i for i in substitutes[k] ]
# A map from a style name to the style associated with that name.
style_map = { }
# True if we have expanded all of the style caches, False otherwise.
styles_built = False
# A list of styles that are pending expansion.
styles_pending = [ ]
# A list of created styles giving the style's name, parent, and description.
style_info = [ ]
def reset():
"""
This resets all of the data structures associated with style
management.
"""
global style_map
global styles_built
global styles_pending
global style_info
style_map = { }
styles_built = False
styles_pending = [ ]
style_info = [ ]
class StyleManager(object):
"""
This is the singleton object that is exported into the store as
'style', and to everyone as renpy.game.style. It's responsible for
mapping style names to styles.
This is the singleton object that is exported into the store
as style
"""
def __init__(self):
self._style_list = [ ]
def __getattr__(self, name):
try:
return style_map[name]
except:
raise Exception('The style %s does not exist.' % name)
def create(self, name, parent='default', description=''):
def create(self, name, parent, description=None):
"""
Creates a new style with the given parent and description, and
adds it to the StyleManager.
Creates a new style.
@param name: The name of the new style, as a string.
@param parent: The parent of the new style, as a string. This
is either 'default' or something more specific.
@param description: A description of the style, for
documentation purposes.
"""
if parent and not hasattr(self, parent):
raise Exception("Style '%s' has non-existent parent '%s'." % (name, parent))
style_map[name] = Style(parent, { })
if description:
style_info.append((name, parent, description))
def exists(self, name):
"""
This determines if the named style exists.
"""
s = Style(parent, defer=True)
s.name = name
s.parent = parent
s.description = description
return name in style_map
setattr(self, name, s)
# This expands out property names and adds them to style's property
# dictionary.
def compute_properties(style, properties):
self._style_list.append(s)
props = { }
def _build_style_caches(self):
# Expand substitutions.
for k, v in properties.items():
if k in substitutes:
for j in substitutes[k]:
props[j] = v
else:
props[k] = v
for i in self._style_list:
i.build_cache()
# Expand prefixes, where necessary.
for k, v in props.items():
if startswith_prefix(k):
continue
def _write_docs(self, filename):
del props[k]
f = file(filename, "w")
for p in prefixes:
props[p + k] = v
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)
style.properties.update(props)
style.cache.update(props)
f.write(re.sub(r'\s+', ' ', s.description))
f.write("</renpy_style>\n\n")
# This builds the style. If recurse is True, this also builds the
# parent style.
def build_style(style, recurse=False):
f.close()
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)
style.cache.update(parent.cache)
style.cache.update(style.properties)
# This builds all pending styles, recursing to ensure that they are built
# in the right order.
def build_styles():
global styles_pending
global styles_built
for s in styles_pending:
build_style(s, True)
styles_pending = None
styles_built = True
class Style(object):
"""
This is an individual style object, which can have properties
looked up on it or its parent. Call the constructor of this
to create an anonymous style.
"""
def __getstate__(self):
return dict(properties=self.properties,
prefix=self.prefix,
parent=self.parent)
return dict(prefix = self.prefix,
parent = self.parent,
cache = { },
properties = self.properties)
def __setstate__(self, state):
self.__dict__.update(state)
vars(self).update(state)
build_style(self)
# This should always work, as only one layer of these styles will
# be serialized.
def __init__(self, parent, properties):
self.build_cache()
fields = dict(
prefix = 'insensitive_',
parent = parent,
cache = { },
properties = { },
)
def __setattr__(self, key, value):
if key in substitutes:
for i in substitutes[key]:
self.__setattr__(i, value)
return
for prefix in prefixes:
prefkey = prefix + key
self.properties[prefkey] = value
self.cache[prefkey] = value
def __getattr__(self, key):
cache = self.cache
try:
return cache[self.prefix + key]
except KeyError:
raise AttributeError("Style property '%s' not found." % key)
def __delattr__(self, key):
del self.properties[key]
del self.cache[key]
def lookup(self, key, prefix):
cache = self.cache
try:
return cache.get(prefix + key, cache[key])
except KeyError:
raise AttributeError("Style property '%s' not found." % key)
vars(self).update(fields)
if styles_built:
build_style(self)
else:
styles_pending.append(self)
compute_properties(self, properties)
def set_prefix(self, prefix):
vars(self)["prefix"] = prefix
self.prefix = prefix
def build_cache(self):
vars(self)["cache"] = { }
self.cache = { }
if self.parent:
self.cache.update(getattr(renpy.game.style, self.parent).cache)
self.cache.update(self.properties)
def __init__(self, parent, properties=None, defer=False):
if parent and not hasattr(renpy.game.style, parent):
raise Exception("Style '%s' is not known." % parent)
if not properties:
properties = { }
for k in properties.keys():
for p in prefixes:
if p + k not in properties:
properties[p + k] = properties[k]
vars(self)["parent"] = parent
vars(self)["prefix"] = ''
vars(self)["properties"] = properties
vars(self)["cache"] = { }
if not defer:
self.build_cache()
def __getattr__(self, name):
return self.cache[self.prefix + name]
def __setattr__(self, name, value):
compute_properties(self, { name : value } )
def write_docs(filename):
f = file(filename, "w")
import re
for name, parent, description in style_info:
f.write(' <renpy_style name="%s">' % name)
if parent:
f.write('<renpy_style_inherits>%s</renpy_style_inherits>' % parent)
f.write(re.sub(r'\s+', ' ', description))
f.write("</renpy_style>\n\n")
f.close()
def write_hierarchy(fn):
f = file(fn, "w")
kids = { }
for name, parent, description in style_info:
kids.setdefault(parent, []).append(name)
def do(name):
f.write('<li> <a href="#%s">%s</a>\n' % (name, name))
if name in kids:
f.write('<ul>\n')
l = kids[name]
l.sort()
for n in l:
do(n)
f.write('</ul>\n')
f.write('</li>')
do('default')
f.close()

Some files were not shown because too many files have changed in this diff Show More