Compare commits
141 Commits
4.8.7
...
renpy-5.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5eb0185d03 | |||
| df4154c854 | |||
| cbe66f8353 | |||
| 143db03196 | |||
| e879687a16 | |||
| fcb3286df9 | |||
| ba23df21ab | |||
| 5f6f3d8e70 | |||
| 9d7cd97fcc | |||
| a90491b678 | |||
| ae8fc4b859 | |||
| c4605e915d | |||
| e809b24663 | |||
| ebf669e8af | |||
| 3b7b40b85a | |||
| 4225f92095 | |||
| 31241ca637 | |||
| 6e06e155dd | |||
| c7059bc3cb | |||
| f6c4f7fda1 | |||
| a6f04a6741 | |||
| 12e4028979 | |||
| f06e4ab954 | |||
| be98f91907 | |||
| ada05d2eb2 | |||
| 1d355ceb0b | |||
| e26d93a3fa | |||
| f622a4f7b6 | |||
| 7cb63a3db9 | |||
| 42b11c2e74 | |||
| f4d5ef1425 | |||
| 8109a8dbfe | |||
| c8c1b93894 | |||
| 597be879e4 | |||
| 3d1a2e4d96 | |||
| 00401fd0e9 | |||
| 6eef431161 | |||
| f3b31c8025 | |||
| df4fa900e9 | |||
| fbd92b66b0 | |||
| a29fd3aa8b | |||
| c9af317092 | |||
| 91fdd68ec1 | |||
| c8b90b04e2 | |||
| 8c85390aaf | |||
| b77f295921 | |||
| c0e3dfc99b | |||
| f5c6ea530d | |||
| ce7ccc2f87 | |||
| a4d1670378 | |||
| c143c0c479 | |||
| f7cb4eae9c | |||
| bb35ff5658 | |||
| b479fd4a06 | |||
| c9c0ac5809 | |||
| 04cbf7620c | |||
| 6770b268fc | |||
| 60f988dc6c | |||
| a8e8dc4f49 | |||
| 622db83c50 | |||
| d17b68a17c | |||
| 8d0465e72e | |||
| 3dc117ed96 | |||
| 111b7e440d | |||
| 3e80c3acbb | |||
| 56b6135b8e | |||
| 24ec412f1e | |||
| cd987bdb6d | |||
| d6f3b7aab0 | |||
| f2f9db46e9 | |||
| 57c38e0363 | |||
| 6d055e775a | |||
| 48033b3b1e | |||
| 9d57e0dad1 | |||
| bed546e6e6 | |||
| 94a9422ddc | |||
| 8f7bd1be77 | |||
| e863d4a645 | |||
| 8d1cf70edc | |||
| eadb1c6385 | |||
| ad51d6a674 | |||
| 6880da07b7 | |||
| 35408d81ea | |||
| d75eb13658 | |||
| dc3a01aae4 | |||
| 1792f7a7af | |||
| eb82c47109 | |||
| 229748a68e | |||
| d5d0d12697 | |||
| 6bce3b0e9a | |||
| 349f2479d3 | |||
| 75d2f81973 | |||
| eadbdebcb0 | |||
| 196e3701e9 | |||
| a2904a7618 | |||
| 3a2fc5498a | |||
| c3757fdea9 | |||
| 6ee37d4933 | |||
| 4ded3eba45 | |||
| 277c7b4d88 | |||
| 403bb65826 | |||
| 0d1946f3bc | |||
| 61381a2f12 | |||
| 168729e9d9 | |||
| 42a2d2c8c0 | |||
| 42c8f4339c | |||
| 8c7ca92e54 | |||
| 0919f556b4 | |||
| 50bb65973b | |||
| 660fa3114e | |||
| 9ebad42f5a | |||
| cd8acdcd8a | |||
| 9bbecc250d | |||
| 518a10cae0 | |||
| c1206e7fbd | |||
| daaba9f294 | |||
| 2e864c971d | |||
| 5b9494ebcc | |||
| d228af6ffd | |||
| 454f103818 | |||
| e26989c288 | |||
| 5c06b96c18 | |||
| d3bfda1a11 | |||
| 0308d48953 | |||
| 9525db2212 | |||
| 7afca9d5d5 | |||
| ccca92c88a | |||
| aa3ff2f012 | |||
| 46e8fdd092 | |||
| f2963d33c3 | |||
| 0c96d52b54 | |||
| 74678d5cef | |||
| 3dd6959fe1 | |||
| 7b6ec9db5b | |||
| 404936dae6 | |||
| f7de1b0eb0 | |||
| 8614e6406d | |||
| 0c6e1893e2 | |||
| f1bec25291 | |||
| 429b34b80f | |||
| 5a28ecd5f5 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
|
||||
@@ -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,
|
||||
} },
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
After Width: | Height: | Size: 693 B |
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 606 B |
@@ -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)
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 505 B |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 60 KiB |
@@ -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()
|
||||
|
||||
@@ -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.)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
@@ -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")'))
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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/.
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
pyrexc _renpy.pyx && python setup.py build_ext -i
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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, ¤tPos);
|
||||
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, ¤tPos);
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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 */
|
||||
@@ -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)¤tsong->MidiStreamHdr))
|
||||
BlockOut(currentsong);
|
||||
break;
|
||||
case MOM_POSITIONCB:
|
||||
if ((currentsong->MusicLoaded) && ((DWORD)dwParam1 == (DWORD)¤tsong->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 */
|
||||
@@ -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))
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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. ***"
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||