Merge branch 'master' into lint-labels-required-params
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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?")):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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,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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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()):
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -189,6 +189,7 @@ Keyword("value")
|
||||
Keyword("mask")
|
||||
Keyword("caret_blink")
|
||||
Keyword("multiline")
|
||||
Keyword("action")
|
||||
Style("caret")
|
||||
add(text_properties)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [ ]
|
||||
|
||||
@@ -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
@@ -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
@@ -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 = [ ]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
GUI Customization Guide
|
||||
=======================
|
||||
|
||||
|
||||
.. ifconfig:: not renpy_figures
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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``
|
||||
|
||||
@@ -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
@@ -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::
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
+503
-492
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user