Merge branch 'master' into lint-labels-required-params

This commit is contained in:
Gouvernathor
2024-04-09 10:48:24 +02:00
committed by GitHub
102 changed files with 12324 additions and 1061 deletions
+20 -63
View File
@@ -61,7 +61,8 @@ init python in distribute:
minor=sys.version_info.minor,
)
# Going from 7.4 to 7.5 or 8.0, the library directory changed.
# * Going from 7.4 to 7.5 or 8.0, the library directory changed.
# * 7.7 called os.makedirs with exist_ok=True, even on Python 2.
RENPY_PATCH = py("""\
def change_renpy_executable():
import sys, os, renpy, site
@@ -70,6 +71,17 @@ def change_renpy_executable():
sys.renpy_executable = os.path.join(renpy.config.renpy_base, "lib", "py{major}-" + site.RENPY_PLATFORM, os.path.basename(sys.renpy_executable))
change_renpy_executable()
if sys.version_info.major == 2:
os.old_makedirs = getattr(os, "old_makedirs", os.makedirs)
def makedirs(name, mode=0o777, exist_ok=False):
if exist_ok and os.path.exists(name):
return
os.old_makedirs(name, mode)
os.makedirs = makedirs
""")
match_cache = { }
@@ -489,10 +501,6 @@ change_renpy_executable()
# A map from a package to a unique update version hash.
self.update_versions = { }
# Map from destination file with extension to (that file's hash,
# hash of the file list)
self.build_cache = { }
# A map from file to its hash.
self.hash_cache = { }
@@ -580,8 +588,6 @@ change_renpy_executable()
except Exception:
pass
self.load_build_cache()
self.packagedest = packagedest
self.include_update = build['include_update']
@@ -699,9 +705,6 @@ change_renpy_executable()
if self.build_update:
self.finish_updates(build_packages)
if not packagedest:
self.save_build_cache()
# Finish up.
self.log.close()
@@ -1537,20 +1540,6 @@ change_renpy_executable()
full_filename = "rpu/" + variant + ".files.rpu"
path = self.destination + "/" + full_filename
if self.build['renpy']:
fl_hash = fl.hash(self)
else:
fl_hash = '<not building renpy>'
file_hash, old_fl_hash = self.build_cache.get(full_filename, ("", ""))
if (not directory) and (old_fl_hash == fl_hash) and not(self.build['renpy'] and (variant == "sdk")):
if file_hash:
self.build_cache[full_filename] = (file_hash, fl_hash)
return
def done():
"""
This is called when the build of the package is done, either
@@ -1566,9 +1555,6 @@ change_renpy_executable()
else:
file_hash = ""
if file_hash:
self.build_cache[full_filename] = (file_hash, fl_hash)
if format == "tar.bz2" or format == "bare-tar.bz2":
pkg = TarPackage(path, "w:bz2")
elif format == "update":
@@ -1648,7 +1634,7 @@ change_renpy_executable()
if "zsync" in self.build["update_formats"]:
digest = self.build_cache[self.base_name + "-" + variant + ".update"][0]
digest = hash_file(self.destination + "/" + self.base_name + "-" + variant + ".update")
sums_size = os.path.getsize(self.destination + "/" + self.base_name + "-" + variant + ".sums")
index[variant].update({
@@ -1666,7 +1652,7 @@ change_renpy_executable()
if "rpu" in self.build["update_formats"]:
index[variant]["rpu_url"] = "rpu/" + variant + ".files.rpu"
index[variant]["rpu_digest"] = self.build_cache["rpu/" + variant + ".files.rpu"][0]
index[variant]["rpu_digest"] = hash_file(self.destination + "/rpu/" + variant + ".files.rpu")
for p in packages:
if p["update"]:
@@ -1674,8 +1660,11 @@ change_renpy_executable()
update_data = json.dumps(index, indent=2)
if not isinstance(update_data, bytes):
update_data = update_data.encode("utf-8")
fn = renpy.fsencode(os.path.join(self.destination, "updates.json"))
with open(fn, "wb" if PY2 else "w") as f:
with open(fn, "wb") as f:
f.write(update_data)
# Write the signed file.
@@ -1686,7 +1675,7 @@ change_renpy_executable()
fn = renpy.fsencode(os.path.join(self.destination, "updates.ecdsa"))
with open(fn, "wb") as f:
f.write(signing_key.sign(update_data.encode("utf-8")))
f.write(signing_key.sign(update_data))
def find_update_pem(self):
if self.build['renpy']:
@@ -1704,38 +1693,6 @@ change_renpy_executable()
with open(key_pem, "wb") as f:
f.write(signing_key.verifying_key.to_pem())
def save_build_cache(self):
if not self.build['renpy']:
return
fn = renpy.fsencode(os.path.join(self.destination, ".build_cache"))
with open(fn, "w", encoding="utf-8") as f:
for k, v in self.build_cache.items():
l = "\t".join([k, v[0], v[1]]) + "\n"
f.write(l)
def load_build_cache(self):
if not self.build['renpy']:
return
fn = renpy.fsencode(os.path.join(self.destination, ".build_cache"))
if not os.path.exists(fn):
return
with open(fn, "rb") as f:
for l in f:
if not l:
continue
l = l.decode("utf-8").rstrip()
l = l.split("\t")
self.build_cache[l[0]] = (l[1], l[2])
os.unlink(fn)
def dump(self):
for k, v in sorted(self.file_lists.items()):
print()
+7 -25
View File
@@ -21,27 +21,13 @@
init python:
import requests
import urllib.request
import os
import threading
import time
ssl_context_cache = None
def ssl_context():
"""
Returns the SSL context.
"""
global ssl_context_cache
if ssl_context_cache is None:
import ssl
import certifi
ssl_context_cache = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
return ssl_context_cache
class Downloader(object):
def __init__(self, url, dest):
@@ -73,7 +59,7 @@ init python:
try:
# Open the URL.
self.urlfile = urllib.request.urlopen(url, context=ssl_context())
self.urlfile = requests.get(url, stream=True, proxies=renpy.proxies, timeout=15)
t = threading.Thread(target=self.thread)
t.daemon = True
@@ -92,12 +78,7 @@ init python:
else:
length = 0
while not self.cancelled:
data = self.urlfile.read(65536)
if not data:
break
for data in self.urlfile.iter_content(1024 * 1024):
count += len(data)
self.tmpfile.write(data)
@@ -105,6 +86,9 @@ init python:
if length > 0:
self.progress = 1.0 * count / length
if self.cancelled:
break
self.tmpfile.close()
if self.cancelled:
@@ -122,7 +106,6 @@ init python:
except Exception as e:
self.failure = str(e)
def safe_unlink(self, fn):
if os.path.exists(fn):
os.unlink(fn)
@@ -164,4 +147,3 @@ init python:
def periodic(self, st):
self.adjustment.change(self.downloader.progress)
return .25
+14 -6
View File
@@ -55,15 +55,23 @@ def generate_minimal(p):
os.makedirs(os.path.dirname(p.prefix), 0o777)
shutil.copytree(p.template, p.prefix)
def delete(fn):
fn = os.path.join(p.prefix, fn)
if os.path.isdir(fn):
shutil.rmtree(fn)
elif os.path.exists(fn):
os.unlink(fn)
# Prune directories.
shutil.rmtree(os.path.join(p.prefix, "cache"))
shutil.rmtree(os.path.join(p.prefix, "saves"))
shutil.rmtree(os.path.join(p.prefix, "tl"))
delete("cache")
delete("saves")
delete("tl")
# Prune files to be regenerated.
os.unlink(os.path.join(p.prefix, "gui.rpy"))
os.unlink(os.path.join(p.prefix, "screens.rpy"))
os.unlink(os.path.join(p.prefix, "options.rpy"))
delete("gui.rpy")
delete("screens.rpy")
delete("options.rpy")
# Generate files.
CodeGenerator(p).generate_code("gui.rpy")
+12 -3
View File
@@ -121,9 +121,14 @@ class CodeGenerator(object):
else:
template = os.path.join(self.p.template, filename)
if not os.path.exists(template):
return False
else:
with codecs.open(template, "r", "utf-8") as f:
self.lines = [ i.rstrip().replace(u"\ufeff", "") for i in f ]
return True
def remove_scale(self):
def scale(m):
@@ -386,7 +391,9 @@ class CodeGenerator(object):
if not os.path.exists(src):
src = os.path.join(self.p.template, name)
self.load_template(src)
if not self.load_template(src):
return
self.remove_scale()
self.write_target(dst)
@@ -401,7 +408,8 @@ class CodeGenerator(object):
if not self.p.update_code:
return
self.load_template(fn)
if not self.load_template(fn):
return
if defines:
self.update_gui_defines()
@@ -422,7 +430,8 @@ class CodeGenerator(object):
if os.path.exists(target):
return
self.load_template(fn)
if not self.load_template(fn):
return
self.translate_strings()
self.translate_comments()
+1 -1
View File
@@ -190,7 +190,7 @@ def download(url, filename, hash=None):
try:
response = requests.get(url, stream=True)
response = requests.get(url, stream=True, proxies=renpy.exports.proxies, timeout=15)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 1))
+2 -1
View File
@@ -59,6 +59,7 @@ init python:
if RENIOS_PATH:
import renios.create
import renios.image
import renios.interface
def IOSState():
if not RENIOS_PATH:
@@ -135,7 +136,7 @@ init python:
if gui:
iface = MobileInterface("ios")
else:
iface = rapt.interface.Interface()
iface = renios.interface.Interface()
if os.path.exists(dest):
if not iface.yesno(_("The Xcode project already exists. Would you like to rename the old project, and replace it with a new one?")):
+1 -1
View File
@@ -52,7 +52,7 @@ init python:
with interface.error_handling(_("Downloading the itch.io butler.")):
url = "https://broth.itch.ovh/butler/{}/LATEST/archive/default".format(platform)
response = requests.get(url, headers={'User-Agent' : "Renpy"})
response = requests.get(url, headers={'User-Agent' : "Renpy"}, proxies=renpy.proxies)
with open(zip, "wb") as f:
f.write(response.content)
+10
View File
@@ -19,6 +19,9 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Should translation files be shown in the launcher file navigation?
default persistent.show_translation_files = True
init python in navigation:
import store.interface as interface
import store.project as project
@@ -110,6 +113,9 @@ init python in navigation:
if shortfn.startswith("game/"):
shortfn = fn[5:]
if shortfn.startswith("tl/") and not persistent.show_translation_files:
continue
rv.append((shortfn, fn, None))
rv.sort()
@@ -218,6 +224,10 @@ screen navigation:
vbox:
style_group "l_navigation"
if persistent.navigation == "file":
textbutton _("Show translation files") style "l_checkbox" action [ ToggleField(persistent, "show_translation_files"), Jump("navigation_loop") ]
add SPACER
for group_name, group in groups:
if group_name is not None:
+4 -1
View File
@@ -184,7 +184,10 @@ init -1 python hide:
#####################
# More customizations can go here.
config.sound = False
config.has_sound = False
config.has_music = False
config.has_voice = False
config.force_sound = False
config.quit_action = Quit(confirm=False)
config.window_icon = "images/logo.png"
config.has_autosave = False
+1 -1
View File
@@ -27,7 +27,7 @@ translate ukrainian strings:
# gui/game/screens.rpy:114
old "## If there's a side image, display it above the text. Do not display on the phone variant - there's no room."
new "Якщо є бічне зображення, показуйте його над текстом. Не виводьте на мобільний варіант — немає місця."
new "## Якщо є бічне зображення, показуйте його над текстом. Не виводьте на мобільний варіант — немає місця."
# gui/game/screens.rpy:120
old "## Make the namebox available for styling through the Character object."
+5 -3
View File
@@ -27,7 +27,7 @@ init python:
PUBLIC_KEY = "renpy_public.pem"
CHANNELS_URL = "https://www.renpy.org/channels.json"
CHANNELS_URL = os.environ.get("RENPY_CHANNELS_URL", "https://www.renpy.org/channels.json")
version_tuple = renpy.version(tuple=True)
@@ -220,11 +220,13 @@ init python:
import requests
url = CHANNELS_URL + "?version=" + ".".join(str(i) for i in version_tuple)
if not quiet:
with interface.error_handling(_("downloading the list of update channels")):
channels = requests.get(CHANNELS_URL).json()["releases"]
channels = requests.get(url, proxies=renpy.proxies).json()["releases"]
else:
channels = requests.get(CHANNELS_URL).json()["releases"]
channels = requests.get(url, proxies=renpy.proxies).json()["releases"]
persistent.has_update = False
+1
View File
@@ -1,6 +1,7 @@
_renpy gen/_renpy.c IMG_savepng.c core.c
_renpybidi gen/_renpybidi.c renpybidicore.c
renpy.audio.renpysound gen/renpy.audio.renpysound.c renpysound_core.c ffmedia.c
renpy.audio.filter gen/renpy.audio.filter.c
renpy.lexersupport gen/renpy.lexersupport.c
renpy.pydict gen/renpy.pydict.c
renpy.style gen/renpy.style.c
+1 -1
View File
@@ -585,7 +585,7 @@ static AVCodecContext *find_context(AVFormatContext *ctx, int index) {
return NULL;
}
AVCodec *codec = NULL;
const AVCodec *codec = NULL;
AVCodecContext *codec_ctx = NULL;
codec_ctx = avcodec_alloc_context3(NULL);
+1
View File
@@ -176,6 +176,7 @@ style_properties = sorted_dict(
kerning=None,
key_events=None,
keyboard_focus=None,
keyboard_focus_insets=None,
language=None,
layout=None,
line_leading=None,
+178 -30
View File
@@ -29,6 +29,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include <string.h>
#include <pygame_sdl2/pygame_sdl2.h>
apply_audio_filter_type RPS_apply_audio_filter = NULL;
SDL_mutex *name_mutex;
#ifdef __EMSCRIPTEN__
@@ -100,7 +102,6 @@ static int initialized = 0;
/** Should fades be linear rather than logarithmic? */
static int linear_fades = 0;
struct Interpolate {
/* The number of samples that are finished so far. */
unsigned int done;
@@ -219,6 +220,15 @@ struct Channel {
/* The relative volume of the playing stream. */
float playing_relative_volume;
/* The number of samples of silence to pad the playing stream with.*/
int playing_pad;
/**
* The AudioFilter that is currently in use on this channel. NULL
* if no filter is in use.
*/
PyObject *playing_audio_filter;
/* The queued up stream. */
struct MediaState *queued;
@@ -237,6 +247,9 @@ struct Channel {
/* The relative volume of the queued stream. */
float queued_relative_volume;
/* The AudioFilter that is queued on this channel. */
PyObject *queued_audio_filter;
/* Is this channel paused? */
int paused;
@@ -279,6 +292,7 @@ struct Channel {
struct Dying {
struct MediaState *stream;
PyObject *audio_filter;
struct Dying *next;
};
@@ -312,6 +326,9 @@ static void start_stream(struct Channel* c, int reset_fade) {
if (!c) return;
c->pos = 0;
if (!c->queued) {
c->playing_pad = audio_spec.freq * 2;
}
if (reset_fade) {
@@ -349,15 +366,12 @@ static void post_event(struct Channel *c) {
#define ZERO_PAN 0.7071067811865476 // cos(PI / 4) and sin(PI / 4)
static inline void mix_sample(struct Channel *c, short left_in, short right_in, float *left_out, float *right_out) {
static inline void mix_sample(struct Channel *c, float left, float right, float *left_out, float *right_out) {
tick_interpolate(&c->fade);
tick_interpolate(&c->secondary_volume);
tick_interpolate(&c->pan);
float left = left_in / 1.0 / -MIN_SHORT;
float right = right_in / 1.0 / -MIN_SHORT;
float pan = get_interpolate(&c->pan);
if (pan == 0.0) {
@@ -392,10 +406,11 @@ void (*RPS_generate_audio_c_function)(float *stream, int length) = NULL;
static void callback(void *userdata, Uint8 *stream, int length) {
// Convert the length to samples.
length /= 4;
length /= (2 * sizeof(float));
float mix_buffer[length * 2];
short stream_buffer[length * 2];
float float_buffer[length * 2];
memset(mix_buffer, 0, length * 2 * sizeof(float));
@@ -430,6 +445,23 @@ static void callback(void *userdata, Uint8 *stream, int length) {
// If we're done with this stream, skip to the next.
if (c->stop_samples == 0 || read_length == 0) {
if (!c->playing_audio_filter || c->playing_audio_filter == Py_None || c->queued) {
c->playing_pad = 0;
}
if (c->playing_pad > 0) {
if (c->playing_pad > mixleft) {
read_length = mixleft;
} else {
read_length = c->playing_pad;
}
c->playing_pad -= read_length;
memset(stream_buffer, 0, read_length * 2 * sizeof(short));
} else {
int old_tight = c->playing_tight;
struct Dying *d;
@@ -440,6 +472,14 @@ static void callback(void *userdata, Uint8 *stream, int length) {
d = malloc(sizeof(struct Dying));
d->next = dying;
d->stream = c->playing;
// If there's a new audio filter, queue the old one for deallocation.
if (c->playing_audio_filter) {
d->audio_filter = c->playing_audio_filter;
} else {
d->audio_filter = NULL;
}
dying = d;
free(c->playing_name);
@@ -451,12 +491,15 @@ static void callback(void *userdata, Uint8 *stream, int length) {
c->playing_start_ms = c->queued_start_ms;
c->playing_relative_volume = c->queued_relative_volume;
c->playing_audio_filter = c->queued_audio_filter;
c->queued = NULL;
c->queued_name = NULL;
c->queued_fadein = 0;
c->queued_tight = 0;
c->queued_start_ms = 0;
c->queued_relative_volume = 1.0;
c->queued_audio_filter = NULL;
if (c->playing_fadein) {
old_tight = 0;
@@ -468,11 +511,21 @@ static void callback(void *userdata, Uint8 *stream, int length) {
continue;
}
}
for (int i = 0; i < read_length; i++) {
float_buffer[i * 2] = stream_buffer[i * 2] / 1.0 / -MIN_SHORT;
float_buffer[i * 2 + 1] = stream_buffer[i * 2 + 1] / 1.0 / -MIN_SHORT;
}
if (c->playing_audio_filter && c->playing_audio_filter != Py_None) {
RPS_apply_audio_filter(c->playing_audio_filter, float_buffer, 2, read_length, audio_spec.freq);
}
// We have some data in the buffer, so mix it.
for (int i = 0; (i < read_length) && c->stop_samples; i++) {
mix_sample(c, stream_buffer[i * 2], stream_buffer[i * 2 + 1], &mix_buffer[mixed * 2], &mix_buffer[mixed * 2 + 1]);
mix_sample(c, float_buffer[i * 2], float_buffer[i * 2 + 1], &mix_buffer[mixed * 2], &mix_buffer[mixed * 2 + 1]);
if (c->stop_samples > 0) {
c->stop_samples--;
@@ -489,27 +542,29 @@ static void callback(void *userdata, Uint8 *stream, int length) {
// Actually output the sound.
for (int i = 0; i < length; i++) {
int left = mix_buffer[i * 2] * MAX_SHORT;
int right = mix_buffer[i * 2 + 1] * MAX_SHORT;
if (left > MAX_SHORT) {
left = MAX_SHORT;
}
if (left < MIN_SHORT) {
left = MIN_SHORT;
}
if (right > MAX_SHORT) {
right = MAX_SHORT;
}
if (right < MIN_SHORT) {
right = MIN_SHORT;
}
((short *) stream)[i * 2] = left;
((short *) stream)[i * 2 + 1] = right;
}
float left = mix_buffer[i * 2];
float right = mix_buffer[i * 2 + 1];
if (left > 1.0) {
left = 1.0;
}
if (left < -1.0) {
left = -1.0;
}
if (right > 1.0) {
right = 1.0;
}
if (right < -1.0) {
right = -1.0;
}
((float *) stream)[i * 2] = left;
((float *) stream)[i * 2 + 1] = right;
}
}
@@ -578,7 +633,7 @@ struct MediaState *load_stream(SDL_RWops *rw, const char *ext, double start, dou
}
void RPS_play(int channel, SDL_RWops *rw, const char *ext, const char *name, int fadein, int tight, int paused, double start, double end, float relative_volume) {
void RPS_play(int channel, SDL_RWops *rw, const char *ext, const char *name, int fadein, int tight, int paused, double start, double end, float relative_volume, PyObject *audio_filter) {
struct Channel *c;
@@ -599,6 +654,11 @@ void RPS_play(int channel, SDL_RWops *rw, const char *ext, const char *name, int
c->playing_tight = 0;
c->playing_start_ms = 0;
c->playing_relative_volume = 1.0;
if (c->playing_audio_filter) {
Py_DECREF(c->playing_audio_filter);
c->queued_audio_filter = NULL;
}
}
if (c->queued) {
@@ -609,6 +669,11 @@ void RPS_play(int channel, SDL_RWops *rw, const char *ext, const char *name, int
c->queued_tight = 0;
c->queued_start_ms = 0;
c->queued_relative_volume = 1.0;
if (c->queued_audio_filter) {
Py_DECREF(c->queued_audio_filter);
c->queued_audio_filter = NULL;
}
}
/* Allocate playing sample. */
@@ -627,6 +692,13 @@ void RPS_play(int channel, SDL_RWops *rw, const char *ext, const char *name, int
c->playing_start_ms = (int) (start * 1000);
c->playing_relative_volume = relative_volume;
if (audio_filter) {
c->playing_audio_filter = audio_filter;
Py_INCREF(c->playing_audio_filter);
} else {
c->playing_audio_filter = NULL;
}
c->paused = paused;
start_stream(c, 1);
@@ -636,7 +708,7 @@ void RPS_play(int channel, SDL_RWops *rw, const char *ext, const char *name, int
error(SUCCESS);
}
void RPS_queue(int channel, SDL_RWops *rw, const char *ext, const char *name, int fadein, int tight, double start, double end, float relative_volume) {
void RPS_queue(int channel, SDL_RWops *rw, const char *ext, const char *name, int fadein, int tight, double start, double end, float relative_volume, PyObject *audio_filter) {
struct Channel *c;
@@ -648,7 +720,7 @@ void RPS_queue(int channel, SDL_RWops *rw, const char *ext, const char *name, in
/* If we're not playing, then we should play instead of queue. */
if (!c->playing) {
RPS_play(channel, rw, ext, name, fadein, tight, 0, start, end, relative_volume);
RPS_play(channel, rw, ext, name, fadein, tight, 0, start, end, relative_volume, audio_filter);
return;
}
@@ -666,6 +738,11 @@ void RPS_queue(int channel, SDL_RWops *rw, const char *ext, const char *name, in
c->queued_tight = 0;
}
if (c->queued_audio_filter) {
Py_DECREF(c->queued_audio_filter);
c->queued_audio_filter = NULL;
}
/* Allocate queued sample. */
c->queued = ms;
@@ -683,6 +760,13 @@ void RPS_queue(int channel, SDL_RWops *rw, const char *ext, const char *name, in
c->queued_start_ms = (int) (start * 1000);
c->queued_relative_volume = relative_volume;
if (audio_filter) {
c->queued_audio_filter = audio_filter;
Py_INCREF(c->queued_audio_filter);
} else {
c->queued_audio_filter = NULL;
}
UNLOCK_AUDIO();
error(SUCCESS);
@@ -719,6 +803,12 @@ void RPS_stop(int channel) {
c->playing_relative_volume = 1.0;
}
if (c->playing_audio_filter) {
Py_DECREF(c->playing_audio_filter);
c->playing_audio_filter = NULL;
}
if (c->queued) {
free_stream(c->queued);
c->queued = NULL;
@@ -726,6 +816,12 @@ void RPS_stop(int channel) {
c->queued_name = NULL;
c->queued_start_ms = 0;
c->queued_relative_volume = 1.0;
}
if (c->queued_audio_filter) {
Py_DECREF(c->queued_audio_filter);
c->queued_audio_filter = NULL;
}
UNLOCK_AUDIO();
@@ -764,6 +860,11 @@ void RPS_dequeue(int channel, int even_tight) {
c->queued_start_ms = 0;
if (c->queued_audio_filter) {
Py_DECREF(c->queued_audio_filter);
c->queued_audio_filter = NULL;
}
UNLOCK_AUDIO();
error(SUCCESS);
@@ -1094,6 +1195,42 @@ void RPS_set_secondary_volume(int channel, float vol2, float delay) {
error(SUCCESS);
}
/**
* Replaces audio filters with the given PyObject.
*/
void RPS_replace_audio_filter(int channel, PyObject *new_filter) {
struct Channel *c;
if (check_channel(channel)) {
return;
}
c = &channels[channel];
LOCK_AUDIO();
if (c->playing_audio_filter) {
Py_DECREF(c->playing_audio_filter);
Py_INCREF(new_filter);
c->playing_audio_filter = new_filter;
}
if (c->queued_audio_filter) {
Py_DECREF(c->queued_audio_filter);
Py_INCREF(new_filter);
c->queued_audio_filter = new_filter;
}
UNLOCK_AUDIO();
error(SUCCESS);
}
PyObject *RPS_read_video(int channel) {
struct Channel *c;
SDL_Surface *surf = NULL;
@@ -1185,7 +1322,7 @@ void RPS_init(int freq, int stereo, int samples, int status, int equal_mono, int
}
audio_spec.freq = freq;
audio_spec.format = AUDIO_S16SYS;
audio_spec.format = AUDIO_F32;
audio_spec.channels = stereo;
audio_spec.samples = samples;
audio_spec.callback = callback;
@@ -1242,6 +1379,11 @@ void RPS_periodic() {
while (d) {
media_close(d->stream);
struct Dying *next_d = d->next;
if (d->audio_filter) {
Py_DECREF(d->audio_filter);
}
free(d);
d = next_d;
}
@@ -1252,6 +1394,7 @@ void RPS_advance_time(void) {
media_advance_time();
}
void RPS_sample_surfaces(PyObject *rgb, PyObject *rgba) {
import_pygame_sdl2();
@@ -1262,6 +1405,11 @@ void RPS_sample_surfaces(PyObject *rgb, PyObject *rgba) {
}
int RPS_get_sample_rate() {
return audio_spec.freq;
}
/*
* Returns the error message string if an error has occured, or
* NULL if no error has happened.
+6 -3
View File
@@ -27,8 +27,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include <Python.h>
#include <SDL.h>
void RPS_play(int channel, SDL_RWops *rw, const char *ext, const char *name, int fadeout, int tight, int paused, double start, double end, float relative_volume);
void RPS_queue(int channel, SDL_RWops *rw, const char *ext, const char *name, int fadeout, int tight, double start, double end, float relative_volume);
void RPS_play(int channel, SDL_RWops *rw, const char *ext, const char *name, int fadeout, int tight, int paused, double start, double end, float relative_volume, PyObject *audio_filter);
void RPS_queue(int channel, SDL_RWops *rw, const char *ext, const char *name, int fadeout, int tight, double start, double end, float relative_volume, PyObject *audio_filter);
void RPS_stop(int channel);
void RPS_dequeue(int channel, int even_tight);
int RPS_queue_depth(int channel);
@@ -43,7 +43,7 @@ void RPS_set_volume(int channel, float volume);
float RPS_get_volume(int channel);
void RPS_set_pan(int channel, float pan, float delay);
void RPS_set_secondary_volume(int channel, float vol2, float delay);
void RPS_replace_audio_filter(int channel, PyObject *audio_filter);
int RPS_video_ready(int channel);
PyObject *RPS_read_video(int channel);
@@ -56,9 +56,12 @@ void RPS_quit(void);
void RPS_advance_time(void);
void RPS_periodic(void);
int RPS_get_sample_rate(void);
char *RPS_get_error(void);
extern void (*RPS_generate_audio_c_function)(float *stream, int length);
typedef void (* apply_audio_filter_type)(PyObject *, float *, int, int, int);
extern apply_audio_filter_type RPS_apply_audio_filter;
#endif
+4 -1
View File
@@ -142,7 +142,10 @@ cython(
"renpy.audio.renpysound",
[ "renpysound_core.c", "ffmedia.c" ],
libs=sdl + sound,
define_macros=macros)
define_macros=macros,
compile_args=[ "-Wno-deprecated-declarations" ] if ("RENPY_FFMPEG_NO_DEPRECATED_DECLARATIONS" in os.environ) else [ ])
cython("renpy.audio.filter")
# renpy
cython("renpy.lexersupport")
+1
View File
@@ -518,6 +518,7 @@ def import_all():
import renpy.audio.audio
import renpy.audio.music
import renpy.audio.sound
import renpy.audio.filter
import renpy.ui
import renpy.screenlang
+16 -3
View File
@@ -1150,6 +1150,15 @@ class RawMultipurpose(RawStatement):
compiling(self.loc)
def check_spline_types(value):
if isinstance(value, (position, int, float)):
return True
if isinstance(value, tuple):
return all(check_spline_types(i) for i in value)
return False
# Figure out what kind of statement we have. If there's no
# interpolator, and no properties, than we have either a
# call, or a child statement.
@@ -1207,6 +1216,12 @@ class RawMultipurpose(RawStatement):
values = [ ctx.eval(i) for i in exprs ]
if not all(check_spline_types(i) for i in values):
if name in { "matrixtransform", "matrixcolor" }:
raise Exception("%s: Spline interpolation requires position types. (You may want to use SplineMatrix.)" % name)
else:
raise Exception("%s: Spline interpolation requires position types." % name)
splines.append((name, values))
for expr, _with in self.expressions:
@@ -1608,8 +1623,6 @@ class Interpolation(Statement):
startradius, endradius = anchorradii
trans.state.anchorradius = interpolate(complete, startradius, endradius, position_or_none)
# Handle any splines we might have.
for name, values in splines:
value = interpolate_spline(complete, values, PROPERTIES[name])
@@ -2196,7 +2209,7 @@ def parse_atl(l):
# Now, look for properties and simple_expressions.
while True:
if (warper is not None) and (not has_block) and ll.match(':'):
if ((warper is not None) or (warp_function is not None)) and (not has_block) and ll.match(':'):
ll.expect_eol()
ll.expect_block("ATL")
has_block = True
+1
View File
@@ -31,6 +31,7 @@ renpy.update_path()
# Generated by scripts/relative_imports.py, do not edit below this line.
if 1 == 0:
from . import audio
from . import filter
from . import music
from . import renpysound
from . import sound
+54 -6
View File
@@ -128,12 +128,15 @@ class QueueEntry(object):
A queue entry object.
"""
def __init__(self, filename, fadein, tight, loop, relative_volume):
audio_filter = None
def __init__(self, filename, fadein, tight, loop, relative_volume, audio_filter):
self.filename = filename
self.fadein = fadein
self.tight = tight
self.loop = loop
self.relative_volume = relative_volume
self.audio_filter = audio_filter
class MusicContext(renpy.revertable.RevertableObject):
@@ -147,6 +150,8 @@ class MusicContext(renpy.revertable.RevertableObject):
pause = False
last_relative_volume = 1.0
audio_filter = None
raw_audio_filter = None
def __init__(self):
@@ -182,6 +187,15 @@ class MusicContext(renpy.revertable.RevertableObject):
# Should we pause this channel?
self.pause = False
# The audio filter that should be applied to things queued to
# this channel.
self.audio_filter = None
# The audio filter that was given to set_audio_filter. (This is usually
# wrapped int a Crossfade for use.)
self.raw_audio_filter = None
def copy(self):
"""
Returns a shallow copy of this context.
@@ -190,6 +204,8 @@ class MusicContext(renpy.revertable.RevertableObject):
rv = MusicContext()
rv.__dict__.update(self.__dict__)
self.audio_filter = self.raw_audio_filter
return rv
@@ -200,11 +216,16 @@ next_channel_number = 0
lock = threading.RLock()
NotSet = renpy.object.Sentinel("NotSet")
class Channel(object):
"""
This stores information about the currently-playing music.
"""
# The audio filter to use.
audio_filter = None
def __init__(self, name, default_loop, stop_on_mute, tight, file_prefix, file_suffix, buffer_queue, movie, framedrop):
# The name assigned to this channel. This is used to look up
@@ -544,9 +565,9 @@ class Channel(object):
renpysound.set_video(self.number, self.movie, loop=False)
if depth == 0:
renpysound.play(self.number, topf, topq.filename, paused=self.synchro_start, fadein=topq.fadein, tight=topq.tight, start=start, end=end, relative_volume=topq.relative_volume) # type:ignore
renpysound.play(self.number, topf, topq.filename, paused=self.synchro_start, fadein=topq.fadein, tight=topq.tight, start=start, end=end, relative_volume=topq.relative_volume, audio_filter=topq.audio_filter) # type:ignore
else:
renpysound.queue(self.number, topf, topq.filename, fadein=topq.fadein, tight=topq.tight, start=start, end=end, relative_volume=topq.relative_volume) # type:ignore
renpysound.queue(self.number, topf, topq.filename, fadein=topq.fadein, tight=topq.tight, start=start, end=end, relative_volume=topq.relative_volume, audio_filter=topq.audio_filter) # type:ignore
self.playing = True
@@ -570,9 +591,9 @@ class Channel(object):
if self.loop:
for i in self.loop:
if topq is not None:
newq = QueueEntry(i, 0, topq.tight, True, topq.relative_volume)
newq = QueueEntry(i, 0, topq.tight, True, topq.relative_volume, self.context.audio_filter)
else:
newq = QueueEntry(i, 0, False, True, 1.0)
newq = QueueEntry(i, 0, False, True, 1.0, self.context.audio_filter)
self.queue.append(newq)
# Try callback:
@@ -668,6 +689,33 @@ class Channel(object):
renpysound.dequeue(self.number, True)
renpysound.stop(self.number)
def set_audio_filter(self, audio_filter, replace=False, duration=0.016):
"""
Sets the audio filter being applied to this channel to `audio_filter`.
"""
with lock:
old_raw_audio_filter = self.context.raw_audio_filter
self.context.raw_audio_filter = audio_filter
if old_raw_audio_filter is None and audio_filter is None:
new_audio_filter = None
else:
new_audio_filter = renpy.audio.filter.Crossfade(
old_raw_audio_filter or renpy.audio.filter.Null(),
audio_filter or renpy.audio.filter.Null(),
duration)
self.context.audio_filter = new_audio_filter
if replace:
for q in self.queue:
q.audio_filter = new_audio_filter
renpysound.replace_audio_filter(self.number, new_audio_filter)
def enqueue(self, filenames, loop=True, synchro_start=False, fadein=0, tight=None, loop_only=False, relative_volume=1.0):
with lock:
@@ -684,7 +732,7 @@ class Channel(object):
self.keep_queue += 1
for filename in filenames:
qe = QueueEntry(filename, fadein, tight, False, relative_volume)
qe = QueueEntry(filename, fadein, tight, False, relative_volume, self.context.audio_filter)
self.queue.append(qe)
# Only fade the first thing in.
+25
View File
@@ -0,0 +1,25 @@
# Copyright 2004-2024 Tom Rothamel <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.
ctypedef void (*apply_audio_filter_type)(object, float *, int, int, int)
cdef apply_audio_filter_type *get_apply_audio_filter()
File diff suppressed because it is too large Load Diff
+37 -1
View File
@@ -34,7 +34,7 @@ from renpy.audio.audio import get_channel, get_serial
from renpy.audio.audio import register_channel, alias_channel
def play(filenames, channel="music", loop=None, fadeout=None, synchro_start=False, fadein=0, tight=None, if_changed=False, relative_volume=1.0):
def play(filenames, channel="music", loop=None, fadeout=None, synchro_start=True, fadein=0, tight=None, if_changed=False, relative_volume=1.0):
"""
:doc: audio
@@ -586,6 +586,8 @@ def pump():
def set_mixer(channel, mixer, default=False):
"""
:doc: audio
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'
@@ -609,6 +611,8 @@ def set_mixer(channel, mixer, default=False):
def get_all_mixers():
"""
:doc: audio
This gets all mixers in use.
"""
@@ -622,6 +626,8 @@ def get_all_mixers():
def channel_defined(channel):
"""
:doc: audio
Returns True if the channel exists, or False otherwise.
"""
@@ -631,6 +637,36 @@ def channel_defined(channel):
except Exception:
return False
def set_audio_filter(channel, audio_filter, replace=False, duration=0.016):
"""
:doc: audio
Sets the audio filter for sounds about to be queued to `audio_filter`.
`audio_filter`
Must be a an :doc:`audio filter <audio_filters>` or list of
audio filters, or None to remove the audio filter.
`replace`
If True, the audio filter replaces the current audio filter immediately,
changing currently playing and queued sounds. If False, the audio
filter will be used the next time a sound is played or queued.
`duration`
The duration to change from the current to the new filter, in seconds.
This prevents a popping sound when changing filters.
"""
if audio_filter is not None:
audio_filter = renpy.audio.filter.to_audio_filter(audio_filter)
try:
c = renpy.audio.audio.get_channel(channel)
c.set_audio_filter(audio_filter, replace=replace, duration=duration)
except Exception:
if renpy.config.debug_sound:
raise
# Music change logic:
# Use the queueing time to determine what should or should not be
+74 -7
View File
@@ -57,8 +57,8 @@ import_pygame_sdl2()
cdef extern from "renpysound_core.h":
void RPS_play(int channel, SDL_RWops *rw, char *ext, char* name, int fadein, int tight, int paused, double start, double end, float volume)
void RPS_queue(int channel, SDL_RWops *rw, char *ext, char *name, int fadein, int tight, double start, double end, float volume)
void RPS_play(int channel, SDL_RWops *rw, char *ext, char* name, int fadein, int tight, int paused, double start, double end, float volume, object audio_filter)
void RPS_queue(int channel, SDL_RWops *rw, char *ext, char *name, int fadein, int tight, double start, double end, float volume, object audio_filter)
void RPS_stop(int channel)
void RPS_dequeue(int channel, int even_tight)
int RPS_queue_depth(int channel)
@@ -73,6 +73,7 @@ cdef extern from "renpysound_core.h":
float RPS_get_volume(int channel)
void RPS_set_pan(int channel, float pan, float delay)
void RPS_set_secondary_volume(int channel, float vol2, float delay)
void RPS_replace_audio_filter(int channel, object audio_filter)
void RPS_advance_time()
int RPS_video_ready(int channel)
@@ -84,9 +85,17 @@ cdef extern from "renpysound_core.h":
void RPS_quit()
void RPS_periodic()
int RPS_get_sample_rate()
char *RPS_get_error()
void (*RPS_generate_audio_c_function)(float *stream, int length)
void (*RPS_apply_audio_filter)(object, float *stream, int length, int channels, int sample_rate)
from renpy.audio.filter cimport get_apply_audio_filter
RPS_apply_audio_filter = <void (*)(object, float *, int, int, int)> get_apply_audio_filter()
def check_error():
"""
@@ -100,7 +109,8 @@ def check_error():
if len(e):
raise Exception(unicode(e, "utf-8", "replace"))
def play(channel, file, name, paused=False, fadein=0, tight=False, start=0, end=0, relative_volume=1.0):
def play(channel, file, name, paused=False, fadein=0, tight=False, start=0, end=0, relative_volume=1.0, audio_filter=None):
"""
Plays `file` on `channel`. This clears the playing and queued samples and
replaces them with this file.
@@ -126,10 +136,16 @@ def play(channel, file, name, paused=False, fadein=0, tight=False, start=0, end=
`relative_volume`
A float giving the relative volume of the file.
`audio_filter`
The audio filter to apply when the file is being played.
"""
cdef SDL_RWops *rw
if audio_filter is not None:
audio_filter.prepare(get_sample_rate())
rw = RWopsFromPython(file)
if rw == NULL:
@@ -146,10 +162,11 @@ def play(channel, file, name, paused=False, fadein=0, tight=False, start=0, end=
tight = 0
name = name.encode("utf-8")
RPS_play(channel, rw, name, name, fadein * 1000, tight, pause, start, end, relative_volume)
RPS_play(channel, rw, name, name, fadein * 1000, tight, pause, start, end, relative_volume, audio_filter)
check_error()
def queue(channel, file, name, fadein=0, tight=False, start=0, end=0, relative_volume=1.0):
def queue(channel, file, name, fadein=0, tight=False, start=0, end=0, relative_volume=1.0, audio_filter=None):
"""
Queues `file` on `channel` to play when the current file ends. If no file is
playing, plays it.
@@ -159,6 +176,9 @@ def queue(channel, file, name, fadein=0, tight=False, start=0, end=0, relative_v
cdef SDL_RWops *rw
if audio_filter is not None:
audio_filter.prepare(get_sample_rate())
rw = RWopsFromPython(file)
if rw == NULL:
@@ -170,9 +190,10 @@ def queue(channel, file, name, fadein=0, tight=False, start=0, end=0, relative_v
tight = 0
name = name.encode("utf-8")
RPS_queue(channel, rw, name, name, fadein * 1000, tight, start, end, relative_volume)
RPS_queue(channel, rw, name, name, fadein * 1000, tight, start, end, relative_volume, audio_filter)
check_error()
def stop(channel):
"""
Immediately stops `channel`, and unqueues any queued audio file.
@@ -181,6 +202,7 @@ def stop(channel):
RPS_stop(channel)
check_error()
def dequeue(channel, even_tight=False):
"""
Dequeues the queued sound file.
@@ -192,6 +214,7 @@ def dequeue(channel, even_tight=False):
RPS_dequeue(channel, even_tight)
def queue_depth(channel):
"""
Returns the queue depth of the channel. 0 if no file is playing, 1 if
@@ -201,6 +224,7 @@ def queue_depth(channel):
return RPS_queue_depth(channel)
def playing_name(channel):
"""
Returns the `name` argument of the playing sound. This was passed into
@@ -214,6 +238,7 @@ def playing_name(channel):
return rv
def pause(channel):
"""
Pauses `channel`.
@@ -222,6 +247,7 @@ def pause(channel):
RPS_pause(channel, 1)
check_error()
def unpause(channel):
"""
Unpauses `channel`.
@@ -230,6 +256,7 @@ def unpause(channel):
RPS_pause(channel, 0)
check_error()
def unpause_all_at_start():
"""
Unpauses all channels that are paused at the start.
@@ -237,6 +264,7 @@ def unpause_all_at_start():
RPS_unpause_all_at_start()
def fadeout(channel, delay):
"""
Fades out `channel` over `delay` seconds.
@@ -245,6 +273,7 @@ def fadeout(channel, delay):
RPS_fadeout(channel, int(delay * 1000))
check_error()
def busy(channel):
"""
Returns true if `channel` is currently playing something, and false
@@ -253,6 +282,7 @@ def busy(channel):
return RPS_get_pos(channel) != -1
def get_pos(channel):
"""
Returns the position of the audio file playing in `channel`, in seconds.
@@ -261,6 +291,7 @@ def get_pos(channel):
return RPS_get_pos(channel) / 1000.0
def get_duration(channel):
"""
Returns the duration of the audio file playing in `channel`, in seconds, or
@@ -269,6 +300,7 @@ def get_duration(channel):
return RPS_get_duration(channel)
def set_volume(channel, volume):
"""
Sets the primary volume for `channel` to `volume`, a number between
@@ -282,6 +314,7 @@ def set_volume(channel, volume):
check_error()
def set_pan(channel, pan, delay):
"""
Sets the pan for channel.
@@ -299,6 +332,7 @@ def set_pan(channel, pan, delay):
RPS_set_pan(channel, pan, delay)
check_error()
def set_secondary_volume(channel, volume, delay):
"""
Sets the secondary volume for channel. This is linear, and is multiplied
@@ -312,6 +346,18 @@ def set_secondary_volume(channel, volume, delay):
RPS_set_secondary_volume(channel, volume, delay)
check_error()
def replace_audio_filter(channel, audio_filter):
"""
Replaces the audio filter for `channel` with `audio_filter`.
"""
if audio_filter is not None:
audio_filter.prepare(get_sample_rate())
RPS_replace_audio_filter(channel, audio_filter)
def get_volume(channel):
"""
Gets the primary volume associated with `channel`.
@@ -319,6 +365,7 @@ def get_volume(channel):
return RPS_get_volume(channel)
def video_ready(channel):
"""
Returns true if the video playing on `channel` has a frame ready for
@@ -327,6 +374,7 @@ def video_ready(channel):
return RPS_video_ready(channel)
def read_video(channel):
"""
Returns the frame of video playing on `channel`. This should be returned
@@ -345,6 +393,7 @@ def read_video(channel):
FRAME_PADDING = 4
return rv.subsurface((FRAME_PADDING, FRAME_PADDING, w - FRAME_PADDING * 2, h - FRAME_PADDING * 2))
# No video will be played from this channel.
NO_VIDEO = 0
@@ -372,6 +421,7 @@ def set_video(channel, video, loop=False):
else:
RPS_set_video(channel, NO_VIDEO)
def init(freq, stereo, samples, status=False, equal_mono=False, linear_fades=False):
"""
Initializes the audio system with the given parameters. The parameter are
@@ -406,6 +456,7 @@ def init(freq, stereo, samples, status=False, equal_mono=False, linear_fades=Fal
RPS_init(freq, stereo, samples, status, equal_mono, linear_fades)
check_error()
def quit(): # @ReservedAssignment
"""
De-initializes the audio system.
@@ -413,6 +464,7 @@ def quit(): # @ReservedAssignment
RPS_quit()
def periodic():
"""
Called periodically (at 20 Hz).
@@ -420,14 +472,25 @@ def periodic():
RPS_periodic()
def advance_time():
"""
Called to advance time at the start of a frame.
"""
RPS_advance_time()
def get_sample_rate():
"""
Returns the sample rate of the audio system.
This is not part of the api shared with other audio implementations
"""
return RPS_get_sample_rate()
def set_generate_audio_c_function(fn):
"""
This can be use to set a C function that totally replaces the Ren'Py
@@ -436,6 +499,8 @@ def set_generate_audio_c_function(fn):
The function is expected to have the signature void (*)(short *buf, int samples,
and fill buf with samples samples of audio, where each sample consists of two
shorts.
This is not part of the api shared with other audio implementations
"""
global RPS_generate_audio_c_function
@@ -446,6 +511,7 @@ def set_generate_audio_c_function(fn):
RPS_generate_audio_c_function = <void (*)(float *, int)> <uintptr_t> fn
# Store the sample surfaces so they stay alive.
rgb_surface = None
rgba_surface = None
@@ -464,6 +530,7 @@ def sample_surfaces(rgb, rgba):
RPS_sample_surfaces(rgb, rgba)
# Is this the webaudio module?
is_webaudio = False
+13 -2
View File
@@ -111,7 +111,7 @@ def proxy_call_both(func):
return func
@proxy_with_channel
def play(channel, file, name, paused=False, fadein=0, tight=False, start=0, end=0, relative_volume=1.0):
def play(channel, file, name, paused=False, fadein=0, tight=False, start=0, end=0, relative_volume=1.0, audio_filter=None):
"""
Plays `file` on `channel`. This clears the playing and queued samples and
replaces them with this file.
@@ -137,6 +137,9 @@ def play(channel, file, name, paused=False, fadein=0, tight=False, start=0, end=
`relative_volume`
A number between 0 and 1 that controls the relative volume of this file
`audio_filter`
The audio filter to apply when the file is being played.
"""
try:
@@ -155,7 +158,7 @@ def play(channel, file, name, paused=False, fadein=0, tight=False, start=0, end=
@proxy_with_channel
def queue(channel, file, name, fadein=0, tight=False, start=0, end=0, relative_volume=1.0):
def queue(channel, file, name, fadein=0, tight=False, start=0, end=0, relative_volume=1.0, audio_filter=None):
"""
Queues `file` on `channel` to play when the current file ends. If no file is
playing, plays it.
@@ -343,6 +346,14 @@ def set_secondary_volume(channel, volume, delay):
call("set_secondary_volume", channel, volume, delay)
@proxy_with_channel
def replace_audio_filter(channel, audio_filter):
"""
Replaces the audio filter for `channel` with `audio_filter`.
"""
# call("replace_audio_filter", channel, audio_filter)
@proxy_with_channel
def get_volume(channel):
+4 -1
View File
@@ -671,7 +671,6 @@ def display_say(
what_text = show_function(who, what_string, **show_args)
# What text is (screen, id, layer) tuple if we're using a screen.
if isinstance(what_text, tuple):
# If this is not the first pause, set the transform event to "replace".
@@ -680,6 +679,10 @@ def display_say(
if screen_displayable is not None:
screen_displayable.set_transform_event("replace")
if not retain and not last_pause:
sls = renpy.game.context().scene_lists
sls.set_transient_prefix(what_text[2], what_text[0], "replaced")
what_text = renpy.display.screen.get_widget(what_text[0], what_text[1], what_text[2])
if not multiple:
+24
View File
@@ -78,6 +78,10 @@ init -1500 python in achievement:
if persistent._achievements is None:
persistent._achievements = _set()
for k in list(persistent._achievements):
if not isinstance(k, str):
persistent._achievements.remove(k)
if persistent._achievement_progress is None:
persistent._achievement_progress = _dict()
@@ -179,6 +183,10 @@ init -1500 python in achievement:
not given, this defaults to 0.
"""
if config.developer:
if not isinstance(name, str):
raise TypeError("Achievement names must be strings.")
for i in backends:
i.register(name, **kwargs)
@@ -190,6 +198,10 @@ init -1500 python in achievement:
granted.
"""
if config.developer:
if not isinstance(name, str):
raise TypeError("Achievement names must be strings.")
if not has(name):
for i in backends:
i.grant(name)
@@ -201,6 +213,10 @@ init -1500 python in achievement:
Clears the achievement with `name`.
"""
if config.developer:
if not isinstance(name, str):
raise TypeError("Achievement names must be strings.")
for i in backends:
i.clear(name)
@@ -223,6 +239,10 @@ init -1500 python in achievement:
the achievement is not known.
"""
if config.developer:
if not isinstance(name, str):
raise TypeError("Achievement names must be strings.")
return persistent._achievement_progress.get(name, 0)
def progress(name, complete, total=None):
@@ -243,6 +263,10 @@ init -1500 python in achievement:
achievement.
"""
if config.developer:
if not isinstance(name, str):
raise TypeError("Achievement names must be strings.")
if has(name):
return
+19 -3
View File
@@ -121,7 +121,16 @@ init -1500 python:
identity_fields = ()
equality_fields = ('range', 'max_is_zero', 'style', 'offset', 'step', 'action', 'force_step')
def __init__(self, range, max_is_zero=False, style="bar", offset=0, step=None, action=None, force_step=False):
_max = max
def __init__(self, range=None, max_is_zero=False, style="bar", offset=0, step=None, action=None, force_step=False, min=None, max=None):
if max is not None and min is not None:
range = max - min
offset = min
elif range is None:
raise Exception("You must specify either range, or both max and min.")
self.range = range
self.max_is_zero = max_is_zero
self.style = style
@@ -131,7 +140,7 @@ init -1500 python:
if isinstance(range, float):
step = range / 10.0
else:
step = max(range // 10, 1)
step = __GenericValue._max(range // 10, 1)
self.step = step
self.action = action
@@ -337,7 +346,8 @@ init -1500 python hide:
generic_params = tuple(inspect.signature(__GenericValue.__init__).parameters.values())[1:]
suffix = inspect.cleandoc("""
`range`
The range to adjust over.
The range to adjust over. This must be specified if `max` and `min`
are not given.
`max_is_zero`
If True, then when the {kind}'s value is zero, the value of the
bar will be `range`, and all other values will be shifted down
@@ -354,6 +364,12 @@ init -1500 python hide:
the bar.
`action`
If not None, an action to call when the {kind}'s value is changed.
`min`
The minimum value of the bar. If both `min` and `max` are given,
`range` and `offset` are calculated from them.
`max`
The maximum value of the bar. If both `min` and `max` are given,
`range` and `offset` are calculated from them.
""")
for value in (DictValue, FieldValue, VariableValue, ScreenVariableValue, LocalVariableValue):
+3
View File
@@ -436,3 +436,6 @@ init 1100 python hide:
if config.fade_music is not None:
config.fadeout_audio = config.fade_music
config.max_texture_size = (max(config.max_texture_size[0], config.fbo_size[0]),
max(config.max_texture_size[1], config.fbo_size[1]))
+5
View File
@@ -520,6 +520,7 @@ init -1500 python in _console:
self.line_history.extend(persistent._console_line_history)
self.first_time = True
self.did_short_warning = False
self.reset()
@@ -719,6 +720,10 @@ init -1500 python in _console:
result = renpy.python.py_eval(code)
if persistent._console_short and not getattr(result, "_console_always_long", False):
he.result = aRepr.repr(result)
if not self.did_short_warning and he.result != repr(result):
self.did_short_warning = True
he.result += "\n\n" + __("The console is using short representations. To disable this, type 'long', and to re-enable, type 'short'")
else:
he.result = repr(result)
+1 -1
View File
@@ -411,7 +411,7 @@ screen _progress:
ypos 0
text "[new] [seen]/[total]":
size 14
size gui._scale(18)
color "#fff"
outlines [ (1, "#000", 0, 0) ]
alt ""
+19
View File
@@ -303,6 +303,24 @@ init -1700 python:
config.expensive_predict_callbacks.append(_predict_screens)
##########################################################################
# Name-only say statements.
# This character is copied when a name-only say statement is called.
name_only = adv
def predict_say(who, what):
who = Character(who, kind=name_only)
try:
who.predict(what)
except Exception:
pass
def say(who, what, interact=True, *args, **kwargs):
who = Character(who, kind=name_only)
who(what, interact=interact, *args, **kwargs)
##########################################################################
# Misc.
@@ -417,6 +435,7 @@ _quit_slot
_rollback
_skipping
_window_subtitle
_scene_show_hide_transition
""".split():
# _history, history_list, and _version are set later, so aren't included.
+58
View File
@@ -38,6 +38,9 @@ init -1500 python:
def __call__(self, other, done):
if isinstance(other, SplineMatrix):
other = other.matrix
if type(other) is not type(self):
return self.get(self.value)
@@ -48,6 +51,58 @@ init -1500 python:
return _MultiplyMatrix(self, other)
class SplineMatrix(_BaseMatrix):
"""
:doc: colormatrix
A Matrix wrapper that uses a spline to interpolate between two
matrices. The spline is used to control how much of each of the
two matrices are used.
`matrix`
The matrix that will be interpolated to.
`spline`
The spline that is used for the interpolation. This must
be a list containing 3 or more floating point numbers. The
The first number is the starting amount (usually 0.0), the
last number is the ending amount (usually 1.0), and the values
in between are the knots:
* For a single knot (3-number list), a quadratic curve is used.
* For two knots (4-number list), a Bezier spline is used.
* For three or more knots, Catmull-Rom splines are used. For
Catmull-Rom splines, the first and last knots (second and
second-last numbers) are control nodes, and the other knots
are the amounts that the spline goes through.
For example, the following will use a quadratic curve to interpolate
from the start, towards the end, before returning back to the start. ::
show eileen happy:
center
matrixcolor BrightnessMatrix(0.0)
linear 2.0 matrixcolor SplineMatrix(BrightnessMatrix(1.0), [ 0.0, 1.0, 0.0 ])
repeat
"""
def __init__(self, matrix, spline):
self.matrix = matrix
self.spline = spline
def __call__(self, other, done):
if type(other) is SplineMatrix:
other = other.matrix
if type(other) is not type(self.matrix):
return self.matrix(None, 1.0)
done = min(max(done, 0.0), 1.0)
done = renpy.atl.interpolate_spline(done, self.spline, float)
return self.matrix(other, done)
class ColorMatrix(_BaseMatrix):
"""
:undocumented:
@@ -149,6 +204,9 @@ init -1500 python:
def __call__(self, other, done):
if type(other) is SplineMatrix:
other = other.matrix
if type(other) is not type(self):
# When not using an old color, we can take
+3
View File
@@ -43,6 +43,9 @@ init -1400 python:
def __call__(self, other, done):
if type(other) is SplineMatrix:
other = other.matrix
if type(other) is not type(self):
return self.function(*self.args)
else:
-1
View File
@@ -209,7 +209,6 @@ init -1500 python:
* Preference("gl powersave", True) - Drop framerate to allow for power savings.
* Preference("gl powersave", False) - Do not drop framerate to allow for power savings.
* Preference("gl powersave", "auto") - Enable powersave when running on battery.
* Preference("gl framerate", None) - Runs at the display framerate.
* Preference("gl framerate", 60) - Runs at the given framerate.
+15 -111
View File
@@ -180,135 +180,39 @@ init -1100 python in _sync:
else:
return key, hashed.hex()
def requests_error(e):
import requests
def verbose_error(e):
renpy.display.log.write("Sync error:")
renpy.display.log.exception()
if isinstance(e, requests.exceptions.ConnectionError):
if renpy.emscripten:
return e.args[0]
import requests
if isinstance(e.original_exception, requests.exceptions.ConnectionError):
return _("Could not connect to the Ren'Py Sync server.")
elif isinstance(e, requests.exceptions.Timeout):
elif isinstance(e.original_exception, requests.exceptions.Timeout):
return _("The Ren'Py Sync server timed out.")
else:
return _("An unknown error occurred while connecting to the Ren'Py Sync server.")
if renpy.emscripten:
def upload_content(content, url):
"""
Uploads content to the sync server, using the given half-hash.
Returns None on success, or an error message on failure.
"""
import emscripten
import time
import os
with open("/sync.data", "wb") as f:
f.write(content)
fetch_id = emscripten.run_script_int(
"""fetchFile("PUT", "{url}", "/sync.data", null, "application/octet-string")""".format(url=url))
status = "PENDING"
message = "Pending."
start = time.time()
while start - time.time() < 15:
renpy.pause(0)
result = emscripten.run_script_string("""fetchFileResult({})""".format(fetch_id))
status, _ignored, message = result.partition(" ")
if status != "PENDING":
break
os.unlink("/sync.data")
if status != "OK":
return message
else:
return None
def download_content(url):
import emscripten
import time
import os
fetch_id = emscripten.run_script_int(
"""fetchFile("GET", "{url}", null, "/sync.data")""".format(url=url))
status = "PENDING"
message = "Pending."
start = time.time()
while start - time.time() < 15:
renpy.pause(0)
result = emscripten.run_script_string("""fetchFileResult({})""".format(fetch_id))
status, _ignored, message = result.partition(" ")
if status != "PENDING":
break
if status == "OK":
with open("/sync.data", "rb") as f:
data = f.read()
os.unlink("/sync.data")
return False, data
else:
if "404" in message:
return True, _("The Ren'Py Sync server does not have a copy of this sync. The sync ID may be invalid, or it may have timed out.")
else:
return True, message
else:
def upload_content(content, url):
"""
Uploads content to the sync server, using the given half-hash.
Returns None on success, or an error message on failure.
"""
import requests
try:
r = requests.put(url, data = content, timeout=15)
except Exception as e:
return requests_error(e)
if r.status_code != 200:
return r.text
renpy.fetch(url, method="PUT", data=content, timeout=15)
except renpy.FetchError as e:
return verbose_error(e)
return None
def download_content(url):
"""
Downloads content from the sync server, using the given half-hash.
Returns True and an error message on errro, and False and the content on success.
"""
import requests
try:
r = requests.get(url, timeout=15)
except Exception as e:
return True, requests_error(e)
return False, renpy.fetch(url, timeout=15)
except renpy.FetchError as e:
if r.status_code == 404:
if e.status_code == 404:
return True, _("The Ren'Py Sync server does not have a copy of this sync. The sync ID may be invalid, or it may have timed out.")
elif r.status_code != 200:
return True, r.text
return False, r.content
return True, verbose_error(e)
def report_error(message):
renpy.call_screen("sync_error", message)
+6 -5
View File
@@ -45,12 +45,12 @@ init -1500 python in updater:
def urlopen(url):
import requests
return io.BytesIO(requests.get(url).content)
return io.BytesIO(requests.get(url, proxies=renpy.exports.proxies, timeout=15).content)
def urlretrieve(url, fn):
import requests
data = requests.get(url).content
data = requests.get(url, proxies=renpy.exports.proxies, timeout=15).content
with open(fn, "wb") as f:
f.write(data)
@@ -370,7 +370,8 @@ init -1500 python in updater:
self.moves = [ ]
if self.allow_empty:
os.makedirs(self.updatedir, exist_ok=True)
if not os.path.isdir(self.updatedir):
os.makedirs(self.updatedir)
if public_key is not None:
with renpy.open_file(public_key, False) as f:
@@ -545,7 +546,7 @@ init -1500 python in updater:
url = urlparse.urljoin(self.url, self.updates[module]["rpu_url"])
try:
resp = requests.get(url)
resp = requests.get(url, proxies=renpy.exports.proxies, timeout=15)
resp.raise_for_status()
except Exception as e:
raise UpdateError(__("Could not download file list: ") + str(e))
@@ -1417,7 +1418,7 @@ init -1500 python in updater:
self.log.write("downloading %r\n" % url)
self.log.flush()
resp = requests.get(url, stream=True)
resp = requests.get(url, stream=True, proxies=renpy.exports.proxies, timeout=15)
if not resp.ok:
raise UpdateError(_("The update file was not downloaded."))
+5 -1
View File
@@ -1085,7 +1085,8 @@ touch_keyboard = os.environ.get("RENPY_TOUCH_KEYBOARD", False)
# The size of the framebuffer Ren'Py creates, which doubles as the
# largest texture size.
fbo_size = (4096, 4096)
max_texture_size = (4096, 4096)
fbo_size = max_texture_size
# Names to ignore the redefinition of.
lint_ignore_redefine = [ "gui.about" ]
@@ -1449,6 +1450,9 @@ screens_never_cancel_hide = True
# A list of transforms that are applied to entire layers.
layer_transforms = { }
# Should Ren'Py scan for exec.py?
exec_py = True
del os
del collections
+69
View File
@@ -34,6 +34,7 @@ import traceback
import os
import builtins
import io
import time
if PY2:
real_open = io.open
@@ -76,3 +77,71 @@ def init_main_thread_open():
return
builtins.open = replacement_open
# The path to the exec.py file, if it exists.
exec_py_exists = False
# The thread that scans for exec_py.
exec_py_thread = None
# The delay between exec_py scans.
exec_py_delay = 0.1
def scan_exec_py():
"""
Called by the save scanning thread to see if exec.py exists. If it does,
the path is stored in exec_py_path.
"""
while True:
time.sleep(.1)
exec_py_path = os.path.join(renpy.config.basedir, "exec.py")
if os.path.exists(exec_py_path):
global exec_py_exists
exec_py_exists = True
def init_exec_py():
"""
Starts the thread that scans for exec.py.
"""
if not renpy.config.exec_py:
return
global exec_py_thread
exec_py_thread = threading.Thread(target=scan_exec_py)
exec_py_thread.daemon = True
exec_py_thread.start()
def run_exec_py():
"""
Called by the save scanning thread to run exec.py, if it exists.
"""
global exec_py_exists
if exec_py_exists:
exec_py_path = os.path.join(renpy.config.basedir, "exec.py")
try:
with open(exec_py_path, "r") as f:
text = f.read()
except Exception as e:
exec_py_exists = False
return
try:
os.unlink(exec_py_path)
exec_py_exists = False
except Exception as e:
renpy.display.log.write("Failed to remove exec.py:")
renpy.display.log.exception(e)
return
renpy.python.py_exec(text)
+4 -2
View File
@@ -402,8 +402,9 @@ adv = ADVCharacter(None,
kind=False)
# This character is copied when a name-only say statement is called.
name_only = adv
# predict_say and who are defined in 00library.rpy, but we add default
# versions here in case there is a problem with initialization. (And
# for pickling purposes.)
def predict_say(who, what):
@@ -418,6 +419,7 @@ def say(who, what, interact=True, *args, **kwargs):
who = Character(who, kind=adv)
who(what, *args, interact=interact, **kwargs)
# Used by renpy.reshow_say and extend.
_last_say_who = None
_last_say_what = None
+1 -1
View File
@@ -229,7 +229,7 @@ cdef class RenderTransform:
else:
mr.mesh = True
if blur is not None:
if (blur is not None) and (blur > 0):
mr.add_shader("-renpy.texture")
mr.add_shader("renpy.blur")
mr.add_uniform("u_renpy_blur_log2", math.log(blur, 2))
+18 -1
View File
@@ -1404,6 +1404,7 @@ class Input(renpy.text.text.Text): # @UndefinedVariable
value = None
shown = False
multiline = False
action = None
st = 0
@@ -1424,6 +1425,7 @@ class Input(renpy.text.text.Text): # @UndefinedVariable
copypaste=False,
caret_blink=None,
multiline=False,
action=None,
**properties):
super(Input, self).__init__("", style=style, replaces=replaces, substitute=False, **properties)
@@ -1460,6 +1462,8 @@ class Input(renpy.text.text.Text): # @UndefinedVariable
self.multiline = multiline
self.action = action
caretprops = { 'color' : None }
for i, v in properties.items():
@@ -1645,8 +1649,21 @@ class Input(renpy.text.text.Text): # @UndefinedVariable
if self.edit_text:
content = content[0:self.caret_pos] + self.edit_text + self.content[self.caret_pos:]
if self.action is not None:
rv = run(self.action)
if rv is not None:
return rv
else:
raise renpy.display.core.IgnoreEvent()
if self.value:
return self.value.enter()
rv = self.value.enter()
if rv is not None:
return rv
else:
raise renpy.display.core.IgnoreEvent()
if not self.changed:
return content
+8 -1
View File
@@ -2579,10 +2579,14 @@ class Interface(object):
if isinstance(a, renpy.display.motion.Transform):
rv = a(child=rv)
new_transform = rv
else:
rv = a(rv)
rv._unique()
if isinstance(rv, renpy.display.transform.Transform):
new_transform = rv
if (new_transform is not None):
scene_lists.transform_state(self.old_root_transform, new_transform, execution=True)
@@ -2752,6 +2756,9 @@ class Interface(object):
# Check for autoreload.
renpy.loader.check_autoreload()
# Check for exec.py.
renpy.debug.run_exec_py()
if renpy.emscripten or os.environ.get('RENPY_SIMULATE_DOWNLOAD', False):
renpy.webloader.process_downloaded_resources()
+2 -1
View File
@@ -672,6 +672,7 @@ class Drag(renpy.display.displayable.Displayable, renpy.revertable.RevertableObj
# Record when the snap started
self.snap_start = at
redraw(self, 0)
self.snapping = True
elif self.target_at <= at or self.target_at <= self.at:
# Snap complete
self.x = self.target_x
@@ -681,7 +682,7 @@ class Drag(renpy.display.displayable.Displayable, renpy.revertable.RevertableObj
if self.snapping:
run(self.snapped, self, self.target_x, self.target_y, True)
self.snapping = False
else:
elif self.snapping:
# Snap in progress
done = (at - self.snap_start) / (self.target_at - self.snap_start)
if self.snap_warper is not None:
+43 -9
View File
@@ -135,6 +135,31 @@ class Focus(object):
return False
def inset_rect(self):
"""
Returns the rectangle with the keyboard focus insets applied.
"""
x = self.x
y = self.y
w = self.w
h = self.h
insets = self.widget.style.keyboard_focus_insets
if insets is not None:
x += insets[0]
y += insets[1]
w -= insets[0] + insets[2]
h -= insets[1] + insets[3]
if w < 1:
w = 1
if h < 1:
h = 1
return x, y, w, h
# The current focus argument.
argument = None
@@ -329,6 +354,9 @@ modal_generation = 0
# was called.
old_max_default = 0
# The name of the old max default focus.
old_max_default_focus_name = None
def mark_modal():
global modal_generation
modal_generation += 1
@@ -397,11 +425,13 @@ def before_interact(roots):
defaults.sort(key=operator.itemgetter(0))
max_default, max_default_focus, max_default_screen = defaults[-1]
max_default_focus_name = max_default_focus.full_focus_name
else:
max_default = 0
max_default_focus = None
max_default_screen = None
max_default_focus_name = None
# Should we do the max_default logic?
should_max_default = (renpy.display.interface.last_event is None) or (renpy.display.interface.last_event.type not in [ pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION ])
@@ -461,7 +491,7 @@ def before_interact(roots):
# If nothing has focus, focus the default if the highest priority has changed,
# or if the default is None.
if (should_max_default and (max_default > 0) and (current is None) and
(renpy.display.interface.start_interact or (max_default != old_max_default))):
(renpy.display.interface.start_interact or (max_default_focus_name != old_max_default_focus_name))):
current = max_default_focus
set_focused(max_default_focus, None, max_default_screen)
@@ -747,10 +777,12 @@ def focus_nearest(from_x0, from_y0, from_x1, from_y1,
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
from_focus_x, from_focus_y, from_focus_w, from_focus_h = from_focus.inset_rect()
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
@@ -776,10 +808,12 @@ def focus_nearest(from_x0, from_y0, from_x1, from_y1,
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
f_x, f_y, f_w, f_h = f.inset_rect()
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)
+56
View File
@@ -1927,6 +1927,29 @@ class AlphaMask(ImageBase):
return self.base.predict_files() + self.mask.predict_files()
class UnoptimizedTexture(ImageBase):
"""
:undocumented:
This is used by unoptimized_texture to force a texture to load without
optimizing the bounds.
"""
def __init__(self, im, **properties):
super(UnoptimizedTexture, self).__init__(im, optimize_bounds=False, **properties)
self.image = image(im)
def get_hash(self):
return self.image.get_hash()
def load(self):
return self.image.load()
def predict_files(self):
return self.image.predict_files()
def image(arg, loose=False, **properties):
"""
:doc: im_image
@@ -2070,7 +2093,40 @@ def load_rgba(data, size):
return renpy.display.draw.load_texture(surf)
def unoptimized_texture(d):
"""
:undocumented:
If `d` is an image manipulator, return an image manipulator that loads
the image without optimizing the bounds. Otherwise, return `d`.
"""
if isinstance(d, ImageBase):
return UnoptimizedTexture(d)
else:
return d
def render_for_texture(d, width, height, st, at):
"""
:undocumented:
Attempts to render `d` for the purpose of getting the underlying texture,
rather than a Render. A render may be returned if `d` is not an UnoptimizedTexture
returned by unoptimized_texture.
"""
if isinstance(d, UnoptimizedTexture):
return renpy.display.im.cache.get(d, texture=True)
else:
return renpy.display.render.render(d, width, height, st, at)
def reset_module():
"""
:undocumented:
"""
print("Resetting cache.")
global cache
+4 -1
View File
@@ -32,6 +32,8 @@ class Texture(object):
displayable = displayable._duplicate(None)
displayable._unique()
displayable = renpy.display.im.unoptimized_texture(displayable)
self.displayable = displayable
self.focus = focus
self.main = main
@@ -234,7 +236,8 @@ class Model(renpy.display.displayable.Displayable):
if self.size is not None:
width, height = self.size
renders = [ renpy.display.render.render(i.displayable, width, height, st, at) for i in self.textures ]
renders = [ renpy.display.im.render_for_texture(i.displayable, width, height, st, at) for i in self.textures ]
for cr, t in zip(renders, self.textures):
if t.fit:
+39 -7
View File
@@ -89,7 +89,7 @@ class SceneLists(renpy.object.Object):
things to the user.
"""
__version__ = 8
__version__ = 9
def after_setstate(self):
self.camera_list = getattr(self, "camera_list", { })
@@ -140,6 +140,9 @@ class SceneLists(renpy.object.Object):
if version < 8:
self.sticky_tags = { }
if version < 9:
self.additional_transient = [ (layer, tag, None) for layer, tag in self.additional_transient ] # type: ignore
def __init__(self, oldsl, shown):
@@ -227,6 +230,23 @@ class SceneLists(renpy.object.Object):
self.music = None
self.focused = None
def set_transient_prefix(self, layer, tag, prefix):
"""
Sets the transient prefix for the given tag on the given layer. This
can be used to have the "replaced" event delivered when the displayable
is hidden, and not the "hide" event.
"""
l = [ ]
for ltp in self.additional_transient:
if ltp[0] == layer and ltp[1] == tag:
ltp = (ltp[0], ltp[1], prefix)
l.append(ltp)
self.additional_transient = l
def replace_transient(self, prefix="hide"): # type: (str|None) -> None
"""
Replaces the contents of the transient display list with
@@ -242,8 +262,8 @@ class SceneLists(renpy.object.Object):
for i in renpy.config.transient_layers:
self.clear(i, True)
for layer, tag in self.additional_transient:
self.remove(layer, tag, prefix=prefix)
for layer, tag, p in self.additional_transient:
self.remove(layer, tag, prefix=p if p is not None else prefix)
self.additional_transient = [ ]
@@ -397,7 +417,7 @@ class SceneLists(renpy.object.Object):
self.shown.predict_show(layer, (key,) + name[1:])
if transient:
self.additional_transient.append((layer, key))
self.additional_transient.append((layer, key, None))
l = self.layers[layer]
@@ -692,10 +712,14 @@ class SceneLists(renpy.object.Object):
if isinstance(a, renpy.display.motion.Transform):
rv = a(child=rv)
new_transform = rv
else:
rv = a(rv)
rv._unique()
if isinstance(rv, renpy.display.motion.Transform):
new_transform = rv
if (new_transform is not None) and (renpy.config.keep_show_layer_state):
self.transform_state(old_transform, new_transform, execution=True)
@@ -721,10 +745,14 @@ class SceneLists(renpy.object.Object):
if isinstance(a, renpy.display.motion.Transform):
rv = a(child=rv)
new_transform = rv
else:
rv = a(rv)
rv._unique()
if isinstance(rv, renpy.display.motion.Transform):
new_transform = rv
if (new_transform is not None):
self.transform_state(old_transform, new_transform, execution=True)
@@ -750,10 +778,14 @@ class SceneLists(renpy.object.Object):
if isinstance(a, renpy.display.motion.Transform):
rv = a(child=rv)
new_transform = rv
else:
rv = a(rv)
rv._unique()
if isinstance(rv, renpy.display.motion.Transform):
new_transform = rv
if (new_transform is not None):
self.transform_state(old_transform, new_transform, execution=True)
+1
View File
@@ -159,6 +159,7 @@ def launch_editor(filenames, line=1, transient=False):
return False
filenames = [ renpy.lexer.unelide_filename(i) for i in filenames ]
filenames = [ os.path.realpath(i) for i in filenames ]
try:
editor.begin(new_window=transient)
+90 -19
View File
@@ -34,6 +34,11 @@ import threading
import fnmatch
import os
if PY2:
from urllib import urlencode as _urlencode
else:
from urllib.parse import urlencode as _urlencode
import renpy
from renpy.pyanalysis import const, pure, not_const
@@ -2229,7 +2234,7 @@ def get_game_runtime():
@renpy_pure
def loadable(filename, directory=None):
def loadable(filename, directory=None, tl=True):
"""
:doc: file
@@ -2241,9 +2246,11 @@ def loadable(filename, directory=None):
If not None, a directory to search in if the file is not found
in the game directory. This will be prepended to filename, and
the search tried again.
`tl`
If True, a translation subdirectory will be considered as well.
"""
return renpy.loader.loadable(filename, directory=directory)
return renpy.loader.loadable(filename, tl=tl, directory=directory)
@renpy_pure
@@ -4697,6 +4704,17 @@ def confirm(message):
return renpy.ui.interact()
try:
if PY2:
import urllib
proxies = urllib.getproxies()
else:
import urllib.request
proxies = urllib.request.getproxies()
except Exception as e:
proxies = {}
class FetchError(Exception):
"""
:undocumented:
@@ -4704,10 +4722,40 @@ class FetchError(Exception):
The type of errors raised by :func:`renpy.fetch`.
"""
pass
def __init__(self, message, exception=None):
super(FetchError, self).__init__(message)
self.original_exception = exception
m = re.search(r'\d\d\d', message)
if m is not None:
self.status_code = int(m.group(0))
else:
self.status_code = None
def fetch_requests(url, method, data, content_type, timeout):
def fetch_pause():
"""
Called by the fetch functions to pause for a short amount of time.
"""
if renpy.game.context().init_phase:
return
if renpy.game.context().interacting:
import pygame_sdl2
pygame_sdl2.event.pump()
renpy.audio.audio.periodic()
if renpy.emscripten:
emscripten.sleep(0)
return
renpy.exports.pause(0)
def fetch_requests(url, method, data, content_type, timeout, headers):
"""
:undocumented:
@@ -4722,26 +4770,30 @@ def fetch_requests(url, method, data, content_type, timeout):
# Because we don't have nonlocal yet.
resp = [ None ]
if data is not None:
headers = dict(headers)
headers["Content-Type"] = content_type
def make_request():
try:
r = requests.request(method, url, data=data, timeout=timeout, headers={ "Content-Type" : content_type } if data is not None else {})
r = requests.request(method, url, data=data, timeout=timeout, headers=headers, proxies=proxies)
r.raise_for_status()
resp[0] = r.content # type: ignore
except Exception as e:
resp[0] = FetchError(str(e)) # type: ignore
resp[0] = FetchError(str(e), e) # type: ignore
t = threading.Thread(target=make_request)
t.start()
while resp[0] is None:
renpy.exports.pause(0)
fetch_pause()
t.join()
return resp[0]
def fetch_emscripten(url, method, data, content_type, timeout):
def fetch_emscripten(url, method, data, content_type, timeout, headers):
"""
:undocumented:
@@ -4760,10 +4812,14 @@ def fetch_emscripten(url, method, data, content_type, timeout):
url = url.replace('"' , '\\"')
import json
headers = json.dumps(headers)
headers = headers.replace("\\", "\\\\").replace('"', '\\"')
if method == "GET" or method == "HEAD":
command = """fetchFile("{method}", "{url}", null, "{fn}", null)""".format( method=method, url=url, fn=fn, content_type=content_type)
command = """fetchFile("{method}", "{url}", null, "{fn}", null, "{headers}")""".format( method=method, url=url, fn=fn, content_type=content_type, headers=headers)
else:
command = """fetchFile("{method}", "{url}", "{fn}", "{fn}", "{content_type}")""".format( method=method, url=url, fn=fn, content_type=content_type)
command = """fetchFile("{method}", "{url}", "{fn}", "{fn}", "{content_type}", "{headers}")""".format( method=method, url=url, fn=fn, content_type=content_type, headers=headers)
fetch_id = emscripten.run_script_int(command)
@@ -4772,7 +4828,7 @@ def fetch_emscripten(url, method, data, content_type, timeout):
start = time.time()
while time.time() - start < timeout:
renpy.exports.pause(0)
fetch_pause()
result = emscripten.run_script_string("""fetchFileResult({})""".format(fetch_id))
status, _ignored, message = result.partition(" ")
@@ -4794,7 +4850,7 @@ def fetch_emscripten(url, method, data, content_type, timeout):
def fetch(url, method=None, data=None, json=None, content_type=None, timeout=5, result="bytes"):
def fetch(url, method=None, data=None, json=None, content_type=None, timeout=5, result="bytes", params=None, headers={}):
"""
:doc: fetch
@@ -4832,9 +4888,21 @@ def fetch(url, method=None, data=None, json=None, content_type=None, timeout=5,
decodes the result as JSON. (Other exceptions may be generated by the decoding
process.)
While waiting for `timeout` to pass, this will repeatedly call :func:`renpy.pause`\ (0),
so Ren'Py doesn't lock up. It may make sense to display a screen to the user
to let them know what is going on.
`params`
A dictionary of parameters that are added to the URL as a query string.
`headers`
A dictionary of headers to send with the request.
This may be called from inside or outside of an interaction.
* Outside of an interation, while waiting for `timeout` to pass, this will
repeatedly call :func:`renpy.pause`\ (0), so Ren'Py doesn't lock up. It
may make sense to display a screen to the user to let them know what is going on.
* Inside of an interaction (for example, inside an Action), this will block
the display system until the fetch request finishes or times out. It will try
to service the audio system, so audio will continue to play.
This function should work on all platforms. However, on the web platform,
requests going to a different origin than the game will fail unless allowed
@@ -4850,8 +4918,8 @@ def fetch(url, method=None, data=None, json=None, content_type=None, timeout=5,
if result not in ( "bytes", "text", "json" ):
raise FetchError("result must be one of 'bytes', 'text', or 'json'.")
if renpy.game.context().init_phase:
raise FetchError("renpy.fetch may not be called during init.")
if params is not None:
url += "?" + _urlencode(params)
if method is None:
if data is not None or json is not None:
@@ -4869,19 +4937,22 @@ def fetch(url, method=None, data=None, json=None, content_type=None, timeout=5,
data = _json.dumps(json).encode("utf-8")
if renpy.emscripten:
content = fetch_emscripten(url, method, data, content_type, timeout)
content = fetch_emscripten(url, method, data, content_type, timeout, headers=headers)
else:
content = fetch_requests(url, method, data, content_type, timeout)
content = fetch_requests(url, method, data, content_type, timeout, headers=headers)
if isinstance(content, Exception):
raise content # type: ignore
try:
if result == "bytes":
return content
elif result == "text":
return content.decode("utf-8")
elif result == "json":
return _json.loads(content)
except Exception as e:
raise FetchError("Failed to decode the result: " + str(e), e)
def can_fullscreen():
+2 -2
View File
@@ -751,7 +751,7 @@ cdef class GL2Draw:
# higher pitch.
BORDER = 64
width, height = renpy.config.fbo_size
width, height = renpy.config.max_texture_size
width = max(self.virtual_size[0] + BORDER, self.drawable_size[0] + BORDER, width)
width = min(width, max_texture_size, max_renderbuffer_size)
@@ -1509,7 +1509,7 @@ cdef class GL2DrawingContext:
# A set of uniforms that are defined by Ren'Py, and shouldn't be set in ATL.
standard_uniforms = { "u_transform", "u_time", "u_random" }
standard_uniforms = { "u_transform", "u_time", "u_random", "u_drawable_size" }
_types = """
standard_uniforms : set[str]
+12 -2
View File
@@ -191,6 +191,8 @@ cdef class Program:
This loads a shader into the GPU, and returns the number.
"""
original_source = source
source = source.encode("utf-8")
cdef GLuint shader
@@ -209,8 +211,14 @@ cdef class Program:
glGetShaderiv(shader, GL_COMPILE_STATUS, &status)
if status == GL_FALSE:
renpy.display.log.write("Error compiling shader %s:", self.name)
for i, l in enumerate(original_source.splitlines()):
renpy.display.log.write("%03d %s" % (i, l))
glGetShaderInfoLog(shader, 1024, NULL, error)
raise ShaderError((<object> error).decode("utf-8"))
raise ShaderError((<object> error).decode("latin-1"))
return shader
@@ -238,7 +246,7 @@ cdef class Program:
if status == GL_FALSE:
glGetProgramInfoLog(program, 1024, NULL, error)
raise ShaderError((<object> error).decode("utf-8"))
raise ShaderError(repr((<object> error)))
glDeleteShader(vertex)
glDeleteShader(fragment)
@@ -260,6 +268,8 @@ cdef class Program:
elif name == "u_viewport":
glGetFloatv(GL_VIEWPORT, viewport)
self.set_uniform("u_viewport", (viewport[0], viewport[1], viewport[2], viewport[3]))
elif name == "u_drawable_size":
self.set_uniform("u_drawable_size", renpy.display.draw.drawable_viewport[2:])
else:
raise Exception("Shader {} has not been given {} {}.".format(self.name, kind, name))
+3 -3
View File
@@ -46,10 +46,10 @@ cdef class TextureLoader:
cdef object texture_load_queue
# The maximum size of a texture.
cdef GLint max_texture_width
cdef GLint max_texture_height
cdef public GLint max_texture_width
cdef public GLint max_texture_height
cdef GLfloat max_anisotropy
cdef public GLfloat max_anisotropy
cdef class GLTexture(GL2Model):
+15 -3
View File
@@ -39,6 +39,7 @@ import sys
import os
import json
import collections
import re
did_onetime_init = False
@@ -217,7 +218,18 @@ class Live2DCommon(object):
self.textures = [ ]
for i in self.model_json["FileReferences"]["Textures"]:
self.textures.append(renpy.easy.displayable(self.base + i))
m = re.search(r'\.(\d+)/', i)
if m:
size = int(m.group(1))
renpy.config.max_texture_size = (
max(renpy.config.max_texture_size[0], size),
max(renpy.config.max_texture_size[1], size),
)
im = renpy.easy.displayable(self.base + i)
im = renpy.display.im.unoptimized_texture(im)
self.textures.append(im)
# A map from the motion file name to the information about it.
motion_files = { }
@@ -947,8 +959,8 @@ class Live2D(renpy.display.displayable.Displayable):
if redraws:
renpy.display.render.redraw(self, min(redraws))
# Render the textures.
textures = [ renpy.display.render.render(d, width, height, st, at) for d in common.textures ]
# Get the textures.
textures = [ renpy.display.im.render_for_texture(d, width, height, st, at) for d in common.textures ]
sw, sh = model.get_size()
+49 -19
View File
@@ -564,30 +564,23 @@ def group_logical_lines(lines):
# Note: We need to be careful with what's in here, because these
# are banned in simple_expressions, where we might want to use
# some of them.
KEYWORDS = set([
'$',
KEYWORDS = {
'as',
'at',
'behind',
'call',
'expression',
'hide',
'if',
'in',
'image',
'init',
'jump',
'menu',
'onlayer',
'python',
'return',
'scene',
'show',
'with',
'while',
}
IMAGE_KEYWORDS = {
'behind',
'at',
'onlayer',
'with',
'zorder',
'transform',
])
}
OPERATORS = [
'<>',
@@ -759,6 +752,21 @@ class Lexer(object):
self.skip_whitespace()
return self.match_regexp(regexp)
def match_multiple(self, *regexps):
"""
Matches multiple regular expressions. Return a tuple of matches
if all match, and if not returns None.
"""
oldpos = self.pos
rv = tuple(self.match(i) for i in regexps)
if None in rv:
self.pos = oldpos
return None
return rv
def keyword(self, word):
"""
Matches a keyword at the current position. A keyword is a word
@@ -1136,7 +1144,7 @@ class Lexer(object):
self.pos = oldpos
return None
if rv in KEYWORDS:
if (rv in KEYWORDS ) or (rv in IMAGE_KEYWORDS):
self.pos = oldpos
return None
@@ -1284,14 +1292,36 @@ class Lexer(object):
return False
def simple_expression(self, comma=False, operator=True):
def simple_expression(self, comma=False, operator=True, image=False):
"""
Tries to parse a simple_expression. Returns the text if it can, or
None if it cannot.
If comma is True, then a comma is allowed to appear in the
expression.
If operator is True, then an operator is allowed to appear in
the expression.
If image is True, then the expression is being parsed as part of
an image, and so keywords that are special in the show/hide/scene
statements are not allowed.
"""
start = self.pos
if image:
def lex_name():
oldpos = self.pos
n = self.name()
if n in IMAGE_KEYWORDS:
self.pos = oldpos
return None
return n
else:
lex_name = self.name
# Operator.
while True:
@@ -1304,7 +1334,7 @@ class Lexer(object):
# We start with either a name, a python_string, or parenthesized
# python
if not (self.python_string() or
self.name() or
lex_name() or
self.float() or
self.parenthesised_python()):
+8
View File
@@ -234,6 +234,8 @@ def image_exists_imprecise(name):
if [ i for i in banned if i in attrs ]:
continue
try:
li = getattr(d, "_list_attributes", None)
if li is not None:
@@ -242,6 +244,9 @@ def image_exists_imprecise(name):
if [ i for i in required if i not in attrs ]:
continue
except Exception:
pass
imprecise_cache.add(name)
return True
@@ -969,6 +974,9 @@ def check_unreachables(all_nodes):
add_names(reach)
elif isinstance(node, renpy.ast.RPY):
weakly_reachable.add(node)
def add_to_check(node):
to_check.add(node)
if (isinstance(node, renpy.ast.Label)
+2 -2
View File
@@ -708,14 +708,14 @@ def loadable_core(name):
return False
def loadable(name, directory=None):
def loadable(name, tl=True, directory=None):
name = name.lstrip('/')
if (renpy.config.loadable_callback is not None) and renpy.config.loadable_callback(name):
return True
for p in get_prefixes(directory=directory):
for p in get_prefixes(tl=tl, directory=directory):
if loadable_core(p + name):
return True
+3
View File
@@ -594,6 +594,9 @@ def main():
# Init the keymap.
renpy.display.behavior.init_keymap()
# Init exec.py scanning.
renpy.debug.init_exec_py()
gc.collect(2)
if gc.garbage:
+1 -1
View File
@@ -81,7 +81,7 @@ def parse_image_name(l, string=False, nodash=False):
if string:
points.append(l.checkpoint())
s = l.simple_expression()
s = l.simple_expression(image=True)
if s is not None:
rv.append(str(s))
+8
View File
@@ -1282,3 +1282,11 @@ def module_unpickle(name):
copyreg.pickle(types.ModuleType, module_pickle)
# Allow weakrefs to be pickled, with the reference being broken during
# unpickling.
def construct_None(*args):
return None
copyreg.pickle(weakref.ReferenceType, lambda r : (construct_None, tuple()))
+1
View File
@@ -189,6 +189,7 @@ Keyword("value")
Keyword("mask")
Keyword("caret_blink")
Keyword("multiline")
Keyword("action")
Style("caret")
add(text_properties)
+1 -2
View File
@@ -283,12 +283,11 @@ class Parser(object):
else:
l.error('%r is not a keyword argument or valid child of the %s statement.' % (name, self.name))
if name == "at" and l.keyword("transform"):
if name == "at" and l.match_multiple("transform", ":"):
if target.atl_transform is not None:
l.error("More than one 'at transform' block is given.")
l.require(":")
l.expect_eol()
l.expect_block("ATL block")
expr = renpy.atl.parse_atl(l.subblock_lexer())
+1
View File
@@ -148,6 +148,7 @@ button_properties = [ Style(i) for i in [
"focus_mask",
"child",
"keyboard_focus",
"keyboard_focus_insets",
"key_events",
] ] + [
Keyword("action"),
+13 -11
View File
@@ -195,7 +195,7 @@ def parse(s):
state = LITERAL
lit = ''
elif c == '!':
elif c == '!' and brackets == 0 and parens == 0:
if s[pos+1:pos+2] == '=':
pos += 1
else:
@@ -203,7 +203,7 @@ def parse(s):
expr = s[cut:pos]
cut = pos + 1
elif c == ':':
elif c == ':' and brackets == 0 and parens == 0:
state = FORMAT
expr = s[cut:pos]
cut = pos + 1
@@ -351,21 +351,23 @@ def substitute(s, scope=None, force=False, translate=True):
old_s = s
dicts = [ renpy.store.__dict__ ]
if "store.interpolate" in renpy.python.store_dicts:
dicts.insert(0, renpy.python.store_dicts["store.interpolate"])
dicts = []
if scope is not None:
dicts.insert(0, scope)
dicts.append(scope)
if dicts:
kwargs = MultipleDict(*dicts)
if "store.interpolate" in renpy.python.store_dicts:
dicts.append(renpy.python.store_dicts["store.interpolate"])
dicts.append(renpy.store.__dict__)
if len(dicts) == 1:
variables = dicts[0]
else:
kwargs = dicts[0]
variables = MultipleDict(*dicts)
try:
s = interpolate(s, kwargs) # type: ignore
s = interpolate(s, variables) # type: ignore
except Exception:
if renpy.display.predict.predicting: # @UndefinedVariable
return " ", True
+2 -2
View File
@@ -770,8 +770,8 @@ cdef class FTFont:
x = <int> (glyph.x + xo)
y = <int> (glyph.y + yo)
underline_x = x - glyph.delta_x_offset
underline_end = x + <int> glyph.advance + expand
underline_x = x - glyph.delta_x_adjustment
underline_end = x + <int> (glyph.advance + expand + .999)
if glyph.variation == 0:
index = FT_Get_Char_Index(face, glyph.character)
+86 -25
View File
@@ -114,6 +114,15 @@ cdef extern from "hb.h":
void hb_font_set_scale(hb_font_t *font, int x_scale, int y_scale)
void hb_font_set_synthetic_slant(hb_font_t *font, float slant)
struct hb_glyph_extents_t:
hb_position_t x_bearing
hb_position_t y_bearing
hb_position_t width
hb_position_t height
void hb_font_get_glyph_advance_for_direction(hb_font_t *font, hb_codepoint_t glyph, hb_direction_t direction, hb_position_t *x, hb_position_t *y)
void hb_font_get_glyph_extents_for_origin(hb_font_t *font, hb_codepoint_t glyph, hb_position_t *x, hb_position_t *y, hb_glyph_extents_t *extents)
# hb-shape
void hb_shape (
hb_font_t *font,
@@ -122,7 +131,6 @@ cdef extern from "hb.h":
unsigned int num_features);
cdef extern from "hb-ft.h":
hb_face_t *hb_ft_face_create(FT_Face ft_face, hb_destroy_func_t *destroy)
hb_font_t *hb_ft_font_create(FT_Face ft_face, hb_destroy_func_t *destroy)
@@ -130,6 +138,7 @@ cdef extern from "hb-ft.h":
void hb_ft_font_set_funcs(hb_font_t *font)
void hb_ft_font_set_load_flags (hb_font_t *font, int load_flags)
cdef extern from "hb-ot.h":
ctypedef unsigned int hb_ot_name_id_t
struct hb_language_impl_t
@@ -141,6 +150,39 @@ cdef extern from "hb-ot.h":
unsigned int *text_size,
char *text)
ctypedef enum hb_ot_metrics_tag_t:
HB_OT_METRICS_TAG_HORIZONTAL_ASCENDER
HB_OT_METRICS_TAG_HORIZONTAL_DESCENDER
HB_OT_METRICS_TAG_HORIZONTAL_LINE_GAP
HB_OT_METRICS_TAG_HORIZONTAL_CLIPPING_ASCENT
HB_OT_METRICS_TAG_HORIZONTAL_CLIPPING_DESCENT
HB_OT_METRICS_TAG_VERTICAL_ASCENDER
HB_OT_METRICS_TAG_VERTICAL_DESCENDER
HB_OT_METRICS_TAG_VERTICAL_LINE_GAP
HB_OT_METRICS_TAG_HORIZONTAL_CARET_RISE
HB_OT_METRICS_TAG_HORIZONTAL_CARET_RUN
HB_OT_METRICS_TAG_HORIZONTAL_CARET_OFFSET
HB_OT_METRICS_TAG_VERTICAL_CARET_RISE
HB_OT_METRICS_TAG_VERTICAL_CARET_RUN
HB_OT_METRICS_TAG_VERTICAL_CARET_OFFSET
HB_OT_METRICS_TAG_X_HEIGHT
HB_OT_METRICS_TAG_CAP_HEIGHT
HB_OT_METRICS_TAG_SUBSCRIPT_EM_X_SIZE
HB_OT_METRICS_TAG_SUBSCRIPT_EM_Y_SIZE
HB_OT_METRICS_TAG_SUBSCRIPT_EM_X_OFFSET
HB_OT_METRICS_TAG_SUBSCRIPT_EM_Y_OFFSET
HB_OT_METRICS_TAG_SUPERSCRIPT_EM_X_SIZE
HB_OT_METRICS_TAG_SUPERSCRIPT_EM_Y_SIZE
HB_OT_METRICS_TAG_SUPERSCRIPT_EM_X_OFFSET
HB_OT_METRICS_TAG_SUPERSCRIPT_EM_Y_OFFSET
HB_OT_METRICS_TAG_STRIKEOUT_SIZE
HB_OT_METRICS_TAG_STRIKEOUT_OFFSET
HB_OT_METRICS_TAG_UNDERLINE_SIZE
HB_OT_METRICS_TAG_UNDERLINE_OFFSET
void hb_ot_metrics_get_position_with_fallback (hb_font_t *font, hb_ot_metrics_tag_t metrics_tag, hb_position_t *position)
# The freetype library object we use.
cdef FT_Library library
@@ -580,9 +622,19 @@ cdef class HBFont:
cdef int error
cdef FT_Face face
cdef FT_Fixed scale
cdef float ascent_scale
cdef hb_position_t horizontal_ascender
cdef hb_position_t horizontal_descender
cdef hb_position_t horizontal_line_gap
cdef hb_position_t vertical_ascender
cdef hb_position_t vertical_descender
cdef hb_position_t vertical_line_gap
cdef hb_position_t underline_offset
cdef hb_position_t underline_size
face = self.face
if self.face_object.variations:
@@ -599,12 +651,35 @@ cdef class HBFont:
self.has_setup = True
scale = face.size.metrics.y_scale
self.hb_font = hb_ft_font_create(self.face_object.face, NULL)
hb_ft_font_set_funcs(self.hb_font)
hb_font_set_scale(self.hb_font, <int> (self.size * 64), <int> (self.size * 64))
if self.italic:
hb_font_set_synthetic_slant(self.hb_font, .207)
hb_ft_font_set_load_flags(self.hb_font, self.hinting | FT_LOAD_COLOR)
# Get the metrics.
hb_ot_metrics_get_position_with_fallback(self.hb_font, HB_OT_METRICS_TAG_HORIZONTAL_ASCENDER, &horizontal_ascender)
hb_ot_metrics_get_position_with_fallback(self.hb_font, HB_OT_METRICS_TAG_HORIZONTAL_DESCENDER, &horizontal_descender)
hb_ot_metrics_get_position_with_fallback(self.hb_font, HB_OT_METRICS_TAG_VERTICAL_ASCENDER, &vertical_ascender)
hb_ot_metrics_get_position_with_fallback(self.hb_font, HB_OT_METRICS_TAG_VERTICAL_DESCENDER, &vertical_descender)
hb_ot_metrics_get_position_with_fallback(self.hb_font, HB_OT_METRICS_TAG_UNDERLINE_OFFSET, &underline_offset)
hb_ot_metrics_get_position_with_fallback(self.hb_font, HB_OT_METRICS_TAG_UNDERLINE_SIZE, &underline_size)
vextent_scale = renpy.config.ftfont_vertical_extent_scale.get(self.face_object.fn, 1.0)
self.ascent = FT_CEIL(int(face.size.metrics.ascender * vextent_scale))
self.descent = FT_FLOOR(int(face.size.metrics.descender * vextent_scale))
if self.vertical:
self.ascent = FT_CEIL(vertical_ascender)
self.descent = FT_FLOOR(vertical_descender)
else:
self.ascent = FT_CEIL(int(horizontal_ascender * vextent_scale))
self.descent = FT_FLOOR(int(horizontal_descender * vextent_scale))
if self.descent > 0:
self.descent = -self.descent
@@ -617,27 +692,17 @@ cdef class HBFont:
self.lineskip = <int> self.height * renpy.game.preferences.font_line_spacing
if self.vertical:
self.underline_offset = FT_FLOOR(FT_MulFix(face.ascender + face.descender - face.underline_position, scale))
self.underline_offset = FT_FLOOR(self.ascent - self.descent - underline_offset)
else:
self.underline_offset = FT_FLOOR(FT_MulFix(face.underline_position, scale))
self.underline_height = FT_FLOOR(FT_MulFix(face.underline_thickness, scale))
self.underline_offset = FT_FLOOR(underline_offset)
self.underline_height = FT_FLOOR(underline_size)
if self.underline_height < 1:
self.underline_height = 1
self.underline_height += self.expand
self.hb_font = hb_ft_font_create(self.face_object.face, NULL)
hb_ft_font_set_funcs(self.hb_font)
hb_font_set_scale(self.hb_font, <int> (self.size * 64), <int> (self.size * 64))
if self.italic:
hb_font_set_synthetic_slant(self.hb_font, .207)
hb_ft_font_set_load_flags(self.hb_font, self.hinting | FT_LOAD_COLOR)
return
cdef glyph_cache *get_glyph(self, int index):
@@ -708,10 +773,8 @@ cdef class HBFont:
shear.yx = 1 << 16
shear.yy = 0
FT_Outline_Transform(&(<FT_OutlineGlyph> g).outline, &shear)
FT_Outline_Translate(&(<FT_OutlineGlyph> g).outline, 0, (face.bbox.xMax - face.bbox.xMin) // 2)
if self.stroker != NULL:
# FT_Glyph_StrokeBorder(&g, self.stroker, 0, 1)
FT_Glyph_Stroke(&g, self.stroker, 1)
if self.antialias:
@@ -954,13 +1017,11 @@ cdef class HBFont:
x = <int> (glyph.x + xo + glyph.x_offset)
y = <int> (glyph.y + yo + glyph.y_offset)
underline_x = x - glyph.delta_x_offset
underline_end = x + <int> glyph.advance + expand
underline_x = x - glyph.delta_x_adjustment
underline_end = x + <int> (glyph.advance + expand + .9999)
cache = self.get_glyph(glyph.glyph)
# with nogil used to be here, but it slowed things down.
bmx = <int> (x + .5) + cache.bitmap_left
bmy = y - cache.bitmap_top
+2 -2
View File
@@ -799,12 +799,12 @@ class Layout(object):
elif adjust_spacing == "vertical":
target_x_delta = 0.0
textsupport.tweak_glyph_spacing(all_glyphs, lines, target_x_delta, target_y_delta, maxx, y) # @UndefinedVariable
textsupport.adjust_glyph_spacing(all_glyphs, lines, target_x_delta, target_y_delta, maxx, y) # @UndefinedVariable
maxx = target_x
y = target_y
textsupport.offset_glyphs(all_glyphs, 0, round(splits_from.baseline * self.oversample) - find_baseline())
textsupport.move_glyphs(all_glyphs, 0, round(splits_from.baseline * self.oversample) - find_baseline())
# Figure out the size of the texture. (This is a little over-sized,
# but it simplifies the code to not have to care about borders on a
+18 -6
View File
@@ -31,16 +31,25 @@ cdef enum ruby_t:
RUBY_TOP
RUBY_ALT
# A note: Ren'Py's text layout algorithm lays out all text horizontally, even
# text that will eventually be displayed vertically - so when thinking about
# vertical text, swap x and y, height and width. ascent and line spacing, etc.
cdef class Glyph:
cdef:
# The x and y coordinates of the placed character.
public int x, y
public int x
public int y
# The change in the amount this character was shifted to the right
# when adjusting placement.
public int delta_x_offset
# This is the amount this character was shifted to the right when
# adjusting placement to match the virtual text, relative to the
# previous character in the line. This is used to draw underlines
# and strikethroughs in the right place.
public int delta_x_adjustment
# The character we use.
public unsigned int character
@@ -60,10 +69,13 @@ cdef class Glyph:
public int ascent
public int line_spacing
# The width and advance of the font.
# The width of the character.
public float width
# The advance to the next character.
public float advance
# The offsets of the character relative to the origin.
public float x_offset
public float y_offset
@@ -73,7 +85,7 @@ cdef class Glyph:
# The hyperlink this is part of.
public short hyperlink
# Should we draw this glyph.
# Should we draw this glyph?
public bint draw
+11 -11
View File
@@ -30,7 +30,7 @@ cdef class Glyph:
def __cinit__(self):
self.variation = 0
self.delta_x_offset = 0
self.delta_x_adjustment = 0
def __repr__(self):
if self.variation == 0:
@@ -41,7 +41,7 @@ cdef class Glyph:
_types = """
x: int
y: int
delta_x_offset : int
delta_x_adjustment : int
character : int
variation : int
split : int
@@ -1139,26 +1139,26 @@ def copy_splits(list source, list dest):
d.split = s.split
def tweak_glyph_spacing(list glyphs, list lines, double dx, double dy, double w, double h):
def adjust_glyph_spacing(list glyphs, list lines, double dx, double dy, double w, double h):
cdef Glyph g
if w <= 0 or h <= 0:
return
cdef int old_x_offset = 0
cdef int x_offset
cdef int old_x_adjustment = 0
cdef int x_adjustment
for g in glyphs:
x_offset = int(dx * g.x / w)
x_adjustment = int(dx * g.x / w)
g.x += x_offset
g.x += x_adjustment
g.y += int(dy * g.y / h)
if x_offset > old_x_offset:
g.delta_x_offset = x_offset - old_x_offset
if x_adjustment > old_x_adjustment:
g.delta_x_adjustment = x_adjustment - old_x_adjustment
old_x_offset = x_offset
old_x_adjustment = x_adjustment
for l in lines:
end = l.y + l.height
@@ -1171,7 +1171,7 @@ def tweak_glyph_spacing(list glyphs, list lines, double dx, double dy, double w,
l.height = end - l.y
def offset_glyphs(list glyphs, int x, int y):
def move_glyphs(list glyphs, int x, int y):
cdef Glyph g
if x == 0 and y == 0:
+4 -1
View File
@@ -224,6 +224,9 @@ def translation_filename(s):
filename = s.elided
if filename.endswith("_ren.py"):
filename = filename[:-7] + ".rpy"
if filename[-1] == "m":
filename = filename[:-1]
@@ -441,7 +444,7 @@ def translate_list_files():
filename = os.path.join(dirname, filename)
if not (filename.endswith(".rpy") or filename.endswith(".rpym")):
if not (filename.endswith(".rpy") or filename.endswith(".rpym") or filename.endswith("_ren.py")):
continue
filename = os.path.normpath(filename)
+8 -3
View File
@@ -22,6 +22,8 @@
from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # *
import renpy
import requests
import re
import os
@@ -126,9 +128,12 @@ def download_ranges(url, ranges, destination, progress_callback=None):
old_ranges = list(ranges)
headers = { 'Range' : 'bytes=' + ', '.join('%d-%d' % (start, end) for start, end in ranges[:10]) }
headers = {
'Range' : 'bytes=' + ', '.join('%d-%d' % (start, end) for start, end in ranges[:10]),
"Accept-Encoding": "identity",
}
r = requests.get(url, headers=headers, stream=True)
r = requests.get(url, headers=headers, stream=True, timeout=10, proxies=renpy.exports.proxies)
r.raise_for_status()
blocks = [ ]
@@ -207,7 +212,7 @@ def download(url, ranges, destination, progress_callback=None):
total_size = sum(i[1] for i in ranges)
downloaded = 0
r = requests.get(url, stream=True)
r = requests.get(url, stream=True, headers={"Accept-Encoding": "identity"}, timeout=10, proxies=renpy.exports.proxies)
r.raise_for_status()
blocks = [ ]
+7 -3
View File
@@ -153,9 +153,13 @@ class Update(object):
Called to initialize the update.
"""
os.makedirs(self.updatedir, exist_ok=True)
os.makedirs(self.blockdir, exist_ok=True)
os.makedirs(self.deleteddir, exist_ok=True)
def makedirs(dir):
if not os.path.isdir(dir):
os.makedirs(dir)
makedirs(self.updatedir)
makedirs(self.blockdir)
makedirs(self.deleteddir)
print("-" * 80, file=self.logfile)
print("Starting update at %s." % time.ctime(), file=self.logfile)
+2 -2
View File
@@ -51,8 +51,8 @@ class Version(object):
Version("main", 3, "8.3.0", "TBD")
Version("main", 2, "7.8.0", "TBD")
Version("fix", 3, "8.2.1", "64bit Sensation")
Version("fix", 2, "7.7.1", "32bit Sensation")
Version("fix", 3, "8.2.2", "64bit Sensation")
Version("fix", 2, "7.7.2", "32bit Sensation")
def make_dict(branch, suffix="00000000", official=False, nightly=False):
+16 -6
View File
@@ -9,6 +9,7 @@ import shutil
import io
import os
import textwrap
import pprint
try:
import builtins
@@ -186,14 +187,16 @@ def write_keywords(srcdir='source'):
with open(outf, "w") as f:
f.write("keywords = %r\n" % kwlist)
f.write("keyword_regex = %r\n" % ("|".join(re.escape(i) for i in kwlist)))
keyword_regex = [ re.escape(i) for i in kwlist ]
f.write("keywords = %s\n" % pprint.pformat(kwlist))
f.write("keyword_regex = %s\n" % pprint.pformat(keyword_regex))
f.write("keyword_regex = '|'.join(keyword_regex)\n")
properties = [ i for i in expanded_sl2_properties() if i not in kwlist ]
f.write("properties = %r\n" % properties)
f.write("property_regexes = %r\n" % sl2_regexps())
f.write("properties = %s\n" % pprint.pformat(properties))
f.write("property_regexes = %s\n" % pprint.pformat(sl2_regexps()))
shutil.copy(outf, os.path.join(srcdir, "../../tutorial/game/keywords.py"))
@@ -256,10 +259,17 @@ def scan(name, o, prefix="", inclass=False):
if doc[0] == ' ':
print("Bad docstring for ", name, repr(doc))
# Cython-generated docstrings start with the function and arguments.
if re.match(r'[\w\.]+\(', doc):
doc = doc.partition("\n\n")[2]
orig = doc
sig, _, doc = doc.partition("\n\n")
doc = textwrap.dedent(doc)
if "(" in sig:
args = "(" + sig.partition("(")[2]
# Break up the doc string, scan it for specials.
lines = [ ]
+1
View File
@@ -18,6 +18,7 @@ init 1000000 python:
doc.scan_section("", renpy.store)
doc.scan_section("renpy.", renpy)
doc.scan_section("renpy.music.", renpy.music)
doc.scan_section("renpy.audio.filter.", renpy.audio.filter)
doc.scan_section("theme.", theme)
doc.scan_section("layout.", layout)
doc.scan_section("define.", define)
+7 -5
View File
@@ -6,7 +6,7 @@
The 3D Stage, named after the stages that plays are performed on, is a concept
that allows displayables to be positioned in 3 dimensions. Ren'Py will then
render the displayables with the proper perspective, and will also make the
z dimension avalable, to make things like lighting and depth rendering
z dimension available, to make things like lighting and depth rendering
possible.
Coordinates
@@ -66,7 +66,7 @@ smaller, while decreasing it will make everything bigger.
Finally, :tpref:`perspective` and :var:`config.perspective` describe the
near and far planes, defaulting to 100 and 100000, respectively. This
means than when an image is closer than 100 z-units from the camera,
means that when an image is closer than 100 z-units from the camera,
it disappears, and it also disappears if it's more than 100,000 z-units
away.
@@ -216,7 +216,7 @@ of the images being displayed.
Ren'Py uses the :tpref:`matrixanchor` transform property to make applying a
matrix easier. This defaults to (0.5, 0.5), and is translated to a pixel offset
inside the image being transformed using the usual Ren'Py anchor rules. (If it's
an integer or abolute, it's considered a number of pixels, otherwise it's a
an integer or absolute, it's considered a number of pixels, otherwise it's a
fraction of the size of the image.)
Ren'Py applies the image by first shifting the image so the anchor is at (0, 0, 0).
@@ -246,7 +246,7 @@ the X axis.
Structural Similarity
^^^^^^^^^^^^^^^^^^^^^^
In ATL, interpolating a the :tpref:`matrixtransform` property requires the
In ATL, interpolating the :tpref:`matrixtransform` property requires the
use of TransformMatrixes that have structural similarity. That means the same
types of TransformMatrix, multiplied together in the same order.
@@ -270,7 +270,7 @@ useful for animating changing transformations. It's also useful to have a way of
taking common matrices and encapsulating them in a way that allows the
matrix to be parameterized.
The TransformMatrix is a base class that is is extended by a number of
The TransformMatrix is a base class that is extended by a number of
Matrix-creating classes. Instances of TransformMatrix are called by Ren'Py,
and return Matrixes. TransformMatrix is well integrated with ATL, allowing
for matrixtransform animations. ::
@@ -291,6 +291,8 @@ This method takes:
Built-In TransformMatrix Subclasses
-----------------------------------
.. seealso:: :class:`SplineMatrix`, which works with TransformMatrix subclasses.
The following is the list of TransformMatrix subclasses that are built into
Ren'Py.
+21 -23
View File
@@ -33,7 +33,7 @@ The transform statement must be run at init time. If it is found outside an
``init`` block, then it is automatically placed inside an ``init`` block with a
priority of 0. The transform may have a list of parameters, which must be
supplied when it is called. Default values for the right-most parameters can
be given by adding "=" and the value (e.g. "transform a (b, c=0):").
be given by adding ``=`` and the value (e.g. ``transform a (b, c=0):``).
`qualname` must be a set of dot-separated Python identifiers. The transform created
by the ATL block is bound to this name, within the given
@@ -102,13 +102,13 @@ Each logical line in an ATL block must contain one or more ATL statements.
There are two kinds of ATL statements: simple and complex. Simple statements
do not take an ATL block. A single logical line may contain one or more ATL
statements, separated by commas. A complex statement contains a block, must
be on its own line. The first line of a complex statement always ends with a
statements, separated by commas. A complex statement always contains a block, and
must be on its own line. The first line of a complex statement always ends with a
colon ``:``.
By default, statements in a block are executed in the order in which they
appear, starting with the first statement in the block. Execution terminates
when the end of the block is reached. Time statements change this, as
when the end of the block is reached. Th ``time`` statements change this, as
described in the appropriate section below.
Execution of a block terminates when all statements in the block have
@@ -229,8 +229,6 @@ Some sample interpolations are::
# Changes xalign and yalign at the same time.
linear 2.0 xalign 1.0 yalign 1.0
# The same thing, using a block.
linear 2.0:
# The same thing, using a block.
linear 2.0:
xalign 1.0
@@ -337,7 +335,7 @@ block ending with ``repeat 2`` will execute at most twice.)
.. productionlist:: atl
atl_repeat : "repeat" (`simple_expression`)?
The repeat statement must be the last statement in a block.::
The repeat statement must be the last statement in a block::
show logo base:
xalign 0.0
@@ -458,7 +456,7 @@ handle a single event name, or a comma-separated list of event names.
The on statement is used to handle events. When an event is handled, handling
of any other event ends and handing of the new event immediately starts. When
an event handler ends without another event occurring, the ``default`` event
is produced (unless were already handing the ``default`` event).
is produced (unless we're already handing the ``default`` event).
Execution of the on statement will never naturally end. (But it can be ended
by the time statement, or an enclosing event handler.)
@@ -623,7 +621,7 @@ example::
pause
This example will cause Eileen to change expression when the first pause
finishes, but will not cause her postion to change, as both animations
finishes, but will not cause her position to change, as both animations
share the same animation time, and hence will place her sprite in the same place.
Without the animation statement, the position would reset when the player
clicks.
@@ -635,11 +633,11 @@ Warpers
=======
A warper is a function that can change the amount of time an interpolation
statement considers to have elapsed. The following warpers are defined by
default. They are defined as functions from t to t', where t and t' are
floating point numbers, with t ranging from 0.0 to 1.0 over the given
amount of time. (If the statement has 0 duration, then t is 1.0 when it runs.)
t' should start at 0.0 and end at 1.0, but can be greater or less.
statement considers to have elapsed. They are defined as functions from t to t',
where t and t' are floating point numbers, with t ranging from 0.0 to 1.0 over
the given amount of time. (If the statement has 0 duration, then t is 1.0 when
it runs.) t' should start at 0.0 and end at 1.0, but can be greater or less.
The following warpers are defined by default.
``pause``
Pause, then jump to the new value. If ``t == 1.0``, ``t' = 1.0``. Otherwise,
@@ -692,7 +690,7 @@ interpreted as a fraction of the size of the containing area (for
:propref:`pos`) or of the displayable (for :propref:`anchor`).
Note that not all properties are independent. For example, :propref:`xalign` and :propref:`xpos`
both update some of the same underlying data. In a parallel statement, not more than
both update some of the same underlying data. In a ``parallel`` statement, not more than
one block should adjust properties sharing the same data. The angle and radius properties set
both horizontal and vertical positions.
@@ -918,7 +916,7 @@ Pixel Effects
The alpha transform is applied to each image comprising the child of
the transform independently. This can lead to unexpected results when
the children overlap, such as as seeing a character through clothing.
the children overlap, such as seeing a character through clothing.
The :func:`Flatten` displayable can help with these problems.
.. transform-property:: additive
@@ -1053,15 +1051,15 @@ Cropping and Resizing
:default: None
If not None, gives the upper-left corner of the crop box. Crop takes
priority over corners. When a float, and crop_relative is enabled,
this is relative to the size of the child.
priority over corners. When a float, and crop_relative is enabled, this
is relative to the size of the child.
.. transform-property:: corner2
:type: None or (position, position)
:default: None
If not None, gives the lower right corner of the crop box. Cropt takes
If not None, gives the lower-right corner of the crop box. Crop takes
priority over corners. When a float, and crop_relative is enabled, this
is relative to the size of the child.
@@ -1187,7 +1185,7 @@ See :ref:`atl-transitions`.
:type: boolean
:default: True
If true, events are passed to the child of this transform. If false,
If True, events are passed to the child of this transform. If False,
events are blocked. (This can be used in ATL transitions to prevent
events from reaching the old_widget.)
@@ -1341,7 +1339,7 @@ The following events can be triggered automatically:
the game is loaded and when styles or translations change.
``hover``, ``idle``, ``selected_hover``, ``selected_idle``, ``insensitive``, ``selected_insensitive``
Triggered when button containing this transform, or a button contained
Triggered when a button containing this transform, or a button contained
by this transform, enters the named state.
@@ -1350,7 +1348,7 @@ The following events can be triggered automatically:
Replacing Transforms
====================
When an an ATL transform or transform defined using the :func:`Transform` class
When an ATL transform or transform defined using the :func:`Transform` class
is replaced by another class, the properties of the transform that's being
replaced are inherited by the transform that's replacing it.
@@ -1453,7 +1451,7 @@ contexts, if the parameter is in the parameter list.
pause .2
child # Go back to the original child.
If can also be used to place the original child inside a ``contains``
It can also be used to place the original child inside a ``contains``
block::
transform marquee(width, height=1.0, duration=2.0, child=None):
+80
View File
@@ -0,0 +1,80 @@
Audio Filters
==============
Audio filters allow a game to change the sound on a channel from what is
present in an audio file. This can be used to add effects to the sound,
including reverb, delay/echo, and high-pass/low-pass filters.
Audio filters are placed in the renpy.audio.filter module. Although
not strictly required, it is recommended that module be aliased to something
shorter, like ``af``, using::
define af = renpy.audio.filter
The filters can then be invoked using the :func:`renpy.music.set_audio_filter`
function, like::
$ renpy.music.set_audio_filter("music", af.Reverb(0.5))
By default, the filter takes effect at the start of the next audio file
queued or played. If you want to apply the filter to the currently playing
and queued audio, you can use the `replace` argument::
$ renpy.music.set_audio_filter("music", af.Lowpass(440), replace=True)
This will apply the filter to the audio as soon as possible.
Finally, you can remove a filter from a channel by passing None as the filter::
$ renpy.music.set_audio_filter("music", None)
(It's also possible to give None with `replace` set to True.)
Filter Reuse
------------
When a filter is set on a channel, it will filter all audio played on
that channel. Specifically, the Comb, Delay, and Reverb filters will
continue to output information from old audio files for some time
after a new audio file has started playing, provided the filter is
not changed.
This means that Ren'Py is storing audio information inside the filter
object. Because of this, it is generally not a good idea to share filter
objects between channels, or to use a filter object multiple times with
a single channel.
Silence Padding
---------------
When an audio filter is active and the last queued sound on a channel finises
playing, Ren'Py will add 2 seconds of silence to the channel, to ensure that
delay and reverb filters have time to finish playing.
If you need more silence, it can be queued with::
queue sound "<silence 10">
Audio Filters
-------------
Whenever a filter is accepted, a list may be given instead, to represent a
:class:`renpy.audio.filter.Sequence` of those filters. So writing::
renpy.music.set_audio_filter("music", [af.Reverb(0.5), af.Lowpass(440)])
is equivalent to::
renpy.music.set_audio_filter("music",
af.Sequence([
af.Reverb(0.5),
af.Lowpass(440),
]))
Apart from that, the audio filters consist of the following classes. It's not
possible to define your own, for performance reasons.
.. include:: inc/audio_filter
+2 -2
View File
@@ -338,13 +338,13 @@ There are two variables that can be used to provide information about
the build. This information is used to generate the game/cache/build_info.json
file, which is loaded as Ren'Py starts.
.. var:: time = None
.. var:: build.time = None
This variable defaults to None, but if your game has been built,
it will be set to the time the game was built, in seconds since
January 1, 1970.
.. var:: info = { }
.. var:: build.info = { }
This variable lets you store information that will be placed into
the game/cache/build_info.json file in the built game. When the built
+158 -3
View File
@@ -4,6 +4,161 @@ Changelog (Ren'Py 7.x-)
*There is also a list of* :doc:`incompatible changes <incompatible>`
.. _renpy-8.3.0:
.. _renpy-7.8.0:
8.3.0 / 7.8.0
=============
Features
--------
The :class:`SplineMatrix` class has been added, which makes it possible to
transform matrices in a non-linear way.
The Input displayable now takes an `action` property, which is an action that
runs when the user presses enter with the text input active.
Launcher Changes
----------------
Under navigation, the files view now has a checkbox that allows a creator to
filter out translation files.
Other Changes
-------------
The :func:`renpy.fetch` function can now take user-specified headers that
are supplied as part of the HTTP/HTTPS request.
Bar Values that set values (like :class:`DictValue`, :class:`FieldValue`,
:class:`VariableValue`, :class:`ScreenVariableValue`, and :class:`LocalVariableValue`)
now take a `min` and `max` parameters, which can be used to directly set the bar's
endpoints.
The :propref:`keyboard_focus_insets` style property makes it possible to
have keyboard focus work with overlapping buttons, by artificially reducing
the size of the buttons to remove the overlap, when determining keyboard focus.
The `synchro_start` option (documented as part of :func:`renpy.music.play`) is
now True by default in that function, and in the ``play`` statement.
.. _renpy-8.2.2:
.. _renpy-7.7.2:
8.2.2 / 7.7.2
=============
Fixes
-----
List slicing is now allowed inside string interpolation. For example,
``The first ten are: [long_list[:10]]`` will now work.
Ren'Py will now generate translations for strings in _ren.py files.
Ren'Py now checks that achievment names are strings.
An issue with weakref pickling on Ren'Py 7 has been fixed.
The ``rpy`` statement is now considered to be always reachable.
The launcher no longer plays a stream of silence while it is running.
When building a small games as an Android App Bundle, fast-forward packages were
incorrectly included. This has been fixed.
.. _renpy-8.2.1:
.. _renpy-7.7.1:
8.2.1 / 7.7.1
=============
Text
----
The Harfbuzz text shaper now reads more information using Harfbuzz. This
will generally yield the same results, with small exceptions, such as
the underline being in a slightly different place.
Vertical text handling under the harfbuzz text shaper has been fixed to
properly place the text. Porting those changes to the freetype shaper
is not possible, so the freetype shaper no longer supports vertical text.
See :propref:`vertical` for more information.
Updater
-------
An issue with the updater that caused it to fail to sign updates when
run on a Windows system has been fixed.
The updater now forces the webserver to use the identity encoding, which
improves compatibility with some web servers. The updater also times out
if the server does not respond to a request within 10 seconds.
Live2D
------
Ren'Py will now automatically guess the size of the live2d textures,
and adjust the maximum texture size the live2d library uses to match
it.
Ren'Py will avoid many render-to-texture operations when showing
Live2D.
Fetch
-----
The :func:`renpy.fetch` function now works during the image phase and
during an interaction, as well as outside an interaction.
The :func:`renpy.fetch` function now takes a `params` argument, which
specifies parameters that will be added to the URL.
Other Changes
-------------
When a textbox is replaced (using {w}), a ``replaced`` event is generated,
rather than hide.
Adding a new displayable with `default_focus` set will cause the
displayable to be focused, if the keyboard or gamepad is used, even
if the interaction does not restart.
It's now possible to build an iOS app from the command line without
installing rapt (Android support).
The renamed and newly-documented :var:`config.max_texture_size` variable
make it possible to set the maximum texture size used by Ren'Py. This isn't
useful for 2D textures, but may make sense for textures used by :class:`Model`.
:doc:`template_projects` are no longer required to have the same files
as a standard Ren'Py project.
Other Fixes
-----------
An issue that could cause an Android device to reach a black screen when
resuming from pause has been fixed.
Ren'Py will now run from a directory with : in the name, on Linux and other
platforms where that's legal.
The use of :var:`config.layer_transforms` will no longer reset the timelines
of transforms set with ``camera`` or ``show layer`` ``at``.
Lint no longer crashes when the a LayeredImage use a variable that isn't set.
A crash when :tpref:`blur` was less than 0 has been prevented, by clamping
the blur value.
An issue that caused drags to block saving has been fixed.
.. _renpy-8.2.0:
.. _renpy-7.7.0:
@@ -18,7 +173,7 @@ versions of Ren'Py, Harfbuzz is used to supply additional information
to the freetype authinter.
On Ren'Py 8, Harfbuzz is also used to shape text, reordering and selecting
gtlphs based on the context they're in and the language of the text provided.
glyphs based on the context they're in and the language of the text provided.
This is required to support scripts that require complex text shaping,
such as Brahmic/Indic scripts. (You'll need to provide a font that
supports the appropriate language.)
@@ -334,8 +489,8 @@ The new :var:`config.pass_controller_events` and newly-documented
:var:`config.pass_joystick_events` variables allow the game to access
controller and joystick events directly.
The new :var:`renpy.get_screen_variable` and :var:`renpy.get_screen_variable`
make it possible to access screen variables, especially in :class:`Action`
The new :var:`renpy.get_screen_variable` and :var:`renpy.set_screen_variable`
functions make it possible to access screen variables, especially in :class:`Action`
subclasses.
The new :var:`build.time` variable is set to the time the game was built.
+4 -4
View File
@@ -20,7 +20,7 @@ you to develop Ren'Py games on your Chromebook.
To install it:
1. Install Linux for Chrombebook, as described at https://support.google.com/chromebook/answer/9145439?hl=en .
1. Install Linux for Chromebook, as described at https://support.google.com/chromebook/answer/9145439?hl=en .
2. Change the Crosstini GPU Support setting to enabled, by typing chrome://flags/#crostini-gpu-support, and choosing enable.
@@ -47,9 +47,9 @@ To run that version of Ren'Py, open a terminal and run::
./renpy.sh
Note that this works with other versions of Ren'Py if you change 8.2.0
for a later versoon.
for a later version.
A SDK installed in this way can be used to run games that do not natively
support ARM chromebooks on an ARM chromebook. Just unpack the game into
An SDK installed in this way can be used to run games that do not natively
support ARM Chromebooks on an ARM Chromebook. Just unpack the game into
the projects directory and hit refresh, then launch it through the
Ren'Py launcher. (The projects directory can be set in preferences.)
+8
View File
@@ -729,6 +729,14 @@ Occasionally Used
The number of seconds to take to fade in :var:`config.main_menu_music`.
.. var:: config.max_texture_size = (4096, 4096)
The maximum size of an image that Ren'Py will load as a single texture.
This is important for 3d models, while 2d images will be split into
multiple textures if necessary.
Live2d will adjust this to fit the largest live2d texture.
.. var:: config.menu_arguments_callback = None
If not None, this should be a function that takes positional and/or
+4
View File
@@ -67,6 +67,8 @@ the omission in future versions.
* Derik
* Diapolo10
* DinakiS
* Dipesh Aggarwal
* Do10HM
* Dogtopus
* Doomfest
* Donghyeok Tak
@@ -113,6 +115,8 @@ the omission in future versions.
* Huanxuantian
* Hyper Sonic
* Ian Leslie
* Iivusly
* ImJustAQ
* JackkelDragon
* Jackmcbarn
* Jacob Kauffmann
+7 -7
View File
@@ -106,7 +106,7 @@ For example::
label start:
show eileen mad
show eileen concerned
e "I'm a little upset at you."
e happy "But it's just a passing thing."
@@ -117,15 +117,15 @@ is equivalent to::
label start:
show eileen mad
show eileen concerned
e "I'm a little upset at you."
show eileen happy
e "But it's just a passing thing."
In the above example, the ``mad`` and ``happy`` replace one another.
In the above example, the ``concerned`` and ``happy`` replace one another.
But it is possible to revert to a ``happy``\ -less eileen without specifying
the ``mad`` attribute. An attribute name prepended with the minus sign ( - )
the ``concerned`` attribute. An attribute name prepended with the minus sign ( - )
has that effect, just as it does with the :ref:`show statement <show-statement>`.
For example::
@@ -135,7 +135,7 @@ For example::
label start:
show eileen
e mad "I'm a little upset at you."
e concerned "I'm a little upset at you."
e happy "That's funny."
@@ -150,7 +150,7 @@ For example, the following code is equivalent to the previous example::
label start:
show eileen mad
show eileen concerned
e "I'm a little upset at you."
e @ happy "That's funny."
@@ -164,7 +164,7 @@ the @, and temporary ones coming after. ::
The minus sign can also be used after the @ sign::
e @ right -mad "My anger is temporarily suspended..."
e @ right -concerned "My anger is temporarily suspended..."
e "HOWEVER !"
To cause a transition to occur whenever the images are changed in this way, set
+1 -1
View File
@@ -99,7 +99,7 @@ tutorial game::
e "To play this game, you'll need to download some data. If you're not on WiFi, you could be charged for it. Tap the screen to proceed."
$ update.continue_game_download()
$ updater.continue_game_download()
This is pretty simple, but there are a few things. The first is that it sets
:var:`config.save` to False. This is important, as it disables saving and
+1
View File
@@ -4,6 +4,7 @@
GUI Customization Guide
=======================
.. ifconfig:: not renpy_figures
.. note::
+12
View File
@@ -28,6 +28,18 @@ renderer.
Support for Window 7, 8, and 8.1 will be dropped after May 2024, to allow the
use of versions of Python that only support Windows 10 and later.
.. _incompatible-8.2.1:
.. _incompatible-7.7.1:
8.2.1 / 7.7.1
--------------
***Vertical Text** Vertical text had been improved in the harfbuzz shaper,
with the text now being rendered in the correct place. This may cause
position changes, but since the previous version was wildly incorrect,
no compatibility define is provided.
.. _incompatible-8.2.0:
.. _incompatible-7.7.0:
+1
View File
@@ -42,6 +42,7 @@ To find out more about Ren'Py, please visit the Ren'Py home page:
python
conditional
audio
audio_filters
movie
voice
+4488 -4
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -176,7 +176,7 @@ The following labels are used by Ren'Py:
If it exists, this label is called when a game is loaded. It can be
use to fix data when the game is updated. If data is changed by this
label, :func:`renpy.block_rollback` should be called to prevent those
changes from being reverted inf the player rolls back past the load
changes from being reverted if the player rolls back past the load
point.
``splashscreen``
+3
View File
@@ -187,6 +187,9 @@ Ren'Py statements are made of a few basic parts.
image tag is ``mary``, while the image attributes are,
``beach``, ``night``, and ``happy``.
The words ``at``, ``as``, ``behind``, ``onlayer``, ``with``, and ``zorder``, may
not be used as parts of an image name.
:dfn:`String`
A string begins with a quote character (one of ", ', or \`), contains some
sequence of characters, and ends with the same quote character.
+26 -21
View File
@@ -40,7 +40,7 @@ consists of the following things:
per model. A texture is a rectangle containing image data that's been loaded
on the GPU, either directly or using a render-to-texture operation.
* A list of shader part names. Ren'Py uses these shader parts to created shaders,
* A list of shader part names. Ren'Py uses these shader parts to create shaders,
which are programs that are run on the GPU to render the model. Shader part
names can be prefixed with a "-" to prevent that shader part from being used.
@@ -72,7 +72,7 @@ as described below.) A Render contains:
Ren'Py draws the screen by performing a depth-first walk through the tree of
Renders, until a Model is encountered. During this walk, Ren'Py updates a
matrix transforming the location of the Model, a clipping polygon, and
lists of shader parts, uniforms, and gl properties. When a Model is encountered
lists of shader parts, uniforms, and GL properties. When a Model is encountered
as part of this walk, the appropriate shader program is activated on the GPU,
all information is transferred, and a drawing operation occurs.
@@ -104,7 +104,7 @@ Live2D
one Model for each layer.
:func:`Transform` and ATL
A Transform creates a model if :tpref:`mesh` is true, or if :tpref:`blur`
A Transform creates a model if :tpref:`mesh` is True, or if :tpref:`blur`
is being used. In this case, the children of the Transform are rendered
to textures, with the mesh of the first texture being used for the mesh
associated with the model.
@@ -116,8 +116,8 @@ Live2D
:class:`Render`
A Transform creates a model if its ``mesh`` attribute is True.
is being used. In this case, the children of the Render are rendered
to textures, with the mesh of the first texture being used for
In this case, the children of the Render are rendered to
textures, with the mesh of the first texture being used for
the mesh associated with the model.
It's expected that Ren'Py will add more ways of creating models in the
@@ -137,11 +137,11 @@ leading "-". (So "-renpy.geometry" will cause itself and "renpy.geometry"
to be removed.)
Ren'Py then takes the list of shader parts, and retrieves lists of variables,
functions, vertex shade parts, and fragment shader parts. These are, in turn,
functions, vertex shader parts, and fragment shader parts. These are, in turn,
used to generate the source code for shaders, with the parts of the vertex and
fragement shaders being included in low-number to high-number priority order.
This means that any variable created by one of the shader will be accessible
This means that any variable created by one of the shaders will be accessible
by every other fragment from any other shader in the list of shader parts.
There is no scope like in Python functions to protect interference between
shaders.
@@ -235,7 +235,7 @@ Model-Based rendering adds the following properties to ATL and :func:`Transform`
If not None, this Transform will be rendered as a model. This means:
* A mesh will be created. If this is a 2-component tuple, it's taken
as the number of points in the mesh, in the x and y directions. (Eacn
as the number of points in the mesh, in the x and y directions. (Each
dimension must be at least 2.) If True, the mesh is taken from the
child.
* The child of this transform will be rendered to a texture.
@@ -246,15 +246,15 @@ Model-Based rendering adds the following properties to ATL and :func:`Transform`
:type: None or tuple
:default: None
If not None, this can either be a 2 or 4 component tuple. If mesh is
true and this is given, this applies padding to the size of the textues
applied to the the textures used by the mesh. A two component tuple applies
padding to the right and bottom, while a four component tuple applies
If not None, this can either be a 2 or 4-component tuple. If mesh is
True and this is given, this applies padding to the size of the textures
applied to the the textures used by the mesh. A 2-component tuple applies
padding to the right and bottom, while a 4-component tuple applies
padding to the left, top, right, and bottom.
This can be used, in conjunction with the ``gl_pixel_perfect`` property,
to render text into a mesh. In Ren'Py, text is rendered at the screen
resoltution, which might overflow the boundaries of the texture that
resolution, which might overflow the boundaries of the texture that
will be applied to the mesh. Adding a few pixels of padding makes the
texture bigger, which will display all pixels. For example::
@@ -273,7 +273,7 @@ Model-Based rendering adds the following properties to ATL and :func:`Transform`
:default: None
If not None, a shader part name or list of shader part names that will be
applied to the this Render (if a Model is created) or the Models reached
applied to this Render (if a Model is created) or the Models reached
through this Render.
.. transform-property:: blend
@@ -345,6 +345,11 @@ The following uniforms are made available to all Models.
to the bottom-left corner of the window. u_viewport.pq is the width
and height of the viewport.
``vec2 u_drawable_size``
The size of the drawable are of the windows, in pixels, at the resolution
the game is running at. For example, if a 1280x720 game is scaled up to
1980x1080, this will be (1920, 1080).
``sampler2D tex0``, ``sampler2D tex1``, ``sampler2D tex2``
If textures are available, the corresponding samplers are placed in
this variable.
@@ -398,11 +403,11 @@ function.
``gl_color_mask``
This is expecting to be a 4-tuple of booleans, corresponding to the four
channels of a pixel (red, green, blue, and alpha). If a given channel is
true, the draw operation will write to that pixel. Otherwise, it will
True, the draw operation will write to that pixel. Otherwise, it will
not.
``gl_depth``
If true, this will clear the depth buffer, and then enable depth
If True, this will clear the depth buffer, and then enable depth
rendering for this displayable and the children of this displayable.
Note that drawing any pixel, even transparent pixels, will update
@@ -420,8 +425,8 @@ by a Transform with :tpref:`mesh` set, or by :func:`Model`, where these
can be supplied the property method.
``gl_drawable_resolution``
If true or not set, the texture is rendered at the same resolution
as the window displaying the game. If false, it's rendered at the
If True or not set, the texture is rendered at the same resolution
as the window displaying the game. If False, it's rendered at the
virtual resolution of the displayable.
``gl_anisotropic``
@@ -430,19 +435,19 @@ can be supplied the property method.
texels (texture pixels) to be sampled when a texture is zoomed by a
different amount in X and Y.
This defaults to true. Ren'Py sets this to False for certain effects,
This defaults to True. Ren'Py sets this to False for certain effects,
like the Pixellate transition.
``gl_mipmap``
If supplied, this determines if the textures supplied to a mesh are
created with mipmaps. This defaults to true.
created with mipmaps. This defaults to True.
``gl_texture_wrap``
When supplied, this determines how the textures applied to a mesh
are wrapped. This expects a 2-component tuple, where the first
component is used to set GL_TEXTURE_WRAP_S and the second component
is used to set GL_TEXTURE_WRAP_T, which conventionally are the X and Y
axes of the created textyure.
axes of the created texture.
The values should be OpenGL constants imported from renpy.uguu::
+2 -3
View File
@@ -82,12 +82,11 @@ can then change it again.)
If None, Ren'Py will attempt to draw at the monitor's full framerate.
.. var:: preferences.gl_powersave = "auto"
.. var:: preferences.gl_powersave = True
This determines how often Ren'Py will redraw an unchanging screen. If True,
Ren'Py will only draw the screen 5 times a second. If False, it will always
draw at the full framerate possible. If "auto", it will draw at full speed
when the device is powered, and 5hz when it is running on battery.
draw at the full framerate possible.
.. var:: preferences.gl_tearing = False
+2 -2
View File
@@ -6,7 +6,7 @@ In more complicated games, it's possible to have files that consist of mostly
Python, with a small number of Ren'Py statements, like ``init python:``, to
introduce the Python code to Ren'Py. Ren'Py has an alternative way to write
these Python-heavy files. Files with a name ending in ``_ren.py`` can be
writen in Python syntax, which is then transformed to Ren'Py script and
written in Python syntax, which is then transformed to Ren'Py script and
processed.
There are two main reasons to take advantage of this:
@@ -15,7 +15,7 @@ There are two main reasons to take advantage of this:
needs to be placed before every line of Python.
* Editors can open \_ren.py files using tools that are specialized for
Python, allowing the editor to perform code analysis and refactoring
operations that aren't avalable for Python-in-Ren'Py.
operations that aren't available for Python-in-Ren'Py.
Ren'Py in Python files have names that end with \_ren.py, for example,
``actions_ren.py``. These files are processed in the same unicode order
+10
View File
@@ -919,6 +919,16 @@ The input statement takes no parameters, and the following properties:
using keyboard (Shift+Enter by default,
can be changed by modifying config.keymap['input_next_line']).
.. screen-property:: action
If not None, an action that is run when enter is pressed and the
input is active. This overrides the default action of returning
the input value.
Generally, this is is used with a `value` that stores the input into
a variable, so the action can access it.
It also takes:
File diff suppressed because it is too large Load Diff
+27 -1
View File
@@ -794,6 +794,21 @@ Text Style Properties
If True, the text will be rendered vertically.
There are multiple caveats:
* If :propref:`shaper` is "freetype", vertical layout will likely be incorrect.
* Harfbuzz will convert horizontal forms to vertical forms if those
forms are present in the font.
* Characters will be laid out top to bottom, right to left, but will
not be rotated. This means that horizontal characters will be laid
out one on top of another.
* If vertical text information is not present in the font, it will be
synthesized, but may be incorrect. Generally, ideographic text works
better than non-ideographic text.
.. _window-style-properties:
Window Style Properties
@@ -929,7 +944,18 @@ Button Style Properties
If True, the default, this button can be focused using the keyboard focus
mechanism, if it can be focused at all. If False, the keyboard focus
mechanism will skip this button. (The keyboard focus mechanism is used
by keyboards and keyboard-like devices, such as joypads.)
by keyboards and keyboard-like devices, such as gamepads.)
.. style-property:: keyboard_focus_insets (int, int, int, int) or None
If not None, this should be a tuple of four integers, giving the
number of pixels that are used to shrink the left, top, right, and
bottom sides of the focus rectangle, when it's used for keyboard
focus.
This can be useful when buttons overlap. The keyboard focus algorithm
doesn't work with overlapping buttons, and so this can be used to to
shrink the size of the buttons internally, allowing focus to work.
.. style-property:: key_events boolean
+1 -1
View File
@@ -12,7 +12,7 @@ template project. If they do, the template project will be copied into
the new project, and the following changes will be made:
* The :var:`config.name`, :var:`config.save_directory`, and
:var:`config.build_name` variables will be set to the name of
:var:`build.name` variables will be set to the name of
the new project.
* The tl directory will be removed, and replaced with default translations
+2 -2
View File
@@ -237,8 +237,8 @@ and hide statements. This transition can be enabled by setting the
The transition will occur after one or more ``scene``, ``show``, or ``hide`` statements,
provided the statement are not followed by a with statement, or a transition
caused by :ref`dialogue window management <dialogue-window-management>`, like
the various ``window`` statements. It's also disabled when in a menu context.
caused by :ref:`dialogue-window-management`, like the various ``window`` statements.
It's also disabled when in a menu context.
For example::

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