This commit is contained in:
tom
2005-01-16 19:10:17 +00:00
parent 06ea834b53
commit 1e6e1cc79d
17 changed files with 460 additions and 61 deletions
+57
View File
@@ -1,3 +1,60 @@
New in 4.4.2
------------
Improved the configurability of the say and menu statements, also
changing the way they work. Say statements without a speaker specified
are routed through the narrator character, while all menu statements
are routed through the menu function. Customizing these functions can
customize how Ren'Py interacts with the user, making it a much more
flexible engine.
As part of this, we changed the way character objects work. They are
now called directly to display dialogue, rather than having the say
method called on them.
If your game uses variables named "narrator" or "menu", you will need
to rename them.
Changed the way spaces are handled by the text widget. Specifically,
they are no longer merged, so it's possible to include spaces in
dialogue and thoughts. Please note that the parser still merges
adjacent spaces, unless you escape them with a backslash.
Added a new widget, ui.sizer, that can shrink the amount of space
allocated to its child.
Added two new text properties, first_indent and rest_indent. These
properties control how many pixels of indentation to put before each
line of text.
Added a function, color, that can translate a hex triple or quadruple
into a Ren'Py color.
New in 4.4.1
------------
4.4.1 is being released to test some sound fixes, and so it isn't
really isn't that complete or well-tested.
Fixed some longstanding bugs with midi music on Windows. First of all,
we now ship with the 1.2.6 version of SDL_mixer, which fixes a bug in
1.2.5. That bug caused panning to go to the hard left. We also try to
read the windows midi device volumes, and set our volume to match the
first one we can read. This should prevent really loud midi music from
happening.
Also, increased the size of the sound buffer to 4096 samples, to
prevent skipping.
Some new features:
Now, adding interact=False to a Character object will prevent an
interaction from occuring.
Implemented functions to get the amount of time elapsed during the
game, renpy.clear_game_runtime() and renpy.get_game_runtime().
New in 4.4
----------
+5 -1
View File
@@ -28,6 +28,9 @@ init -250:
green = (0, 128, 0, 255)
# Magic.
style.default.enable_hover = True
# Text properties.
style.default.font = "Vera.ttf"
style.default.antialias = True
@@ -38,7 +41,8 @@ init -250:
style.default.minwidth = 0
style.default.textalign = 0
style.default.text_y_fudge = 0
style.default.enable_hover = True
style.default.first_indent = 0
style.default.rest_indent = 0
# Change this if you're not using Vera 22.
if renpy.windows():
+7
View File
@@ -88,6 +88,9 @@ label start:
# that we won the date.
$ date = False
# Clear the game runtime timer, so it doesn't reflect time spent
# sitting at the main menu.
$ renpy.clear_game_runtime()
# Start some music playing in the background.
$ renpy.music_start('sun-flower-slow-drag.mid')
@@ -594,6 +597,10 @@ label ending:
who encouraged him.'
"We can't wait to see what you do with this. Good luck!"
$ minutes, seconds = divmod(int(renpy.get_game_runtime()), 60)
"It took you %(minutes)d minutes and %(seconds)d seconds to
finish this demo."
$ renpy.full_restart()
+1 -1
View File
@@ -115,7 +115,7 @@ def main():
cp("archive_images.bat")
cp("run_game.py", license=license)
cp("archiver.py", license=license)
cp("build_exe.py", license=license)
# cp("build_exe.py", license=license)
cp("add_from.py", license=license)
cp("renpy-mode.el")
+44
View File
@@ -383,6 +383,20 @@ centered "American Bishoujo presents..."
centered "The Ren'Py Demo Game"
</example>
<p>
The way this is done is that the character object is called with
the string to display. The character object doesn't actually have to
be a Character object, it can be any python callable. It's responsible
for displaying the dialogue to the user. In the case of a thought,
the contents of the narrator variable is used to display the text to
the user.
</p>
<var name="narrator" value="...">
A function that expects a single string as input, that is called to
display narration to the user.
</var>
<p>
The say statement also takes a with clause that is used to control
the transition that is used to introduce the dialogue or
@@ -513,6 +527,14 @@ menu what_to_do:
the user has chosen to go shopping.
</p>
<var name="menu" value="...">
The menu variable contains the function that is called to display
the menu to the user. By default, this variable contains a
function that passes its only argument to renpy.display_menu. You
can read the documentation of that function to find out what menu
expects as input.
</var>
<h3>Displaying Images</h3>
<p>
@@ -1726,6 +1748,12 @@ e "Pleased to meet you, %(name)s."
<!-- func renpy.windows -->
<!-- func renpy.clear_game_runtime -->
<!-- func renpy.get_game_runtime -->
<!-- func color -->
<p>
Finally, there exists an object named renpy.random. This object is
a random number generator that implements the <a
@@ -2236,6 +2264,20 @@ init:
of empty space that should be to the left of each line of
text. (To center text, it should be 0.5.)
</prop>
<prop name="first_indent">
This is used to give, in pixels, the indentation of the first line
of text in the text widget. It can be used to indent the first line of
a paragraph of text. (Not that that's a good idea on a computer
monitor, better to leave a blank line between paragraphs.)
</prop>
<prop name="rest_indent">
This is used to give, in pixels, the indentation of the second
and later lines of a text widget. It can be used to give a hanging
indent to quoted dialogue.
</prop>
<h4>Window Properties</h4>
@@ -2804,6 +2846,8 @@ automatically present in the game namespace.
<!-- func ui.button -->
<!-- func ui.sizer -->
<h4>Multiple-Child Widgets</h4>
<!-- func ui.vbox -->
+2 -2
View File
@@ -2,9 +2,9 @@
# order.
# Some version numbers and things.
version = "Ren'Py 4.4 \"Christmas Bonus\""
version = "Ren'Py 4.4.2"
script_version = 5
savegame_suffix = "-2.save"
savegame_suffix = "-3.save"
# Can be first, because has no dependencies, and may be imported
+14 -2
View File
@@ -349,7 +349,14 @@ class Button(renpy.display.layout.Window):
if map_event(ev, "button_select"):
if inside and self.clicked:
renpy.sound.play(self.style.activate_sound)
return self.clicked()
rv = self.clicked()
if rv is not None:
return rv
else:
raise renpy.display.core.IgnoreEvent()
return super(Button, self).event(ev, x, y)
@@ -475,7 +482,12 @@ class Bar(renpy.display.core.Displayable):
value = max(value, 0)
return self.clicked(value)
rv = self.clicked(value)
if rv is not None:
return rv
else:
raise renpy.display.core.IgnoreEvent()
def render(self, width, height, st):
+13 -17
View File
@@ -11,6 +11,9 @@ import cStringIO
# KEYREPEATEVENT = USEREVENT + 1
DISPLAYTIME = USEREVENT + 2
# The number of msec
DISPLAYTIME_INTERVAL = 50
class IgnoreEvent(Exception):
"""
Exception that is raised when we want to ignore an event, but
@@ -355,10 +358,9 @@ class Display(object):
def __init__(self):
# It shouldn't matter if pygame is already initialized.
pygame.init()
renpy.sound.init()
pygame.init()
self.fullscreen = renpy.game.preferences.fullscreen
fsflag = 0
@@ -629,7 +631,7 @@ class Interface(object):
# pygame.time.set_timer(KEYREPEATEVENT, renpy.config.skip_delay)
# Set up display time event.
pygame.time.set_timer(DISPLAYTIME, 50)
pygame.time.set_timer(DISPLAYTIME, DISPLAYTIME_INTERVAL)
# Clear some events.
pygame.event.clear((MOUSEMOTION, DISPLAYTIME,
@@ -734,8 +736,6 @@ class Interface(object):
if show_mouse:
self.display.draw_mouse()
# Update the playing music, if necessary.
renpy.music.restore()
# If we need to redraw again, do it if we don't have an
# event going on.
@@ -753,20 +753,16 @@ class Interface(object):
self.profile_time = time.time()
if ev.type == DISPLAYTIME:
pygame.event.clear([DISPLAYTIME])
events = 1 + len(pygame.event.get([DISPLAYTIME]))
renpy.game.context().runtime += events * DISPLAYTIME_INTERVAL
ev = pygame.event.Event(DISPLAYTIME, {},
duration=(time.time() - start_time))
# if ev.type == KEYREPEATEVENT:
# pygame.time.set_timer(KEYREPEATEVENT, 0)
# i = renpy.display.behavior.is_pressed(pygame.key.get_pressed(),
# "repeating")
# if i:
# ev = pygame.event.Event(KEYDOWN, key=i, unicode=u'')
# Update the playing music, if necessary.
renpy.music.restore()
# Handle skipping.
renpy.display.behavior.skipping(ev)
+29 -1
View File
@@ -119,7 +119,7 @@ class Container(renpy.display.core.Displayable):
class Position(Container):
"""
Controls the placement of a displayable on the screen, using
supplied positon properties. This is the non-curried form of
supplied position properties. This is the non-curried form of
Position, which should be used when the user has directly created
the displayable that will be shown on the screen.
"""
@@ -531,3 +531,31 @@ class Move(Container):
return rv
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.)
"""
def __init__(self, maxwidth, maxheight, child,
style='default', **properties):
super(Sizer, self).__init__()
self.add(child)
self.maxwidth = maxwidth
self.maxheight = maxheight
self.style = renpy.style.Style(style, properties)
def render(self, width, height, st):
if self.maxwidth:
width = min(width, self.maxwidth)
if self.maxheight:
height = min(height, self.maxheight)
return super(Sizer, self).render(width, height, st)
+27 -11
View File
@@ -107,8 +107,12 @@ class Text(renpy.display.core.Displayable):
maxwidth = 0
# The indent of the current line.
indent = self.style.first_indent
for p in pars:
words = p.split()
words = p.split(' ')
line = ""
@@ -124,12 +128,13 @@ class Text(renpy.display.core.Displayable):
lw, lh = font.size(line + " " + w)
if lw < width:
if lw + indent < width:
line += " " + w
maxwidth = max(maxwidth, lw)
else:
lines.append(line)
line = w
indent = self.style.rest_indent
lines.append(line)
@@ -168,26 +173,38 @@ class Text(renpy.display.core.Displayable):
lines = laidout.split('\n')
first_indent = self.style.first_indent
rest_indent = self.style.rest_indent
# Common rendering code.
def render_lines(x, y, color):
y += self.style.text_y_fudge
indent = first_indent
for l in lines:
ls = font.render(l, self.style.antialias, color)
lw, lh = ls.get_size()
xo = int((self.width - lw) * self.style.textalign)
surf.blit(ls, (x + xo, y + font.get_descent()))
y += font.get_linesize() + self.style.line_height_fudge
fudge = 1
lw, lh = ls.get_size()
lw += indent
xo = int((self.width - lw) * self.style.textalign)
surf.blit(ls, (x + xo + indent, y + font.get_descent()))
y += font.get_linesize() + self.style.line_height_fudge
indent = rest_indent
# Render drop-shadow.
if self.style.drop_shadow:
render_lines(dsxo, dsyo + fudge, self.style.drop_shadow_color)
render_lines(dsxo, dsyo, self.style.drop_shadow_color)
# Render foreground.
render_lines(0, 0 + fudge, self.style.color)
render_lines(0, 0, self.style.color)
return surf
@@ -199,8 +216,7 @@ class Text(renpy.display.core.Displayable):
if not self.slow:
return None
if ( ev.type == MOUSEBUTTONDOWN and ev.button == 1) or \
( ev.type == KEYDOWN and (ev.key == K_RETURN or ev.key == K_SPACE)):
if renpy.display.behavior.map_event(ev, "dismiss"):
self.slow = False
raise renpy.display.core.IgnoreEvent()
+5
View File
@@ -20,6 +20,8 @@ class Context(object):
context.
@ivar rollback: True if this context participates in rollbacks.
@ivar runtime: The time spent in this context, in milliseconds.
"""
def __init__(self, rollback, context=None):
@@ -27,10 +29,12 @@ class Context(object):
self.current = None
self.return_stack = [ ]
self.rollback = rollback
self.runtime = 0
oldsl = None
if context:
oldsl = context.scene_lists
self.runtime = context.runtime
import renpy.display.core as dcore
self.scene_lists = dcore.SceneLists(oldsl)
@@ -111,6 +115,7 @@ class Context(object):
rv.return_stack = self.return_stack[:]
rv.current = self.current
rv.scene_lists = self.scene_lists.rollback_copy()
rv.runtime = self.runtime
return rv
+40 -10
View File
@@ -132,7 +132,7 @@ def input(prompt, default='', length=None):
return interact(win)
def menu(items, set_expr, window_style='menu_window'):
def menu(items, set_expr):
"""
Displays a menu, and returns to the user the value of the selected
choice. Also handles conditions and the menuset.
@@ -161,7 +161,7 @@ def menu(items, set_expr, window_style='menu_window'):
return None
# Show the menu.
rv = display_menu(items, window_style=window_style)
rv = renpy.store.menu(items)
# If we have a set, fill it in with the label of the chosen item.
if set is not None and rv is not None:
@@ -203,11 +203,12 @@ def say(who, what):
what = what % renpy.game.store
if who is None:
display_say(who, what, what_style='say_thought')
elif isinstance(who, (str, unicode)):
who = renpy.store.narrator
if isinstance(who, (str, unicode)):
display_say(who, what, what_style='say_dialogue')
else:
who.say(what)
who(what)
def display_say(who, what, who_style='say_label',
what_style='say_dialogue',
@@ -216,6 +217,8 @@ def display_say(who, what, who_style='say_label',
who_suffix=': ',
what_prefix='',
what_suffix='',
interact=True,
slow=True,
**properties):
"""
@param who: Who is saying the dialogue, or None if it's not being
@@ -226,6 +229,11 @@ def display_say(who, what, who_style='say_label',
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, right before ui.text.
if interact:
renpy.ui.saybehavior()
if who is not None:
who = who_prefix + who + who_suffix
@@ -238,13 +246,12 @@ def display_say(who, what, who_style='say_label',
if who is not None:
renpy.ui.text(who, style=who_style, **properties)
renpy.ui.text(what, style=what_style, slow=True)
renpy.ui.text(what, style=what_style, slow=slow)
renpy.ui.close()
renpy.ui.saybehavior()
interact()
checkpoint()
if interact:
renpy.ui.interact()
checkpoint()
def imagemap(ground, selected, hotspots, unselected=None, overlays=False,
style='imagemap', **properties):
@@ -428,6 +435,29 @@ def transition(trans):
else:
renpy.game.interface.set_transition(trans)
def clear_game_runtime():
"""
Resets the game runtime timer down to 0.
The game runtime counter counts the number of seconds that have
elapsed while waiting for user input in the current context. (So
it doesn't count time spent in the game menu.)
"""
renpy.game.context().runtime = 0
def get_game_runtime():
"""
Returns the number of seconds that have elapsed in gameplay since
the last call to clear_game_timer, as a float.
The game runtime counter counts the number of seconds that have
elapsed while waiting for user input in the current context. (So
it doesn't count time spent in the game menu.)
"""
return renpy.game.context().runtime / 1000.0
call_in_new_context = renpy.game.call_in_new_context
curried_call_in_new_context = renpy.curry.curry(renpy.game.call_in_new_context)
+1 -1
View File
@@ -146,7 +146,7 @@ def main(basepath):
renpy.config.backup()
# Load the script.
game.script = renpy.script.load_script(game.basepath)
game.script = renpy.script.load_script()
# Start things running.
+139
View File
@@ -2,6 +2,133 @@
import pygame
import renpy
import sys # to detect windows.
# The Windows Volume Management Strategy (tm).
# We keep a master music volume, which is the volume we use directly
# when playing music as mp3, ogg, etc. When we start up, we compute
# a midi music scaling factor. This midi music scaling factor is
# computed from the current master music volume such that when we are not
# fading, if we pygame.mixer.music.set_volume() to the mmv * mmsv, we get
# read the same value from midiOutGetVolume() as we did before we tried
# doing that.
playing_midi = True
fading = False
master_music_volume = 1.0
windows_magic = False
if hasattr(sys, 'winver'):
midi_msf = 0.0
last_raw_volume = -1
def read_raw_volume():
res = c_uint()
for i in range(0, winmm.midiOutGetNumDevs()):
rv = winmm.midiOutGetVolume(i, byref(res))
if not rv:
return res.value
else:
print "Couldn't read raw midi volume."
return -1
try:
from ctypes import windll, c_uint, byref
winmm = windll.winmm
def compute_midi_msf():
"""
Computes the Midi MSF. Returns True if successful, False if otherwise.
"""
# Don't update the MSF when fading is going on, or when not
# playing a midi. (Except before playing any music whatsoever.)
if fading or not playing_midi:
return False
global last_raw_volume
raw_vol = read_raw_volume()
if raw_vol < 0:
return False
# The case in which the volume hasn't changed recently.
if raw_vol == last_raw_volume:
return True
last_raw_volume = raw_vol
# print "raw_vol", raw_vol
# The fraction that the midi mixer is at.
mixfrac = 1.0 * ( raw_vol & 0xffff ) / 0xffff
global midi_msf
midi_msf = mixfrac / master_music_volume
# print "Midi msf is now:", midi_msf
return True
# This should get called after the music starts playing.
def set_music_volume(vol):
global master_music_volume
master_music_volume = vol
if playing_midi:
vol *= midi_msf
if vol > 1.0:
vol = 1.0
pygame.mixer.music.set_volume(vol)
global last_raw_volume
last_raw_volume = read_raw_volume()
# Figure out the default msf, and set it up.
windows_magic = compute_midi_msf()
playing_midi = False
except Exception, e:
print "Exception when trying to init music:", str(e)
print "Falling back to Unix mode."
if not windows_magic:
def compute_midi_msf():
return
def set_music_volume(vol):
global master_music_volume
master_music_volume = vol
pygame.mixer.music.set_volume(vol)
playing_midi = False
# This detects if the filename is a midi, and sets playing_midi
# appropriately.
def detect_midi(fn):
fn = fn.lower()
global playing_midi
playing_midi = fn.endswith(".mid") or fn.endswith(".midi")
# Information about the currently playing track.
current_music = None
@@ -61,6 +188,10 @@ def restore():
"""
global current_music
global fading
compute_midi_msf()
set_music_volume(1.0)
new_music = renpy.game.context().scene_lists.music
@@ -75,11 +206,19 @@ def restore():
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:
+9 -7
View File
@@ -35,7 +35,7 @@ class Script(object):
"""
def __init__(self, dir):
def __init__(self, node_callback=None):
"""
Loads the script by parsing all of the given files, and then
walking the various ASTs to initialize this Script object.
@@ -80,12 +80,12 @@ class Script(object):
# print "Loading", fn
if self.load_file(fn):
if self.load_file(fn, node_callback):
continue
print "Couldn't load %s, trying %s instead." % (fn, alt)
if self.load_file(alt):
if self.load_file(alt, node_callback):
continue
raise Exception("Could not load %s or %s." % (fn, alt))
@@ -95,7 +95,7 @@ class Script(object):
# Do some generic init here.
def load_file(self, fn):
def load_file(self, fn, node_callback):
if fn.endswith(".rpy"):
stmts = renpy.parser.parse(fn)
@@ -135,6 +135,9 @@ class Script(object):
# Check each node individually.
for node in all_stmts:
if node_callback:
node_callback(node)
# Check to see if the name is defined twice. If it is,
# report the error.
name = node.name
@@ -174,9 +177,8 @@ class Script(object):
return label in self.namemap
def load_script(dir):
rv = Script(dir)
def load_script():
rv = Script()
return rv
+13 -3
View File
@@ -7,10 +7,20 @@ from pygame.constants import *
def init():
try:
pygame.mixer.init(renpy.config.sound_sample_rate)
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:
if renpy.config.debug_sound:
raise
try:
pygame.mixer.pre_init()
except:
if renpy.config.debug_sound:
raise
def play(fn, loops=0):
"""
+54 -5
View File
@@ -79,12 +79,17 @@ class Character(object):
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.
@param who_suffix: A suffix that is appended to the name. (Defaults to ':')
@param what_prefix: A prefix that is prepended to the text body.
@param what_suffix: A suffix that is appended to the text body.
@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.
"""
self.name = name
@@ -93,7 +98,7 @@ class Character(object):
self.window_style = window_style
self.properties = properties
def say(self, what):
def __call__(self, what):
renpy.display_say(self.name, what,
who_style=self.who_style,
what_style=self.what_style,
@@ -129,7 +134,7 @@ class DynamicCharacter(object):
self.window_style = window_style
self.properties = properties
def say(self, what):
def __call__(self, what):
import renpy.python as python
renpy.display_say(python.py_eval(self.name_expr),
@@ -139,10 +144,54 @@ class DynamicCharacter(object):
window_style=self.window_style,
**self.properties)
# Conveniently get rid of all the packages we had imported before.
import renpy.exports as renpy
def narrator(what):
renpy.display_say(None, what, what_style='say_thought')
menu = renpy.display_menu
def color(s):
"""
This function converts a hexcode into a color/alpha tuple. Leading
# marks are ignored. Colors can be rgb or rgba, with each element having
either one or two digits. (So the strings can be 3, 4, 6, or 8 digits long,
not including the optional #.) A missing alpha is interpreted as 255,
fully opaque.
For example, color('#123a') returns (17, 34, 51, 170), while
color('c0c0c0') returns (192, 192, 192, 255).
"""
if s[0] == '#':
s = s[1:]
if len(s) == 6:
r = int(s[0]+s[1], 16)
g = int(s[2]+s[3], 16)
b = int(s[4]+s[5], 16)
a = 255
elif len(s) == 8:
r = int(s[0]+s[1], 16)
g = int(s[2]+s[3], 16)
b = int(s[4]+s[5], 16)
a = int(s[6]+s[7], 16)
elif len(s) == 3:
r = int(s[0], 16) * 0x11
g = int(s[1], 16) * 0x11
b = int(s[2], 16) * 0x11
a = 255
elif len(s) == 4:
r = int(s[0], 16) * 0x11
g = int(s[1], 16) * 0x11
b = int(s[2], 16) * 0x11
a = int(s[3], 16) * 0x11
else:
raise Exception("Argument to color() must be 3, 4, 6, or 8 hex digits long.")
return (r, g, b, a)
# The default transition.
default_transition = None