Compare commits
222 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d8644f2f8 | |||
| c7a284c1e4 | |||
| 8b79a3663c | |||
| fdf38adc83 | |||
| 921feb4ada | |||
| f367e996aa | |||
| c0c182d200 | |||
| a5fec5a7a5 | |||
| 0f668f2810 | |||
| 5cba186326 | |||
| 74631fe701 | |||
| 13eeb9d0c0 | |||
| 1066157585 | |||
| 7742627373 | |||
| 95102a6b71 | |||
| acac446c8b | |||
| 9fc8e43f17 | |||
| 6d6003f4e0 | |||
| 7ff1b3b5c5 | |||
| 4fd318880e | |||
| 6cd57b192a | |||
| 8c6aab0bc6 | |||
| 2451b3f0da | |||
| daea1375c3 | |||
| ada2b860f6 | |||
| 6758447cad | |||
| 035d6e7b30 | |||
| 0675f6ce44 | |||
| c7317c076b | |||
| 6bd420b421 | |||
| c1f2db72b5 | |||
| f87917e399 | |||
| 6bc50d6454 | |||
| a2bfa02de1 | |||
| a3ad875263 | |||
| 80d3075bf0 | |||
| 0b213cc7db | |||
| 23fc2f66c9 | |||
| 19498d5bce | |||
| 7f37382012 | |||
| 7fcad404b6 | |||
| d8ad7d1672 | |||
| e4b550b8c3 | |||
| 67bdb9ab03 | |||
| 6f44533bbe | |||
| d084b624b9 | |||
| 06d3a90bd7 | |||
| 5e64403699 | |||
| d8954aa732 | |||
| 669b07afef | |||
| e2bc3fb55c | |||
| 088b1572ef | |||
| a30f562d38 | |||
| 4768a012db | |||
| c7d8bd1dc9 | |||
| be226363e8 | |||
| 267a807146 | |||
| 34468cf57d | |||
| 172f9f926e | |||
| f6afced012 | |||
| 00cd7e4ada | |||
| 9f4ab7d761 | |||
| 9ac420c259 | |||
| 6c548d30ad | |||
| 615c6a728d | |||
| bb8c508837 | |||
| 30833f1f5f | |||
| 57bf18c705 | |||
| e30855cb93 | |||
| ed0f536853 | |||
| e4f601310b | |||
| b92107dd02 | |||
| 19c5e806b1 | |||
| f6d9762c5c | |||
| 8b679cca71 | |||
| 34fe132e55 | |||
| 0036bc3fe9 | |||
| d321d1ff51 | |||
| a98d15eed4 | |||
| 57dba01448 | |||
| 3532b589d5 | |||
| 51cc42a901 | |||
| e09b87dffe | |||
| c5752398b9 | |||
| d5fe9c9a11 | |||
| b8ca91a62a | |||
| 5aec41f9e1 | |||
| b1d1db1075 | |||
| 2708eda3d3 | |||
| ad83e0354e | |||
| 1c823a4e7c | |||
| 11186c86ae | |||
| 6014bfa007 | |||
| ef0d2db11d | |||
| 7dd1db50d2 | |||
| c8413912d2 | |||
| 969b2ccecf | |||
| 228c72f6c1 | |||
| ad9af51510 | |||
| 45faa71da8 | |||
| e530c78cc0 | |||
| c3a26654f7 | |||
| 14710606ef | |||
| 72cb9e3929 | |||
| 9c3b082260 | |||
| 46cfe60086 | |||
| 588d1e1137 | |||
| 15a12efd61 | |||
| b0434ea1d9 | |||
| 96b44cdcb2 | |||
| 1fde5f985a | |||
| 34dcdc7f0e | |||
| 2238b460a7 | |||
| ff8ba2d10c | |||
| 5e3495c333 | |||
| 79945d9649 | |||
| 254fa8c4ee | |||
| 14b596e15a | |||
| f33aef4711 | |||
| 7d86e8509a | |||
| dd26bbf859 | |||
| cde1f301d2 | |||
| 2d8e7d3a1a | |||
| 1c2b3ed231 | |||
| 021aa22a80 | |||
| e548ed13b2 | |||
| b1cf7eb64e | |||
| 86e9454b79 | |||
| f7d2434dff | |||
| 07c96e8872 | |||
| 0f51948f80 | |||
| 8448808c67 | |||
| 8da08dd187 | |||
| eedc86f20e | |||
| ccb978f454 | |||
| 33830fd6b4 | |||
| 615c624a70 | |||
| 66f17ae6dd | |||
| 7bf7303f85 | |||
| 4d8d065c89 | |||
| fc111cc0ab | |||
| b535e2b63c | |||
| fb294567fe | |||
| b2893b5ea8 | |||
| 5853d63076 | |||
| bd7cd2a0c6 | |||
| 9d42e5c667 | |||
| ab9ee40e1a | |||
| 0aa28b6804 | |||
| 0891fff309 | |||
| ca37ebacd1 | |||
| 51aff5d94d | |||
| 7ff6acc36c | |||
| cda1c18e03 | |||
| 7f615dcddc | |||
| 646a7535b0 | |||
| 4600339cf7 | |||
| 03fdf2801e | |||
| e427b4e910 | |||
| 0826778a5e | |||
| 5050ee20ee | |||
| b4cf19cb0c | |||
| dee35cfc66 | |||
| 325175f3c3 | |||
| 787f09929b | |||
| c5f9ddeae8 | |||
| c43c34c943 | |||
| ce07691bbc | |||
| 9e76a31da1 | |||
| 90701d03c1 | |||
| 0256f2bba7 | |||
| a12d9c544e | |||
| 8bb5291324 | |||
| 186b082e3c | |||
| 4dd40736a1 | |||
| 8c3741e6f9 | |||
| 6e7f28f574 | |||
| fa24e47d29 | |||
| 12acbfc963 | |||
| 8e1945a628 | |||
| fdae52f6ad | |||
| 4b47d59b3b | |||
| 9cde14fa6d | |||
| a98fa5f7fe | |||
| bf0bc1d837 | |||
| db24c1eb3a | |||
| c8a88bb17e | |||
| 9c73cff8ce | |||
| 5a4b14009e | |||
| a4cb1a123e | |||
| 057f69602b | |||
| 2040da88db | |||
| 82de5b7af0 | |||
| a688409eb9 | |||
| 2ec1cb0cdc | |||
| a73082df6f | |||
| e33888949a | |||
| 6422153ce1 | |||
| 9d1263d7c3 | |||
| 608cbab10c | |||
| 2b08c40a4a | |||
| bd01358481 | |||
| 1e518437dd | |||
| 2096c691ce | |||
| 6b4eadf0fd | |||
| fc8ac8344d | |||
| f6d8afdad6 | |||
| 420564557f | |||
| a8cab927bb | |||
| 81ee1d994e | |||
| 52c1e64b6f | |||
| 3ef755d5d4 | |||
| e719dd8d47 | |||
| ab3f1242d7 | |||
| 8df82cbac0 | |||
| 13c557b5a2 | |||
| 44e32ddffb | |||
| 60745892c1 | |||
| d511804be3 | |||
| 487c60da2d | |||
| 1a375c525c | |||
| 553ada3b04 |
@@ -103,6 +103,9 @@ cache/
|
||||
# Pygame_sdl2.
|
||||
/pygame_sdl2
|
||||
|
||||
# Updater.
|
||||
update.pem
|
||||
|
||||
# Steam.
|
||||
steam_appid.txt
|
||||
/steamapi.py
|
||||
@@ -127,3 +130,6 @@ CubismSdkForNative-4-*.zip
|
||||
# Works in progress, throwaway scripts, etc.
|
||||
/scratch
|
||||
/*-dists
|
||||
/c
|
||||
/cg
|
||||
/.vscode/launch.json
|
||||
|
||||
+100
-24
@@ -584,6 +584,9 @@ change_renpy_executable()
|
||||
self.include_update = build['include_update']
|
||||
self.build_update = self.include_update and build_update
|
||||
|
||||
if self.include_update:
|
||||
self.make_key_pem()
|
||||
|
||||
# The various executables, which change names based on self.executable_name.
|
||||
self.app = self.executable_name + ".app"
|
||||
self.exe = self.executable_name + ".exe"
|
||||
@@ -681,11 +684,12 @@ change_renpy_executable()
|
||||
dlc=p["dlc"])
|
||||
|
||||
if self.build_update and p["update"]:
|
||||
self.make_package(
|
||||
p["name"],
|
||||
"update",
|
||||
p["file_lists"],
|
||||
dlc=p["dlc"])
|
||||
for update_format in self.list_update_formats():
|
||||
self.make_package(
|
||||
p["name"],
|
||||
update_format,
|
||||
p["file_lists"],
|
||||
dlc=p["dlc"])
|
||||
|
||||
wait_parallel_threads()
|
||||
|
||||
@@ -704,6 +708,23 @@ change_renpy_executable()
|
||||
if open_directory:
|
||||
renpy.run(store.OpenDirectory(self.destination, absolute=True))
|
||||
|
||||
def list_update_formats(self):
|
||||
"""
|
||||
Returns a list of update formats to build.
|
||||
"""
|
||||
|
||||
rv = [ ]
|
||||
|
||||
for update_format in self.build["update_formats"]:
|
||||
if update_format == "rpu":
|
||||
rv.append("rpu")
|
||||
elif update_format == "zsync":
|
||||
rv.append("update")
|
||||
else:
|
||||
raise Exception("Unknown update format: " + update_format)
|
||||
|
||||
return rv
|
||||
|
||||
def scan_and_classify(self, directory, patterns):
|
||||
"""
|
||||
Walks through the `directory`, finds files and directories that
|
||||
@@ -1425,6 +1446,7 @@ change_renpy_executable()
|
||||
|
||||
FORMATS = {
|
||||
"update" : (".update", False, False, False),
|
||||
"rpu" : ("", False, False, False),
|
||||
|
||||
"tar.bz2" : (".tar.bz2", False, False, True),
|
||||
"zip" : (".zip", False, False, True),
|
||||
@@ -1474,7 +1496,7 @@ change_renpy_executable()
|
||||
|
||||
update_fn = os.path.join(self.destination, filename + ".update.json")
|
||||
|
||||
if self.include_update and (variant not in [ 'ios', 'android', 'source']) and (not format.startswith("app-")):
|
||||
if self.include_update and not format.startswith("app-"):
|
||||
|
||||
with open(update_fn, "wb" if PY2 else "w") as f:
|
||||
json.dump(update, f, indent=2)
|
||||
@@ -1483,6 +1505,9 @@ change_renpy_executable()
|
||||
fl.append(File("update", None, True, False))
|
||||
fl.append(File("update/current.json", update_fn, False, False))
|
||||
|
||||
if not dlc:
|
||||
fl.append(File("update/key.pem", self.temp_filename("key.pem"), False, False))
|
||||
|
||||
# If we're not an update file, prepend the directory.
|
||||
if (not dlc) and prepend:
|
||||
fl.prepend_directory(filename)
|
||||
@@ -1493,6 +1518,10 @@ change_renpy_executable()
|
||||
full_filename = filename + ext
|
||||
path += ext
|
||||
|
||||
if format == "rpu":
|
||||
full_filename = "rpu/" + variant + ".files.rpu"
|
||||
path = self.destination + "/" + full_filename
|
||||
|
||||
if self.build['renpy']:
|
||||
fl_hash = fl.hash(self)
|
||||
else:
|
||||
@@ -1529,6 +1558,8 @@ change_renpy_executable()
|
||||
pkg = TarPackage(path, "w:bz2")
|
||||
elif format == "update":
|
||||
pkg = UpdatePackage(path, filename, self.destination)
|
||||
elif format == "rpu":
|
||||
pkg = RPUPackage(self.destination, variant)
|
||||
elif format == "zip" or format == "app-zip" or format == "bare-zip":
|
||||
if self.build['renpy']:
|
||||
pkg = ExternalZipPackage(path)
|
||||
@@ -1565,13 +1596,13 @@ change_renpy_executable()
|
||||
|
||||
self.reporter.progress_done()
|
||||
|
||||
|
||||
if format == "update":
|
||||
# Build the zsync file.
|
||||
|
||||
self.reporter.info(_("Making the [variant] update zsync file."), variant=variant)
|
||||
|
||||
pkg.close()
|
||||
def close_progress(done, total):
|
||||
self.reporter.progress(_("Finishing the [variant] [format] package."), done, total, variant=variant, format=format)
|
||||
|
||||
pkg.close(close_progress)
|
||||
|
||||
if done is not None:
|
||||
done()
|
||||
@@ -1592,33 +1623,78 @@ change_renpy_executable()
|
||||
|
||||
def add_variant(variant):
|
||||
|
||||
digest = self.build_cache[self.base_name + "-" + variant + ".update"][0]
|
||||
|
||||
sums_size = os.path.getsize(self.destination + "/" + self.base_name + "-" + variant + ".sums")
|
||||
|
||||
index[variant] = {
|
||||
"version" : self.update_versions[variant],
|
||||
"pretty_version" : self.pretty_version,
|
||||
"digest" : digest,
|
||||
"zsync_url" : self.base_name + "-" + variant + ".zsync",
|
||||
"sums_url" : self.base_name + "-" + variant + ".sums",
|
||||
"sums_size" : sums_size,
|
||||
"json_url" : self.base_name + "-" + variant + ".update.json",
|
||||
}
|
||||
"renpy_version" : renpy.version_only,
|
||||
}
|
||||
|
||||
fn = renpy.fsencode(os.path.join(self.destination, self.base_name + "-" + variant + ".update"))
|
||||
if "update" in self.build["update_formats"]:
|
||||
|
||||
if os.path.exists(fn):
|
||||
os.unlink(fn)
|
||||
digest = self.build_cache[self.base_name + "-" + variant + ".update"][0]
|
||||
sums_size = os.path.getsize(self.destination + "/" + self.base_name + "-" + variant + ".sums")
|
||||
|
||||
index[variant].update({
|
||||
"digest" : digest,
|
||||
"zsync_url" : self.base_name + "-" + variant + ".zsync",
|
||||
"sums_url" : self.base_name + "-" + variant + ".sums",
|
||||
"sums_size" : sums_size,
|
||||
"json_url" : self.base_name + "-" + variant + ".update.json",
|
||||
})
|
||||
|
||||
fn = renpy.fsencode(os.path.join(self.destination, self.base_name + "-" + variant + ".update"))
|
||||
|
||||
if os.path.exists(fn):
|
||||
os.unlink(fn)
|
||||
|
||||
if "rpu" in self.build["update_formats"]:
|
||||
|
||||
rpu_size = 0
|
||||
|
||||
rpu_dir = os.path.join(self.destination, "rpu")
|
||||
|
||||
for i in os.listdir(rpu_dir):
|
||||
rpu_size += os.path.getsize(os.path.join(rpu_dir, i))
|
||||
|
||||
index[variant]["rpu_url"] = "rpu/" + variant + ".files.rpu"
|
||||
index[variant]["rpu_digest"] = self.build_cache["rpu/" + variant + ".files.rpu"][0]
|
||||
index[variant]["rpu_size"] = rpu_size
|
||||
|
||||
for p in packages:
|
||||
if p["update"]:
|
||||
add_variant(p["name"])
|
||||
|
||||
update_data = json.dumps(index, indent=2)
|
||||
|
||||
fn = renpy.fsencode(os.path.join(self.destination, "updates.json"))
|
||||
with open(fn, "wb" if PY2 else "w") as f:
|
||||
json.dump(index, f, indent=2)
|
||||
f.write(update_data)
|
||||
|
||||
# Write the signed file.
|
||||
import ecdsa
|
||||
|
||||
with open(self.find_update_pem(), "rb") as f:
|
||||
signing_key = ecdsa.SigningKey.from_pem(f.read())
|
||||
|
||||
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")))
|
||||
|
||||
def find_update_pem(self):
|
||||
if self.build['renpy']:
|
||||
return os.path.join(config.renpy_base, "update.pem")
|
||||
else:
|
||||
return os.path.join(self.project.path, "update.pem")
|
||||
|
||||
def make_key_pem(self):
|
||||
import ecdsa
|
||||
|
||||
with open(self.find_update_pem(), "rb") as f:
|
||||
signing_key = ecdsa.SigningKey.from_pem(f.read())
|
||||
|
||||
key_pem = self.temp_filename("key.pem")
|
||||
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']:
|
||||
|
||||
@@ -267,7 +267,23 @@ label start_update_old_game:
|
||||
call update_old_game
|
||||
jump build_distributions
|
||||
|
||||
label add_update_pem:
|
||||
|
||||
python hide:
|
||||
interface.info("You're trying to build an update, but an update.pem file doesn't exist.\n\nThis file is used to sign updates, and will be automatically created in your projects's base directory.\n\nYou'll need to back up update.pem and keep it safe.", cancel=Jump("build_distributions"))
|
||||
|
||||
import ecdsa
|
||||
key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p).to_pem()
|
||||
|
||||
with open(os.path.join(project.current.path, "update.pem"), "wb") as f:
|
||||
f.write(key)
|
||||
|
||||
return
|
||||
|
||||
label start_distribute:
|
||||
if project.current.dump["build"]["include_update"] and not os.path.exists(os.path.join(project.current.path, "update.pem")):
|
||||
call add_update_pem
|
||||
|
||||
if project.current.data["add_from"]:
|
||||
call add_from_common
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ label lint:
|
||||
interface.processing(_("Checking script for potential problems..."))
|
||||
lint_fn = project.current.temp_filename("lint.txt")
|
||||
|
||||
project.current.launch([ 'lint', lint_fn ], wait=True)
|
||||
project.current.launch([ 'lint', lint_fn, ] + list(persistent.lint_options), wait=True)
|
||||
|
||||
e = renpy.editor.editor
|
||||
e.begin(True)
|
||||
|
||||
@@ -81,7 +81,6 @@ init python:
|
||||
|
||||
label install_live2d:
|
||||
python hide:
|
||||
|
||||
if PY2:
|
||||
_prefix = r"lib/py2-"
|
||||
else:
|
||||
@@ -98,6 +97,11 @@ label install_live2d:
|
||||
(r".*/Core/dll/android/(x86_64/libLive2DCubismCore.so)", r"rapt/prototype/renpyandroid/src/main/jniLibs/\1"),
|
||||
]
|
||||
|
||||
if PY2:
|
||||
patterns.extend([
|
||||
(r".*/Core/dll/windows/x86/(Live2DCubismCore.dll)", _prefix + r"windows-i686/\1"),
|
||||
])
|
||||
|
||||
install_from_zip("Live2D Cubism SDK for Native", "CubismSdkForNative-4-*.zip", patterns)
|
||||
|
||||
jump front_page
|
||||
|
||||
@@ -492,7 +492,7 @@ init python in interface:
|
||||
The amount of time to pause for after showing the message.
|
||||
"""
|
||||
|
||||
common(title, store.INTERACTION_COLOR, message, submessage=None, pause=pause, show_screen=True, **kwargs)
|
||||
common(title, store.INTERACTION_COLOR, message, submessage=submessage, pause=pause, show_screen=True, **kwargs)
|
||||
renpy.pause(pause)
|
||||
|
||||
def processing(message, submessage=None, complete=None, total=None, **kwargs):
|
||||
|
||||
@@ -62,6 +62,9 @@ label new_project:
|
||||
if persistent.projects_directory is None:
|
||||
$ interface.error(_("The projects directory could not be set. Giving up."))
|
||||
|
||||
if PY2:
|
||||
$ interface.info(_("Warning : you are using Ren'Py 7. It is recommended to start new projects using Ren'Py 8 instead."))
|
||||
|
||||
python:
|
||||
if persistent.legacy:
|
||||
|
||||
|
||||
@@ -242,10 +242,12 @@ init python:
|
||||
# allows the updater to run.
|
||||
build.include_update = True
|
||||
|
||||
# Build both kinds of updates.
|
||||
build.update_formats = [ "zsync", "rpu" ]
|
||||
|
||||
# Allow empty directories, so we can distribute the images directory.
|
||||
build.exclude_empty_directories = False
|
||||
|
||||
|
||||
# Mac signing options.
|
||||
import os
|
||||
build.mac_identity = os.environ.get("RENPY_MAC_IDENTITY", None)
|
||||
|
||||
@@ -33,7 +33,6 @@ init python in distribute:
|
||||
|
||||
from zipfile import crc32
|
||||
|
||||
|
||||
# Since the long type doesn't exist on py3, define it here
|
||||
if not PY2:
|
||||
long = int
|
||||
@@ -195,7 +194,7 @@ init python in distribute:
|
||||
|
||||
self.zipfile.write_with_info(zi, path)
|
||||
|
||||
def close(self):
|
||||
def close(self, progress=None):
|
||||
self.zipfile.close()
|
||||
|
||||
|
||||
@@ -243,9 +242,10 @@ init python in distribute:
|
||||
def add_directory(self, name, path):
|
||||
self.add_file(name, path, True)
|
||||
|
||||
def close(self):
|
||||
def close(self, progress=None):
|
||||
self.tarfile.close()
|
||||
|
||||
|
||||
class UpdatePackage(TarPackage):
|
||||
|
||||
def __init__(self, filename, basename, destination):
|
||||
@@ -255,7 +255,7 @@ init python in distribute:
|
||||
|
||||
TarPackage.__init__(self, filename, "w", notime=True)
|
||||
|
||||
def close(self):
|
||||
def close(self, progress=None):
|
||||
TarPackage.close(self)
|
||||
|
||||
cmd = [
|
||||
@@ -283,7 +283,6 @@ init python in distribute:
|
||||
sums.write(struct.pack("<I", zlib.adler32(data) & 0xffffffff))
|
||||
|
||||
|
||||
|
||||
class DirectoryPackage(object):
|
||||
|
||||
def mkdir(self, path):
|
||||
@@ -315,9 +314,10 @@ init python in distribute:
|
||||
fn = os.path.join(self.path, name)
|
||||
self.mkdir(fn)
|
||||
|
||||
def close(self):
|
||||
def close(self, progress=None):
|
||||
return
|
||||
|
||||
|
||||
class ExternalZipPackage(object):
|
||||
|
||||
def __init__(self, path):
|
||||
@@ -331,7 +331,7 @@ init python in distribute:
|
||||
def add_directory(self, name, path):
|
||||
self.dp.add_directory(name, path)
|
||||
|
||||
def close(self):
|
||||
def close(self, progress=None):
|
||||
self.dp.close()
|
||||
|
||||
if os.path.exists(self.path):
|
||||
@@ -350,17 +350,40 @@ init python in distribute:
|
||||
shutil.rmtree(self.directory)
|
||||
|
||||
|
||||
|
||||
class DMGPackage(DirectoryPackage):
|
||||
def __init__(self, path, make_dmg):
|
||||
self.make_dmg = make_dmg
|
||||
DirectoryPackage.__init__(self, path)
|
||||
|
||||
def close(self):
|
||||
def close(self, progress=None):
|
||||
DirectoryPackage.close(self)
|
||||
self.make_dmg()
|
||||
|
||||
|
||||
class RPUPackage(object):
|
||||
|
||||
def __init__(self, directory, variant):
|
||||
import renpy.update.common
|
||||
|
||||
self.directory = directory + "/rpu"
|
||||
self.variant = variant
|
||||
self.file_list = renpy.update.common.FileList()
|
||||
|
||||
if not os.path.exists(self.directory):
|
||||
os.mkdir(self.directory)
|
||||
|
||||
def add_file(self, name, path, xbit):
|
||||
self.file_list.add_file(name, path, xbit)
|
||||
|
||||
def add_directory(self, name, _path):
|
||||
self.file_list.add_directory(name)
|
||||
|
||||
def close(self, progress=None):
|
||||
import renpy.update.generate
|
||||
|
||||
renpy.update.generate.BlockGenerator(self.variant, self.file_list, self.directory, progress)
|
||||
|
||||
|
||||
parallel_threads = [ ]
|
||||
|
||||
class ParallelPackage(object):
|
||||
@@ -378,7 +401,7 @@ init python in distribute:
|
||||
def add_directory(self, name, path):
|
||||
self.worklist.append((True, name, path, True))
|
||||
|
||||
def close(self):
|
||||
def close(self, progress=None):
|
||||
t = threading.Thread(target=self.run)
|
||||
t.start()
|
||||
|
||||
|
||||
@@ -19,15 +19,15 @@
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
default persistent.show_edit_funcs = True
|
||||
default persistent.windows_console = False
|
||||
default persistent.lint_options = { # the ones which should be enabled by default
|
||||
"--orphan-tl",
|
||||
}
|
||||
|
||||
init python:
|
||||
from math import ceil
|
||||
|
||||
if persistent.show_edit_funcs is None:
|
||||
persistent.show_edit_funcs = True
|
||||
|
||||
if persistent.windows_console is None:
|
||||
persistent.windows_console = False
|
||||
|
||||
def scan_translations(piglatin=True):
|
||||
|
||||
languages = renpy.known_languages()
|
||||
@@ -64,6 +64,9 @@ default persistent.daily_update_check_once = False
|
||||
# Keep the default update check from triggering until tomorrow.
|
||||
default persistent.last_update_check = datetime.date.today()
|
||||
|
||||
# Should we try to skip the splashscreen?
|
||||
default persistent.skip_splashscreen = False
|
||||
|
||||
init python:
|
||||
if not persistent.daily_update_check_once:
|
||||
persistent.daily_update_check_once = True
|
||||
@@ -71,6 +74,14 @@ init python:
|
||||
|
||||
|
||||
default preference_tab = "general"
|
||||
define preference_tabs = {
|
||||
"general" : _("General"),
|
||||
"options" : _("Options"),
|
||||
"theme" : _("Theme"),
|
||||
"install" : _("Install Libraries"),
|
||||
"actions" : _("Actions"),
|
||||
"lint" : _("Lint Options"),
|
||||
}
|
||||
|
||||
screen preferences():
|
||||
|
||||
@@ -98,17 +109,12 @@ screen preferences():
|
||||
|
||||
has vbox
|
||||
|
||||
# Projects directory selection.
|
||||
add SEPARATOR2
|
||||
|
||||
add HALF_SPACER
|
||||
|
||||
textbutton _("General") action SetVariable("preference_tab", "general") style "l_list"
|
||||
textbutton _("Options") action SetVariable("preference_tab", "options") style "l_list"
|
||||
textbutton _("Theme") action SetVariable("preference_tab", "theme") style "l_list"
|
||||
textbutton _("Install Libraries") action SetVariable("preference_tab", "install") style "l_list"
|
||||
textbutton _("Actions") action SetVariable("preference_tab", "actions") style "l_list"
|
||||
|
||||
for i, l in preference_tabs.items():
|
||||
textbutton l action SetVariable("preference_tab", i) style "l_list"
|
||||
|
||||
if preference_tab == "general":
|
||||
|
||||
@@ -119,7 +125,6 @@ screen preferences():
|
||||
|
||||
has vbox
|
||||
|
||||
# Projects directory selection.
|
||||
add SEPARATOR2
|
||||
|
||||
|
||||
@@ -207,6 +212,22 @@ screen preferences():
|
||||
add SPACER
|
||||
add SEPARATOR2
|
||||
|
||||
frame:
|
||||
style "l_indent"
|
||||
has vbox
|
||||
|
||||
text _("Game Options:")
|
||||
|
||||
add HALF_SPACER
|
||||
|
||||
if renpy.windows:
|
||||
textbutton _("Console output") style "l_checkbox" action ToggleField(persistent, "windows_console")
|
||||
|
||||
textbutton _("Skip splashscreen") style "l_checkbox" action ToggleField(persistent, "skip_splashscreen")
|
||||
|
||||
add SPACER
|
||||
add SEPARATOR2
|
||||
|
||||
frame:
|
||||
style "l_indent"
|
||||
has vbox
|
||||
@@ -218,15 +239,11 @@ screen preferences():
|
||||
textbutton _("Show edit file section") style "l_checkbox" action ToggleField(persistent, "show_edit_funcs")
|
||||
textbutton _("Large fonts") style "l_checkbox" action [ ToggleField(persistent, "large_print"), renpy.utter_restart ]
|
||||
|
||||
if renpy.windows:
|
||||
textbutton _("Console output") style "l_checkbox" action ToggleField(persistent, "windows_console")
|
||||
|
||||
textbutton _("Sponsor message") style "l_checkbox" action ToggleField(persistent, "sponsor_message")
|
||||
|
||||
if ability.can_update:
|
||||
textbutton _("Daily check for update") style "l_checkbox" action [ToggleField(persistent, "daily_update_check"), SetField(persistent, "last_update_check", None)] selected persistent.daily_update_check
|
||||
|
||||
|
||||
elif preference_tab == "theme":
|
||||
|
||||
frame:
|
||||
@@ -236,7 +253,6 @@ screen preferences():
|
||||
|
||||
has vbox
|
||||
|
||||
# Projects directory selection.
|
||||
add SEPARATOR2
|
||||
|
||||
frame:
|
||||
@@ -276,7 +292,6 @@ screen preferences():
|
||||
|
||||
use install_preferences
|
||||
|
||||
|
||||
elif preference_tab == "actions":
|
||||
|
||||
frame:
|
||||
@@ -300,6 +315,39 @@ screen preferences():
|
||||
textbutton _("Reset window size") style "l_nonbox" action Preference("display", 1.0)
|
||||
textbutton _("Clean temporary files") style "l_nonbox" action Jump("clean_tmp")
|
||||
|
||||
elif preference_tab == "lint":
|
||||
|
||||
frame:
|
||||
style "l_indent"
|
||||
xmaximum TWOTHIRDS
|
||||
xfill True
|
||||
|
||||
has vbox
|
||||
|
||||
add SEPARATOR2
|
||||
|
||||
frame:
|
||||
style "l_indent"
|
||||
has vbox
|
||||
|
||||
text _("Lint toggles:")
|
||||
|
||||
add HALF_SPACER
|
||||
|
||||
textbutton _("Orphan translations"):
|
||||
style "l_checkbox"
|
||||
action ToggleSetMembership(persistent.lint_options, "--orphan-tl")
|
||||
textbutton _("Parameters overriding builtin names"):
|
||||
style "l_checkbox"
|
||||
action ToggleSetMembership(persistent.lint_options, "--builtins-parameters")
|
||||
textbutton _("Word count and character count for speaking characters"):
|
||||
style "l_checkbox"
|
||||
action ToggleSetMembership(persistent.lint_options, "--words-char-count")
|
||||
|
||||
add SPACER
|
||||
|
||||
textbutton _("Check Script (Lint)") action Jump("lint")
|
||||
|
||||
|
||||
textbutton _("Return") action Jump("front_page") style "l_left_button"
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ init python in project:
|
||||
|
||||
def __init__(self, path, name=None):
|
||||
|
||||
if name is None:
|
||||
name = os.path.basename(path)
|
||||
|
||||
while path.endswith("/"):
|
||||
path = path[:-1]
|
||||
|
||||
if name is None:
|
||||
name = os.path.basename(path)
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise Exception("{} does not exist.".format(path))
|
||||
|
||||
@@ -260,6 +260,10 @@ init python in project:
|
||||
|
||||
environ = dict(os.environ)
|
||||
environ["RENPY_LAUNCHER_LANGUAGE"] = _preferences.language or "english"
|
||||
|
||||
if persistent.skip_splashscreen:
|
||||
environ["RENPY_SKIP_SPLASHSCREEN"] = "1"
|
||||
|
||||
environ.update(env)
|
||||
|
||||
# Filter out system PYTHON* environment variables.
|
||||
@@ -766,7 +770,12 @@ init python in project:
|
||||
blurb = LAUNCH_BLURBS[persistent.blurb % len(LAUNCH_BLURBS)]
|
||||
persistent.blurb += 1
|
||||
|
||||
interface.interaction(_("Launching"), blurb, pause=2.5)
|
||||
if persistent.skip_splashscreen:
|
||||
submessage = _("Splashscreen skipped in launcher preferences.")
|
||||
else:
|
||||
submessage = None
|
||||
|
||||
interface.interaction(_("Launching"), blurb, submessage=submessage, pause=2.5)
|
||||
|
||||
|
||||
def __call__(self):
|
||||
|
||||
@@ -1590,7 +1590,7 @@
|
||||
|
||||
# game/gui7.rpy:339
|
||||
old "Custom. The GUI is optimized for a 16:9 aspect ratio."
|
||||
new "Personnalisé. Le GUI est optimié pour un ratio de cadre 16:9."
|
||||
new "Personnalisé. Le GUI est optimisé pour un ratio de cadre 16:9."
|
||||
|
||||
# game/gui7.rpy:355
|
||||
old "WIDTH"
|
||||
@@ -2147,3 +2147,23 @@
|
||||
# game/androidstrings.rpy:69
|
||||
old "I found a bundle.keystore file in the rapt directory. Do you want to use this file?"
|
||||
new "J'ai trouvé un fichier bundle.keystore dans le dossier \"rapt\". Voulez-vous l'utiliser ?"
|
||||
|
||||
# game/choose_directory.rpy:72
|
||||
old "No directory was selected, but one is required."
|
||||
new "Un chemin d'accès est nécessaire, mais aucun n'a été fourni."
|
||||
|
||||
# game/choose_directory.rpy:80
|
||||
old "The selected directory does not exist."
|
||||
new "Le dossier sélectionné n'existe pas."
|
||||
|
||||
# game/choose_directory.rpy:82
|
||||
old "The selected directory is not writable."
|
||||
new "Le dossier sélectionné n'est pas ouvert en écriture."
|
||||
|
||||
# game/distribute.rpy:554
|
||||
old "This may be derived from build.name and config.version or build.version."
|
||||
new "Cette variable peut être dérivée de build.name, et de config.version ou build.version."
|
||||
|
||||
# game/new_project.rpy:66
|
||||
old "Warning : you are using Ren'Py 7. It is recommended to start new projects using Ren'Py 8 instead."
|
||||
new "Attention : vous utilisez Ren'Py 7. Il est recommandé d'utiliser Ren'Py 8 pour de nouveaux projets."
|
||||
|
||||
@@ -19,7 +19,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00action_file.rpy:26
|
||||
old "{#weekday}Friday"
|
||||
new "{#weekday}П'ятниця"
|
||||
new "{#weekday}П’ятниця"
|
||||
|
||||
# 00action_file.rpy:26
|
||||
old "{#weekday}Saturday"
|
||||
@@ -59,51 +59,51 @@ translate ukrainian strings:
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}January"
|
||||
new "{#month}Січня"
|
||||
new "{#month}січня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}February"
|
||||
new "{#month}Лютого"
|
||||
new "{#month}лютого"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}March"
|
||||
new "{#month}Березня"
|
||||
new "{#month}березня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}April"
|
||||
new "{#month}Квітня"
|
||||
new "{#month}квітня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}May"
|
||||
new "{#month}Травня"
|
||||
new "{#month}травня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}June"
|
||||
new "{#month}Червня"
|
||||
new "{#month}червня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}July"
|
||||
new "{#month}Липня"
|
||||
new "{#month}липня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}August"
|
||||
new "{#month}Серпня"
|
||||
new "{#month}серпня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}September"
|
||||
new "{#month}Вересня"
|
||||
new "{#month}вересня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}October"
|
||||
new "{#month}Жовтня"
|
||||
new "{#month}жовтня"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}November"
|
||||
new "{#month}Листопада"
|
||||
new "{#month}листопада"
|
||||
|
||||
# 00action_file.rpy:47
|
||||
old "{#month}December"
|
||||
new "{#month}Грудня"
|
||||
new "{#month}грудня"
|
||||
|
||||
# 00action_file.rpy:63
|
||||
old "{#month_short}Jan"
|
||||
@@ -155,19 +155,19 @@ translate ukrainian strings:
|
||||
|
||||
# 00action_file.rpy:240
|
||||
old "%b %d, %H:%M"
|
||||
new "%d %b, %H:%M"
|
||||
new "%b %d, %H:%M"
|
||||
|
||||
# 00action_file.rpy:353
|
||||
old "Save slot %s: [text]"
|
||||
new "Слот збереження %s: [text]"
|
||||
new "Зберегти комірку %s: [text]"
|
||||
|
||||
# 00action_file.rpy:434
|
||||
old "Load slot %s: [text]"
|
||||
new "Слот завантаження %s: [text]"
|
||||
new "Завантажити комірку %s: [text]"
|
||||
|
||||
# 00action_file.rpy:487
|
||||
old "Delete slot [text]"
|
||||
new "Видалити слот [text]"
|
||||
new "Видалити комірку [text]"
|
||||
|
||||
# 00action_file.rpy:569
|
||||
old "File page auto"
|
||||
@@ -179,19 +179,19 @@ translate ukrainian strings:
|
||||
|
||||
# 00action_file.rpy:573
|
||||
old "File page [text]"
|
||||
new "Сторінка збережень [text]"
|
||||
new "Сторінка [text]"
|
||||
|
||||
# 00action_file.rpy:763
|
||||
old "Next file page."
|
||||
new "Наступна сторінка збережень."
|
||||
new "Наступна сторінка."
|
||||
|
||||
# 00action_file.rpy:827
|
||||
old "Previous file page."
|
||||
new "Минула сторінка збережень"
|
||||
new "Попередня сторінка"
|
||||
|
||||
# 00action_file.rpy:888
|
||||
old "Quick save complete."
|
||||
new "Швидке збереження завершено."
|
||||
new "Швидке збереження виконано."
|
||||
|
||||
# 00action_file.rpy:906
|
||||
old "Quick save."
|
||||
@@ -207,7 +207,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00director.rpy:708
|
||||
old "The interactive director is not enabled here."
|
||||
new "Інтерактивний директор недоступний."
|
||||
new "Інтерактивний режисер тут не ввімкнений."
|
||||
|
||||
# 00director.rpy:1481
|
||||
old "⬆"
|
||||
@@ -219,7 +219,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00director.rpy:1551
|
||||
old "Done"
|
||||
new "Прийняти"
|
||||
new "Готово"
|
||||
|
||||
# 00director.rpy:1561
|
||||
old "(statement)"
|
||||
@@ -231,7 +231,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00director.rpy:1563
|
||||
old "(attributes)"
|
||||
new "(атрибут)"
|
||||
new "(атрибути)"
|
||||
|
||||
# 00director.rpy:1564
|
||||
old "(transform)"
|
||||
@@ -247,7 +247,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00director.rpy:1602
|
||||
old "(filename)"
|
||||
new "(ім'я файла)"
|
||||
new "(ім’я файла)"
|
||||
|
||||
# 00director.rpy:1631
|
||||
old "Change"
|
||||
@@ -263,19 +263,19 @@ translate ukrainian strings:
|
||||
|
||||
# 00director.rpy:1639
|
||||
old "Remove"
|
||||
new "Видалити"
|
||||
new "Вилучити"
|
||||
|
||||
# 00director.rpy:1674
|
||||
old "Statement:"
|
||||
new "Функції:"
|
||||
new "Функція:"
|
||||
|
||||
# 00director.rpy:1695
|
||||
old "Tag:"
|
||||
new "Теги:"
|
||||
new "Тег:"
|
||||
|
||||
# 00director.rpy:1711
|
||||
old "Attributes:"
|
||||
new "Атрибут:"
|
||||
new "Атрибути:"
|
||||
|
||||
# 00director.rpy:1729
|
||||
old "Transforms:"
|
||||
@@ -287,15 +287,15 @@ translate ukrainian strings:
|
||||
|
||||
# 00director.rpy:1767
|
||||
old "Transition:"
|
||||
new "Переходи:"
|
||||
new "Перехід:"
|
||||
|
||||
# 00director.rpy:1785
|
||||
old "Channel:"
|
||||
new "Канали:"
|
||||
new "Канал:"
|
||||
|
||||
# 00director.rpy:1803
|
||||
old "Audio Filename:"
|
||||
new "Ім'я файла:"
|
||||
new "Ім’я аудіофайлу::"
|
||||
|
||||
# 00gui.rpy:370
|
||||
old "Are you sure?"
|
||||
@@ -303,63 +303,63 @@ translate ukrainian strings:
|
||||
|
||||
# 00gui.rpy:371
|
||||
old "Are you sure you want to delete this save?"
|
||||
new "Ви впевнені що хочете видалити це збереження?"
|
||||
new "Ви дійсно бажаєте видалити збереження?"
|
||||
|
||||
# 00gui.rpy:372
|
||||
old "Are you sure you want to overwrite your save?"
|
||||
new "Ви впевнені що хочете перезаписати ваше збереження?"
|
||||
new "Ви дійсно бажаєте перезаписати збереження?"
|
||||
|
||||
# 00gui.rpy:373
|
||||
old "Loading will lose unsaved progress.\nAre you sure you want to do this?"
|
||||
new "Завантаження гри приведе до втрати незбереженого прогресу.\nВи впевнені що хочете це зробити?"
|
||||
new "Завантаження гри приведе до втрати незбереженого прогресу.\nВи дійсно бажаєте це зробити?"
|
||||
|
||||
# 00gui.rpy:374
|
||||
old "Are you sure you want to quit?"
|
||||
new "Ви впевнені що бажаєте вийти?"
|
||||
new "Ви дійсно бажаєте вийти?"
|
||||
|
||||
# 00gui.rpy:375
|
||||
old "Are you sure you want to return to the main menu?\nThis will lose unsaved progress."
|
||||
new "Ви впевнені, що хочете повернутися до головного меню?\nЦе призведе до втрати незбереженого прогресу."
|
||||
new "Ви дійсно бажаєте повернутися до головного меню?\nЦе призведе до втрати незбереженого прогресу."
|
||||
|
||||
# 00gui.rpy:376
|
||||
old "Are you sure you want to end the replay?"
|
||||
new "Ви впевнені, що хочете завершити повтор?"
|
||||
new "Ви дійсно бажаєте закінчити повтор?"
|
||||
|
||||
# 00gui.rpy:377
|
||||
old "Are you sure you want to begin skipping?"
|
||||
new "Ви впевнені, що бажаєте пропустити?"
|
||||
new "Ви дійсно бажаєте почати пропускати?"
|
||||
|
||||
# 00gui.rpy:378
|
||||
old "Are you sure you want to skip to the next choice?"
|
||||
new "Ви точно хочете пропустити все до наступного вибору?"
|
||||
new "Ви дійсно бажаєте пропустити все до наступного вибору?"
|
||||
|
||||
# 00gui.rpy:379
|
||||
old "Are you sure you want to skip unseen dialogue to the next choice?"
|
||||
new "Ви впевнені, що хочете пропустити непрочитані діалоги до наступного вибору?"
|
||||
new "Ви дійсно бажаєте пропустити непрочитані діалоги до наступного вибору?"
|
||||
|
||||
# 00keymap.rpy:258
|
||||
old "Failed to save screenshot as %s."
|
||||
new "Провалено спробу зберегти скріншот як %s."
|
||||
new "Не вдалося зберегти знімок екрана як %s."
|
||||
|
||||
# 00keymap.rpy:270
|
||||
old "Saved screenshot as %s."
|
||||
new "Скріншот збережений як %s."
|
||||
new "Знімок екрана збережено як %s."
|
||||
|
||||
# 00library.rpy:146
|
||||
old "Self-voicing disabled."
|
||||
new "Синтезатор мови вимкнено."
|
||||
new "Синтез мовлення вимкнено."
|
||||
|
||||
# 00library.rpy:147
|
||||
old "Clipboard voicing enabled. "
|
||||
new "Озвучення буфера обміну включено."
|
||||
new "Озвучення буфера обміну увімкнено."
|
||||
|
||||
# 00library.rpy:148
|
||||
old "Self-voicing enabled. "
|
||||
new "Синтезатор мови включений."
|
||||
new "Синтез мовлення увімкнено."
|
||||
|
||||
# 00library.rpy:150
|
||||
old "bar"
|
||||
new "Смуга налаштування"
|
||||
new "смуга"
|
||||
|
||||
# 00library.rpy:151
|
||||
old "selected"
|
||||
@@ -371,39 +371,39 @@ translate ukrainian strings:
|
||||
|
||||
# 00library.rpy:153
|
||||
old "horizontal scroll"
|
||||
new "горизонтальна смуга прокручування"
|
||||
new "горизонтальне прокручування"
|
||||
|
||||
# 00library.rpy:154
|
||||
old "vertical scroll"
|
||||
new "вертикальна смуга прокручування"
|
||||
new "вертикальне прокручування"
|
||||
|
||||
# 00library.rpy:155
|
||||
old "activate"
|
||||
new "елемент активовано"
|
||||
new "увімкнути"
|
||||
|
||||
# 00library.rpy:156
|
||||
old "deactivate"
|
||||
new "елемент деактивовано"
|
||||
new "вимкнути"
|
||||
|
||||
# 00library.rpy:157
|
||||
old "increase"
|
||||
new "більше"
|
||||
new "збільшити"
|
||||
|
||||
# 00library.rpy:158
|
||||
old "decrease"
|
||||
new "менше"
|
||||
new "зменшити"
|
||||
|
||||
# 00library.rpy:193
|
||||
old "Skip Mode"
|
||||
new "Режим Пропуску"
|
||||
new "Режим пропуску"
|
||||
|
||||
# 00library.rpy:279
|
||||
old "This program contains free software under a number of licenses, including the MIT License and GNU Lesser General Public License. A complete list of software, including links to full source code, can be found {a=https://www.renpy.org/l/license}here{/a}."
|
||||
new "Ця програма містить вільне та відкрите програмне забезпечення під кількома ліцензіями, включаючи ліцензію MIT та GNU Lesser General Public. Повний список ліцензій, враховуючи посилання на повний вихідний код, можна знайти {a=https://www.renpy.org/l/license}тут{/a}."
|
||||
new "Ця програма містить вільне програмне забезпечення під низкою ліцензій, зокрема ліцензією MIT та GNU Lesser General Public License. Повний список ПЗ, включно з посиланнями на повний вихідний код, можна знайти {a=https://www.renpy.org/l/license}тут{/a}."
|
||||
|
||||
# 00preferences.rpy:207
|
||||
old "display"
|
||||
new "режим екрана"
|
||||
new "режим показу"
|
||||
|
||||
# 00preferences.rpy:219
|
||||
old "transitions"
|
||||
@@ -415,11 +415,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00preferences.rpy:230
|
||||
old "video sprites"
|
||||
new "відео-спрайти"
|
||||
new "відео спрайти"
|
||||
|
||||
# 00preferences.rpy:239
|
||||
old "show empty window"
|
||||
new "показувати порожнє вікно діалогу"
|
||||
new "показати порожнє вікно"
|
||||
|
||||
# 00preferences.rpy:248
|
||||
old "text speed"
|
||||
@@ -443,11 +443,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00preferences.rpy:271
|
||||
old "skip unseen text"
|
||||
new "пропускати весь текст"
|
||||
new "пропускати увесь текст"
|
||||
|
||||
# 00preferences.rpy:273
|
||||
old "begin skipping"
|
||||
new "почати пропуск"
|
||||
new "почати пропускати"
|
||||
|
||||
# 00preferences.rpy:277
|
||||
old "after choices"
|
||||
@@ -459,23 +459,23 @@ translate ukrainian strings:
|
||||
|
||||
# 00preferences.rpy:286
|
||||
old "auto-forward time"
|
||||
new "швидкість авточитання"
|
||||
new "швидкість перемотки"
|
||||
|
||||
# 00preferences.rpy:300
|
||||
old "auto-forward"
|
||||
new "авточитання"
|
||||
new "перемотка"
|
||||
|
||||
# 00preferences.rpy:307
|
||||
old "Auto forward"
|
||||
new "Авточитання"
|
||||
new "Перемотка"
|
||||
|
||||
# 00preferences.rpy:310
|
||||
old "auto-forward after click"
|
||||
new "продовжувати авточитання після кліку"
|
||||
new "перемотка після дотику"
|
||||
|
||||
# 00preferences.rpy:319
|
||||
old "automatic move"
|
||||
new "автоматично пересувати мишу до кнопки" ###
|
||||
new "автоматичний рух"
|
||||
|
||||
# 00preferences.rpy:328
|
||||
old "wait for voice"
|
||||
@@ -483,23 +483,23 @@ translate ukrainian strings:
|
||||
|
||||
# 00preferences.rpy:337
|
||||
old "voice sustain"
|
||||
new "не зупиняти голос"
|
||||
new "підтримка голосу"
|
||||
|
||||
# 00preferences.rpy:346
|
||||
old "self voicing"
|
||||
new "озвучка через синтезатор мови"
|
||||
new "синтез мовлення"
|
||||
|
||||
# 00preferences.rpy:355
|
||||
old "clipboard voicing"
|
||||
new "синтез мови з буфера обміну"
|
||||
new "озвучення буфера обміну"
|
||||
|
||||
# 00preferences.rpy:364
|
||||
old "debug voicing"
|
||||
new "режим дебагу синтезу мови"
|
||||
new "режим налагодження синтезу мови"
|
||||
|
||||
# 00preferences.rpy:373
|
||||
old "emphasize audio"
|
||||
new "посилити гучність заздалегідь заданих звукових каналів рахунок приглушення інших каналів"
|
||||
new "виділити звук"
|
||||
|
||||
# 00preferences.rpy:382
|
||||
old "rollback side"
|
||||
@@ -507,15 +507,15 @@ translate ukrainian strings:
|
||||
|
||||
# 00preferences.rpy:392
|
||||
old "gl powersave"
|
||||
new "налаштування графіки. Економія енергії"
|
||||
new "економія енергії"
|
||||
|
||||
# 00preferences.rpy:398
|
||||
old "gl framerate"
|
||||
new "налаштування графіки. Частота кадрів"
|
||||
new "частота кадрів"
|
||||
|
||||
# 00preferences.rpy:401
|
||||
old "gl tearing"
|
||||
new "налаштування графіки. Розривання кадрів"
|
||||
new "розрив кадрів"
|
||||
|
||||
# 00preferences.rpy:413
|
||||
old "music volume"
|
||||
@@ -523,11 +523,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00preferences.rpy:414
|
||||
old "sound volume"
|
||||
new "гучність звуків"
|
||||
new "гучність звук. ефектів"
|
||||
|
||||
# 00preferences.rpy:415
|
||||
old "voice volume"
|
||||
new "гучність голосу"
|
||||
new "гучність озвучення"
|
||||
|
||||
# 00preferences.rpy:416
|
||||
old "mute music"
|
||||
@@ -535,39 +535,39 @@ translate ukrainian strings:
|
||||
|
||||
# 00preferences.rpy:417
|
||||
old "mute sound"
|
||||
new "без звуків"
|
||||
new "без звук. ефектів"
|
||||
|
||||
# 00preferences.rpy:418
|
||||
old "mute voice"
|
||||
new "без голосу"
|
||||
new "без озвучення"
|
||||
|
||||
# 00preferences.rpy:419
|
||||
old "mute all"
|
||||
new "режим без звуку"
|
||||
new "без звуку"
|
||||
|
||||
# 00preferences.rpy:500
|
||||
old "Clipboard voicing enabled. Press 'shift+C' to disable."
|
||||
new "Озвучення буфера обміну увімкнено. Натисніть 'shift+C', щоб вимкнути це."
|
||||
new "Озвучення буфера обміну увімкнено. Натисніть 'shift+C', щоб вимкнути."
|
||||
|
||||
# 00preferences.rpy:502
|
||||
old "Self-voicing would say \"[renpy.display.tts.last]\". Press 'alt+shift+V' to disable."
|
||||
new "Синтезатор мови повинен сказати \"[renpy.display.tts.last]\". Натисніть 'alt+shift+V', щоб відключити його."
|
||||
new "Синтез мовлення має сказати \"[renpy.display.tts.last]\". Натисніть 'alt+shift+V', щоб вимкнути."
|
||||
|
||||
# 00preferences.rpy:504
|
||||
old "Self-voicing enabled. Press 'v' to disable."
|
||||
new "Синтезатор мови включено. Натисніть 'v', щоб вимкнути його."
|
||||
new "Синтез мовлення увімкнено. Натисніть 'v', щоб вимкнути його."
|
||||
|
||||
# _compat\gamemenu.rpym:198
|
||||
old "Empty Slot."
|
||||
new "Порожній слот"
|
||||
new "Порожній комірка."
|
||||
|
||||
# _compat\gamemenu.rpym:355
|
||||
old "Previous"
|
||||
new "Назад"
|
||||
new "Попередня"
|
||||
|
||||
# _compat\gamemenu.rpym:362
|
||||
old "Next"
|
||||
new "Далі"
|
||||
new "Наступна"
|
||||
|
||||
# _compat\preferences.rpym:428
|
||||
old "Joystick Mapping"
|
||||
@@ -579,7 +579,7 @@ translate ukrainian strings:
|
||||
|
||||
# _developer\developer.rpym:43
|
||||
old "Interactive Director (D)"
|
||||
new "Інтерактивний Директор (D)"
|
||||
new "Інтерактивний режисер (D)"
|
||||
|
||||
# _developer\developer.rpym:45
|
||||
old "Reload Game (Shift+R)"
|
||||
@@ -591,11 +591,11 @@ translate ukrainian strings:
|
||||
|
||||
# _developer\developer.rpym:49
|
||||
old "Variable Viewer"
|
||||
new "Перегляд змінних"
|
||||
new "Переглядач змінних"
|
||||
|
||||
# _developer\developer.rpym:51
|
||||
old "Image Location Picker"
|
||||
new "Інструмент позиціонування на зображеннях"
|
||||
new "Вибір розташування зображень"
|
||||
|
||||
# _developer\developer.rpym:53
|
||||
old "Filename List"
|
||||
@@ -603,11 +603,11 @@ translate ukrainian strings:
|
||||
|
||||
# _developer\developer.rpym:57
|
||||
old "Show Image Load Log (F4)"
|
||||
new "Показати лог завантаження зображень (F4)"
|
||||
new "Показати журнал завант. зображень (F4)"
|
||||
|
||||
# _developer\developer.rpym:60
|
||||
old "Hide Image Load Log (F4)"
|
||||
new "Скрыть лог загрузки изображений (F4)"
|
||||
new "Сховати журнал завант. зображень (F4)"
|
||||
|
||||
# _developer\developer.rpym:63
|
||||
old "Image Attributes"
|
||||
@@ -623,7 +623,7 @@ translate ukrainian strings:
|
||||
|
||||
# _developer\developer.rpym:137
|
||||
old "Nothing to inspect."
|
||||
new "Змінні не задано."
|
||||
new "Нічого оглядати."
|
||||
|
||||
# _developer\developer.rpym:265
|
||||
old "Return to the developer menu"
|
||||
@@ -639,27 +639,27 @@ translate ukrainian strings:
|
||||
|
||||
# _developer\developer.rpym:435
|
||||
old "Right-click or escape to quit."
|
||||
new "Натисніть праву кнопку миші або ESC, щоб вийти."
|
||||
new "Натисніть ПКМ або Escape, щоб вийти."
|
||||
|
||||
# _developer\developer.rpym:467
|
||||
old "Rectangle copied to clipboard."
|
||||
new "Координати прямокутника скопійовані в буфер обміну."
|
||||
new "Координати прямокутника скопійовано в буфер обміну."
|
||||
|
||||
# _developer\developer.rpym:470
|
||||
old "Position copied to clipboard."
|
||||
new "Координати позиції скопійовані у буфер обміну."
|
||||
new "Координати позиції скопійовано у буфер обміну."
|
||||
|
||||
# _developer\developer.rpym:489
|
||||
old "Type to filter: "
|
||||
new "Поточний фільтр: "
|
||||
new "Фільтр: "
|
||||
|
||||
# _developer\developer.rpym:617
|
||||
old "Textures: [tex_count] ([tex_size_mb:.1f] MB)"
|
||||
new "Текстур: [tex_count] ([tex_size_mb:.1f] МБ)"
|
||||
new "Текстури: [tex_count] ([tex_size_mb:.1f] Мб)"
|
||||
|
||||
# _developer\developer.rpym:621
|
||||
old "Image cache: [cache_pct:.1f]% ([cache_size_mb:.1f] MB)"
|
||||
new "Кеш зображень: [cache_pct:.1f]% ([cache_size_mb:.1f] МБ)"
|
||||
new "Кеш зображень: [cache_pct:.1f]% ([cache_size_mb:.1f] Мб)"
|
||||
|
||||
# _developer\developer.rpym:631
|
||||
old "✔ "
|
||||
@@ -671,11 +671,11 @@ translate ukrainian strings:
|
||||
|
||||
# _developer\developer.rpym:639
|
||||
old "\n{color=#cfc}✔ predicted image (good){/color}\n{color=#fcc}✘ unpredicted image (bad){/color}\n{color=#fff}Drag to move.{/color}"
|
||||
new "\n{color=#cfc}✔ передбачене зображення (добре){/color}\n{color=#fcc}✘ раптове зображення (погано){/color}\n{color=#fff}Натисніть, щоб пересунути.{/color}"
|
||||
new "\n{color=#cfc}✔ передбачене зображення (добре){/color}\n{color=#fcc}✘ раптове зображення (погано){/color}\n{color=#fff}Перетягніть, щоб перемістити.{/color}"
|
||||
|
||||
# _developer\inspector.rpym:38
|
||||
old "Displayable Inspector"
|
||||
new "Диспетчер об'єктів"
|
||||
new "Оглядач об’єктів"
|
||||
|
||||
# _developer\inspector.rpym:61
|
||||
old "Size"
|
||||
@@ -691,23 +691,23 @@ translate ukrainian strings:
|
||||
|
||||
# _developer\inspector.rpym:122
|
||||
old "Inspecting Styles of [displayable_name!q]"
|
||||
new "Інспектую стилі [displayable_name!q]"
|
||||
new "Оглядання стилів [displayable_name!q]"
|
||||
|
||||
# _developer\inspector.rpym:139
|
||||
old "displayable:"
|
||||
new "об'єкт:"
|
||||
new "об’єкт:"
|
||||
|
||||
# _developer\inspector.rpym:145
|
||||
old " (no properties affect the displayable)"
|
||||
new " (на об'єкт не впливають жодні параметри)"
|
||||
new " (на об’єкт не впливають жодні параметри)"
|
||||
|
||||
# _developer\inspector.rpym:147
|
||||
old " (default properties omitted)"
|
||||
new " (налаштування за замовчуванням опущено)"
|
||||
new " (стандартні властивості опущено)"
|
||||
|
||||
# _developer\inspector.rpym:185
|
||||
old "<repr() failed>"
|
||||
new "<repr() провалений>"
|
||||
new "<repr() failed>"
|
||||
|
||||
# _layout\classic_load_save.rpym:170
|
||||
old "a"
|
||||
@@ -719,11 +719,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00iap.rpy:217
|
||||
old "Contacting App Store\nPlease Wait..."
|
||||
new "Зв'язуюсь з App Store\nБудь ласка, чекайте..."
|
||||
new "Зв’язок з App Store\nБудь ласка, зачекайте..."
|
||||
|
||||
# 00updater.rpy:375
|
||||
old "The Ren'Py Updater is not supported on mobile devices."
|
||||
new "Ren'Py Updater не підтримується на мобільних пристроях."
|
||||
new "Оновлювач Ren'Py не підтримується на мобільних пристроях."
|
||||
|
||||
# 00updater.rpy:494
|
||||
old "An error is being simulated."
|
||||
@@ -731,35 +731,35 @@ translate ukrainian strings:
|
||||
|
||||
# 00updater.rpy:678
|
||||
old "Either this project does not support updating, or the update status file was deleted."
|
||||
new "Або цей проект не підтримує оновлення, або видалено файл статусу оновлення."
|
||||
new "Або цей проєкт не підтримує оновлення, або файл стану оновлення було видалено."
|
||||
|
||||
# 00updater.rpy:692
|
||||
old "This account does not have permission to perform an update."
|
||||
new "Цей обліковий запис не має права проводити оновлення."
|
||||
new "Цей обліковий запис не має дозволу на виконання оновлення."
|
||||
|
||||
# 00updater.rpy:695
|
||||
old "This account does not have permission to write the update log."
|
||||
new "Цей обліковий запис не має права писати лог оновлення."
|
||||
new "Цей обліковий запис не має дозволу на запис журналу оновлень."
|
||||
|
||||
# 00updater.rpy:722
|
||||
old "Could not verify update signature."
|
||||
new "Не можу верифікувати підпис оновлення."
|
||||
new "Не вдалося перевірити підпис оновлення."
|
||||
|
||||
# 00updater.rpy:997
|
||||
old "The update file was not downloaded."
|
||||
new "Файл поновлення не був завантажений."
|
||||
new "Файл оновлення не було завантажено."
|
||||
|
||||
# 00updater.rpy:1015
|
||||
old "The update file does not have the correct digest - it may have been corrupted."
|
||||
new "Файл поновлення не містить коректного дайджесту — він може бути пошкоджений."
|
||||
new "Файл оновлення не має правильного дайджесту — можливо, він був пошкоджений."
|
||||
|
||||
# 00updater.rpy:1071
|
||||
old "While unpacking {}, unknown type {}."
|
||||
new "При розпаковуванні {} виявлено невідомий тип {}."
|
||||
new "Під час розпакування {}, невідомий тип {}."
|
||||
|
||||
# 00updater.rpy:1439
|
||||
old "Updater"
|
||||
new "Оновлення"
|
||||
new "Оновлювач"
|
||||
|
||||
# 00updater.rpy:1450
|
||||
old "This program is up to date."
|
||||
@@ -767,7 +767,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00updater.rpy:1452
|
||||
old "[u.version] is available. Do you want to install it?"
|
||||
new "[u.version] доступна. Ви бажаєте її встановити?"
|
||||
new "[u.version] доступно. Бажаєте її встановити?"
|
||||
|
||||
# 00updater.rpy:1454
|
||||
old "Preparing to download the updates."
|
||||
@@ -799,11 +799,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00gallery.rpy:605
|
||||
old "prev"
|
||||
new "мин"
|
||||
new "попер."
|
||||
|
||||
# 00gallery.rpy:606
|
||||
old "next"
|
||||
new "наст"
|
||||
new "наст."
|
||||
|
||||
# 00gallery.rpy:607
|
||||
old "slideshow"
|
||||
@@ -823,11 +823,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00accessibility.rpy:76
|
||||
old "Font Override"
|
||||
new "Перевизначення шрифту"
|
||||
new "Замінити шрифт"
|
||||
|
||||
# 00accessibility.rpy:80
|
||||
old "Default"
|
||||
new "За замовчуванням"
|
||||
new "Стандратний"
|
||||
|
||||
# 00accessibility.rpy:84
|
||||
old "DejaVu Sans"
|
||||
@@ -839,7 +839,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00accessibility.rpy:94
|
||||
old "Text Size Scaling"
|
||||
new "Масштабування розміру тексту"
|
||||
new "Масштаб розміру тексту"
|
||||
|
||||
# 00accessibility.rpy:100
|
||||
old "Reset"
|
||||
@@ -847,11 +847,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00accessibility.rpy:105
|
||||
old "Line Spacing Scaling"
|
||||
new "Масштабування міжрядкового інтервалу"
|
||||
new "Масштаб міжрядкового інтервалу"
|
||||
|
||||
# 00accessibility.rpy:117
|
||||
old "Self-Voicing"
|
||||
new "Озвучка через синтезатор мовлення"
|
||||
new "Синтез мовлення"
|
||||
|
||||
# 00accessibility.rpy:121
|
||||
old "Off"
|
||||
@@ -859,11 +859,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00accessibility.rpy:125
|
||||
old "Text-to-speech"
|
||||
new "Перетворення тексту на мовлення"
|
||||
new "Перетворення тексту в мовлення"
|
||||
|
||||
# 00accessibility.rpy:129
|
||||
old "Clipboard"
|
||||
new "З буфера обміну"
|
||||
new "Буфер обміну"
|
||||
|
||||
# 00preferences.rpy:430
|
||||
old "font transform"
|
||||
@@ -875,22 +875,23 @@ translate ukrainian strings:
|
||||
|
||||
# 00preferences.rpy:441
|
||||
old "font line spacing"
|
||||
new "міжрядковий інтервал"
|
||||
new "міжрядковий інтервал шрифту"
|
||||
|
||||
# renpy/common/00accessibility.rpy:191
|
||||
old "The options on this menu are intended to improve accessibility. They may not work with all games, and some combinations of options may render the game unplayable. This is not an issue with the game or engine. For the best results when changing fonts, try to keep the text size the same as it originally was."
|
||||
new "Установки цього меню призначені для збільшення доступності. Вони можуть працювати не з усіма іграми, а деякі комбінації опцій можуть зробити гру, що відображається некоректно. Це не проблема з грою чи движком. Для досягнення найкращих результатів при зміні шрифтів намагайтеся зберігати розмір тексту таким, яким він був спочатку."
|
||||
new "Параметри цього меню призначені для покращення доступності. Вони можуть працювати не з усіма іграми, а деякі комбінації параметрів можуть зробити гру неможливою для гри. Це не є проблемою гри або рушія. Для досягнення найкращих результатів при зміні шрифтів намагайтеся зберігати розмір тексту таким, яким він був спочатку."
|
||||
|
||||
# renpy/common/00accessibility.rpy:193
|
||||
old "Self-Voicing Volume Drop"
|
||||
new "Зменшення гучності при озвученні через синтезатор мовлення"
|
||||
new "Зменшити гучність синтезу мовлення"
|
||||
|
||||
# renpy/common/00preferences.rpy:384
|
||||
old "self voicing volume drop"
|
||||
new "зменшення гучності при озвученні через синтезатор мовлення"
|
||||
new "зменшити гучність синтезу мовлення"
|
||||
|
||||
# renpy/common/00preferences.rpy:464
|
||||
old "system cursor"
|
||||
new "системний курсор"
|
||||
new "системний вказівник"
|
||||
|
||||
# renpy/common/00accessibility.rpy:180
|
||||
old "High Contrast Text"
|
||||
@@ -898,11 +899,11 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00preferences.rpy:487
|
||||
old "renderer menu"
|
||||
new "меню рендерера"
|
||||
new "меню візуалізації"
|
||||
|
||||
# renpy/common/00preferences.rpy:490
|
||||
old "accessibility menu"
|
||||
new "меню спеціальних можливостей"
|
||||
new "меню доступності"
|
||||
|
||||
# renpy/common/00preferences.rpy:493
|
||||
old "high contrast text"
|
||||
@@ -910,15 +911,15 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00preferences.rpy:511
|
||||
old "audio when minimized"
|
||||
new "звук при згортанні вікна"
|
||||
new "звук при згортанні"
|
||||
|
||||
# renpy/common/00preferences.rpy:531
|
||||
old "main volume"
|
||||
new "загальна гучність"
|
||||
new "основна гучність"
|
||||
|
||||
# renpy/common/00preferences.rpy:535
|
||||
old "mute main"
|
||||
new "режим без звуку"
|
||||
new "без основи"
|
||||
|
||||
translate ukrainian strings:
|
||||
|
||||
@@ -936,31 +937,31 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00director.rpy:1745
|
||||
old "Click to toggle attribute, right click to toggle negative attribute."
|
||||
new "Клацніть, аби увімкнути атрибут, клацніть ПКМ, аби увімкнути від’ємний атрибут."
|
||||
new "Натисніть, аби увімкнути атрибут, натисніть ПКМ, аби увімкнути від’ємний атрибут."
|
||||
|
||||
# renpy/common/00director.rpy:1768
|
||||
old "Click to set transform, right click to add to transform list."
|
||||
new "Клацніть, щоб встановити трансформацію, клацніть ПКМ, щоб додати до списку трансформацій."
|
||||
new "Натисніть, щоб встановити трансформацію, натисніть ПКМ, щоб додати до списку трансформацій."
|
||||
|
||||
# renpy/common/00director.rpy:1789
|
||||
old "Click to set, right click to add to behind list."
|
||||
new "Клацніть, щоб встановити, клацніть ПКМ, щоб додати до списку."
|
||||
new "Натисніть, щоб встановити, натисніть ПКМ, щоб додати до списку."
|
||||
|
||||
# renpy/common/00gui.rpy:456
|
||||
old "This save was created on a different device. Maliciously constructed save files can harm your computer. Do you trust this save's creator and everyone who could have changed the file?"
|
||||
new "Це збереження було створено на іншому пристрої. Зловмисно створені файли збережень можуть завдати шкоди вашому комп'ютеру. Чи довіряєте ви творцю цього збереження і всім, хто міг змінити файл?"
|
||||
new "Це збереження було створено на іншому пристрої. Зловмисно створені файли збережень можуть завдати шкоди вашому пристрою. Чи довіряєте ви творцю цього збереження і всім, хто міг змінити файл?"
|
||||
|
||||
# renpy/common/00gui.rpy:457
|
||||
old "Do you trust the device the save was created on? You should only choose yes if you are the device's sole user."
|
||||
new "Чи довіряєте ви пристрою, на якому було створено збереження? Ви повинні вибрати \"Так\", тільки якщо ви є єдиним користувачем пристрою."
|
||||
new "Чи довіряєте ви пристрою, на якому було створено збереження? Ви повинні вибрати «Так», тільки якщо ви є єдиним користувачем пристрою."
|
||||
|
||||
# renpy/common/00preferences.rpy:528
|
||||
old "audio when unfocused"
|
||||
new "звук при розфокусуванні"
|
||||
new "звук при згортанні вікна"
|
||||
|
||||
# renpy/common/00preferences.rpy:537
|
||||
old "web cache preload"
|
||||
new "попереднє завантаження веб-кешу"
|
||||
new "попер. завантаження вебкешу"
|
||||
|
||||
# renpy/common/00preferences.rpy:552
|
||||
old "voice after game menu"
|
||||
@@ -980,7 +981,7 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00sync.rpy:190
|
||||
old "Could not connect to the Ren'Py Sync server."
|
||||
new "Не вдалося підключитися до сервера Ren'Py Sync."
|
||||
new "Не вдалося під’єднатися до сервера Ren'Py Sync."
|
||||
|
||||
# renpy/common/00sync.rpy:192
|
||||
old "The Ren'Py Sync server timed out."
|
||||
@@ -988,7 +989,7 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00sync.rpy:194
|
||||
old "An unknown error occurred while connecting to the Ren'Py Sync server."
|
||||
new "Під час підключення до сервера Ren'Py Sync виникла невідома помилка."
|
||||
new "Під час з’єднання до сервера Ren'Py Sync виникла невідома помилка."
|
||||
|
||||
# renpy/common/00sync.rpy:267
|
||||
old "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."
|
||||
@@ -1016,7 +1017,7 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00sync.rpy:529
|
||||
old "This will upload your saves to the {a=https://sync.renpy.org}Ren'Py Sync Server{/a}.\nDo you want to continue?"
|
||||
new "Це завантажить ваші збереження на сервер синхронізації {a=https://sync.renpy.org}Ren'Py Sync{/a}.\nБажаєте продовжити?"
|
||||
new "Це завантажить ваші збереження на {a=https://sync.renpy.org}сервер Ren'Py Sync{/a}.\nБажаєте продовжити?"
|
||||
|
||||
# renpy/common/00sync.rpy:558
|
||||
old "Enter Sync ID"
|
||||
@@ -1024,7 +1025,7 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00sync.rpy:569
|
||||
old "This will contact the {a=https://sync.renpy.org}Ren'Py Sync Server{/a}."
|
||||
new "Зв’язок зі сервером синхронізації {a=https://sync.renpy.org}Ren'Py Sync{/a}."
|
||||
new "Зв’язок зі {a=https://sync.renpy.org}сервером Ren'Py Sync{/a}."
|
||||
|
||||
# renpy/common/00sync.rpy:596
|
||||
old "Sync Success"
|
||||
@@ -1036,7 +1037,7 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00sync.rpy:605
|
||||
old "You can use this ID to download your save on another device.\nThis sync will expire in an hour.\nRen'Py Sync is supported by {a=https://www.renpy.org/sponsors.html}Ren'Py's Sponsors{/a}."
|
||||
new "Ви можете використовувати цей ідентифікатор, щоб завантажити збереження на інший пристрій.\nЦя синхронізація закінчиться через годину.\nRen'Py Sync підтримується {a=https://www.renpy.org/sponsors.html}спонсорами Ren'Py{/a}."
|
||||
new "Ви можете використати цей ідентифікатор, щоб завантажити збереження на інший пристрій.\nЦя синхронізація завершиться за годину.\nRen'Py Sync підтримується за підтримки {a=https://www.renpy.org/sponsors.html}спонсорів Ren'Py{/a}."
|
||||
|
||||
# renpy/common/00sync.rpy:631
|
||||
old "Sync Error"
|
||||
|
||||
@@ -3,19 +3,19 @@ translate ukrainian strings:
|
||||
|
||||
# 00console.rpy:255
|
||||
old "Press <esc> to exit console. Type help for help.\n"
|
||||
new "Натисніть <esc>, щоб вийти із консолі. Введіть <help> для допомоги.\n"
|
||||
new "Натисніть <esc>, щоб вийти із консолі. Введіть help для довідки.\n"
|
||||
|
||||
# 00console.rpy:259
|
||||
old "Ren'Py script enabled."
|
||||
new "Ren'Py script активовано."
|
||||
new "Скрипт Ren'Py увімкено."
|
||||
|
||||
# 00console.rpy:261
|
||||
old "Ren'Py script disabled."
|
||||
new "Ren'Py script деактивовано."
|
||||
new "Скрипт Ren'Py вимкнуто."
|
||||
|
||||
# 00console.rpy:496
|
||||
old "help: show this help"
|
||||
new "help: показує допомогу"
|
||||
new "help: показує цю довідку"
|
||||
|
||||
# 00console.rpy:501
|
||||
old "commands:\n"
|
||||
@@ -23,27 +23,27 @@ translate ukrainian strings:
|
||||
|
||||
# 00console.rpy:511
|
||||
old " <renpy script statement>: run the statement\n"
|
||||
new " <оператор renpy script>: запуск оператора\n"
|
||||
new " <функція скрипту renpy>: запуск функції\n"
|
||||
|
||||
# 00console.rpy:513
|
||||
old " <python expression or statement>: run the expression or statement"
|
||||
new " <вираз або оператор python>: запустити вираз або оператор"
|
||||
new " <вираз або функція python>: запуск виразу або функції"
|
||||
|
||||
# 00console.rpy:521
|
||||
old "clear: clear the console history"
|
||||
new "clear: очищення історії консолі"
|
||||
new "clear: очистити історію консолі"
|
||||
|
||||
# 00console.rpy:525
|
||||
old "exit: exit the console"
|
||||
new "exit: вихід із консолі"
|
||||
new "exit: вийти із консолі"
|
||||
|
||||
# 00console.rpy:533
|
||||
old "load <slot>: loads the game from slot"
|
||||
new "load <слот>: завантажує гру з вибраного слота"
|
||||
new "load <комірка>: завантажує гру з вибраної комірки"
|
||||
|
||||
# 00console.rpy:546
|
||||
old "save <slot>: saves the game in slot"
|
||||
new "save <слот>: зберігає гру у вибраний слот"
|
||||
new "save <комірка>: зберігає гру у вибраній комірці"
|
||||
|
||||
# 00console.rpy:557
|
||||
old "reload: reloads the game, refreshing the scripts"
|
||||
@@ -51,49 +51,49 @@ translate ukrainian strings:
|
||||
|
||||
# 00console.rpy:565
|
||||
old "watch <expression>: watch a python expression"
|
||||
new "watch <вираз>: спостерігати за виразом python"
|
||||
new "watch <вираз>: дивитися вираз python"
|
||||
|
||||
# 00console.rpy:591
|
||||
old "unwatch <expression>: stop watching an expression"
|
||||
new "unwatch <вираз>: припинити спостерігати за виразом"
|
||||
new "unwatch <вираз>: припинити дивитися вираз"
|
||||
|
||||
# 00console.rpy:622
|
||||
old "unwatchall: stop watching all expressions"
|
||||
new "unwatchall: глобальне припинення спостереження"
|
||||
new "unwatchall: припинити дивитися за всіма виразами"
|
||||
|
||||
# 00console.rpy:639
|
||||
old "jump <label>: jumps to label"
|
||||
new "jump <мітка>: стрибок на мітку"
|
||||
new "jump <мітка>: перейти на мітку"
|
||||
|
||||
# 00console.rpy:685
|
||||
old "short: Shorten the representation of objects on the console (default)."
|
||||
new "short: Скорочене представлення об'єктів (repr) у консолі (за замовчуванням)."
|
||||
new "short: Скоротити представлення об’єктів на консолі (за стандартом)."
|
||||
|
||||
# 00console.rpy:690
|
||||
old "long: Print the full representation of objects on the console."
|
||||
new "long: Виводити повне уявлення об'єктів (repr) у консолі."
|
||||
new "long: Вивести повне представлення об’єктів на консолі."
|
||||
|
||||
# renpy/common/00console.rpy:814
|
||||
old "watch <expression>: watch a python expression\n watch short: makes the representation of traced expressions short (default)\n watch long: makes the representation of traced expressions as is"
|
||||
new "watch <вираз>: спостерігати за виразом python\n watch short: вкорочує відображення виразів, що відстежуються (за замовчуванням)\n watch long: робить відображення відстежуваних виразів як є"
|
||||
new "watch <вираз>: дивитися вираз python\n watch short: робить представлення відстежених виразів коротким (за стандартом)\n watch long: робить представлення відстежених виразів як є"
|
||||
|
||||
# renpy/common/00console.rpy:925
|
||||
old "escape: Enables escaping of unicode symbols in unicode strings."
|
||||
new "escape: Включає екранування Unicode символів у рядках unicode."
|
||||
new "escape: Вмикає екранування символів Unicode в рядках Unicode."
|
||||
|
||||
# renpy/common/00console.rpy:929
|
||||
old "unescape: Disables escaping of unicode symbols in unicode strings and print it as is (default)."
|
||||
new "unescape: Вимикає екранування Unicode символів у рядках unicode та виводить їх як є (за замовчуванням)."
|
||||
new "unescape: Вимикає екранування символів Unicode в рядках Unicode і друкує їх як є (за стандартом)."
|
||||
|
||||
# renpy/common/00console.rpy:784
|
||||
old "stack: print the return stack"
|
||||
new "stack: виводить стек повернення (return stack)"
|
||||
new "stack: вивести стек повернення"
|
||||
|
||||
translate ukrainian strings:
|
||||
|
||||
# renpy/common/_developer/developer.rpym:51
|
||||
old "Persistent Viewer"
|
||||
new "Переглядач постійних даних"
|
||||
new "Переглядач даних"
|
||||
|
||||
# renpy/common/_developer/developer.rpym:70
|
||||
old "Speech Bubble Editor (Shift+B)"
|
||||
@@ -101,9 +101,9 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00console.rpy:789
|
||||
old "help: show this help\n help <expr>: show signature and documentation of <expr>"
|
||||
new "help: показати цю довідку\n help <expr>: показати підпис та документацію <expr>"
|
||||
new "help: показує цю довідку\n help <вираз>: показує підпис та документацію <виразу>"
|
||||
|
||||
# renpy/common/00console.rpy:813
|
||||
old "Help may display undocumented functions. Please check that the function or\nclass you want to use is documented.\n\n"
|
||||
new "У довідці можуть відображатися недокументовані функції. Будь ласка, перевірте, чи функцію або\nклас, який ви хочете використати, задокументовано.\n\n"
|
||||
new "У довідці можуть відображатися недокументовані функції. Будь ласка, перевірте, чи функцію або\nклас, який бажаєте використати, задокументовано.\n\n"
|
||||
|
||||
|
||||
@@ -7,23 +7,23 @@ translate ukrainian strings:
|
||||
|
||||
# 00gltest.rpy:74
|
||||
old "Automatically Choose"
|
||||
new "Вибрати автоматично"
|
||||
new "Обрати автоматично"
|
||||
|
||||
# 00gltest.rpy:79
|
||||
old "Force Angle/DirectX Renderer"
|
||||
new "Примусова візуалізація Angle/DirectX"
|
||||
new "Візуалізація Angle/DirectX"
|
||||
|
||||
# 00gltest.rpy:83
|
||||
old "Force OpenGL Renderer"
|
||||
new "Примусова візуалізація OpenGL"
|
||||
new "Візуалізація OpenGL"
|
||||
|
||||
# 00gltest.rpy:87
|
||||
old "Force Software Renderer"
|
||||
new "Примусова програмна візуалізація"
|
||||
new "Програмна візуалізація"
|
||||
|
||||
# 00gltest.rpy:93
|
||||
old "NPOT"
|
||||
new "NPOT (OpenGL 2+)"
|
||||
new "NPOT"
|
||||
|
||||
# 00gltest.rpy:97
|
||||
old "Enable"
|
||||
@@ -51,7 +51,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00gltest.rpy:163
|
||||
old "Tearing"
|
||||
new "Розривання кадрів"
|
||||
new "Розрив кадрів"
|
||||
|
||||
# 00gltest.rpy:179
|
||||
old "Changes will take effect the next time this program is run."
|
||||
@@ -79,11 +79,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00gltest.rpy:229
|
||||
old "Its graphics drivers may be out of date or not operating correctly. This can lead to slow or incorrect graphics display. Updating DirectX could fix this problem."
|
||||
new "Графічні драйвера застаріли чи працюють неправильно. Це може призвести до повільного або неправильного відображення графіки. Оновлення DirectX може вирішити цю проблему."
|
||||
new "Графічні драйвери можуть бути застарілими або працювати неправильно. Це може призвести до повільного або некоректного відображення графіки. Оновлення DirectX може вирішити цю проблему."
|
||||
|
||||
# 00gltest.rpy:231
|
||||
old "Its graphics drivers may be out of date or not operating correctly. This can lead to slow or incorrect graphics display."
|
||||
new "Графічні драйвера застаріли чи працюють неправильно. Це може призвести до повільного або неправильного відображення графіки."
|
||||
new "Графічні драйвери можуть бути застарілими або працювати неправильно. Це може призвести до повільного або некоректного відображення графіки."
|
||||
|
||||
# 00gltest.rpy:236
|
||||
old "Update DirectX"
|
||||
@@ -91,7 +91,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00gltest.rpy:242
|
||||
old "Continue, Show this warning again"
|
||||
new "Продовжити, показати знову"
|
||||
new "Продовжити, показувати знову"
|
||||
|
||||
# 00gltest.rpy:246
|
||||
old "Continue, Don't show warning again"
|
||||
@@ -103,11 +103,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00gltest.rpy:268
|
||||
old "DirectX web setup has been started. It may start minimized in the taskbar. Please follow the prompts to install DirectX."
|
||||
new "Запущено веб-налаштування DirectX. Вона може згорнутися на панелі завдань. Будь ласка, дотримуйтесь підказок для встановлення DirectX."
|
||||
new "Запущено вебналаштування DirectX. Вона може згорнутися на панелі завдань. Будь ласка, дотримуйтесь підказок для встановлення DirectX."
|
||||
|
||||
# 00gltest.rpy:272
|
||||
old "{b}Note:{/b} Microsoft's DirectX web setup program will, by default, install the Bing toolbar. If you do not want this toolbar, uncheck the appropriate box."
|
||||
new "{b}Примітка:{/b} Інсталятор DirectX за замовчуванням намагається встановити панель інструментів Bing. Якщо ви цього не хочете, зніміть відповідну галочку."
|
||||
new "{b}Нотатка:{/b} Програма вебналаштування DirectX від Microsoft за стандартом встановлює панель інструментів Bing. Якщо вам не потрібна ця панель, зніміть позначку у відповідному полі."
|
||||
|
||||
# 00gltest.rpy:276
|
||||
old "When setup finishes, please click below to restart this program."
|
||||
@@ -119,11 +119,11 @@ translate ukrainian strings:
|
||||
|
||||
# 00gamepad.rpy:32
|
||||
old "Select Gamepad to Calibrate"
|
||||
new "Виберіть геймпад для калібрування"
|
||||
new "Оберіть ґеймпад для калібрування"
|
||||
|
||||
# 00gamepad.rpy:35
|
||||
old "No Gamepads Available"
|
||||
new "Геймпадів не виявлено"
|
||||
new "Ґеймпадів не виявлено"
|
||||
|
||||
# 00gamepad.rpy:54
|
||||
old "Calibrating [name] ([i]/[total])"
|
||||
@@ -131,7 +131,7 @@ translate ukrainian strings:
|
||||
|
||||
# 00gamepad.rpy:58
|
||||
old "Press or move the [control!s] [kind]."
|
||||
new "Натисніть або перемістіть [control!s] [kind]."
|
||||
new "Натисніть або рухніть [control!s] [kind]."
|
||||
|
||||
# 00gamepad.rpy:66
|
||||
old "Skip (A)"
|
||||
@@ -167,7 +167,7 @@ translate ukrainian strings:
|
||||
|
||||
# _errorhandling.rpym:584
|
||||
old "Attempts a roll back to a prior time, allowing you to save or choose a different choice."
|
||||
new "Спроба відкоту до попереднього часу, що дозволяє зберегти або вибрати інший варіант."
|
||||
new "Спроба відкату до попереднього часу, що дозволяє зберегти або вибрати інший варіант."
|
||||
|
||||
# _errorhandling.rpym:587
|
||||
old "Ignore"
|
||||
@@ -203,7 +203,7 @@ translate ukrainian strings:
|
||||
|
||||
# _errorhandling.rpym:638
|
||||
old "Parsing the script failed."
|
||||
new "Опрацювання скрипту завершилося невдало."
|
||||
new "Розбір скрипта не вдався."
|
||||
|
||||
# _errorhandling.rpym:664
|
||||
old "Opens the errors.txt file in a text editor."
|
||||
@@ -219,7 +219,7 @@ translate ukrainian strings:
|
||||
|
||||
# _errorhandling.rpym:544
|
||||
old "Copies the traceback.txt file to the clipboard as BBcode for forums like https://lemmasoft.renai.us/."
|
||||
new "Копіює файл traceback.txt у буфер обміну як BBCode для форумів типу https://lemmasoft.renai.us/."
|
||||
new "Копіює файл traceback.txt у буфер обміну як BBCode для форумів як https://lemmasoft.renai.us/."
|
||||
|
||||
# _errorhandling.rpym:546
|
||||
old "Copy Markdown"
|
||||
@@ -231,35 +231,35 @@ translate ukrainian strings:
|
||||
|
||||
# _errorhandling.rpym:683
|
||||
old "Copies the errors.txt file to the clipboard as BBcode for forums like https://lemmasoft.renai.us/."
|
||||
new "Копіює файл errors.txt у буфер обміну як BBCode для форумів типу https://lemmasoft.renai.us/."
|
||||
new "Копіює файл errors.txt у буфер обміну як BBCode для форумів як https://lemmasoft.renai.us/."
|
||||
|
||||
# _errorhandling.rpym:687
|
||||
old "Copies the errors.txt file to the clipboard as Markdown for Discord."
|
||||
new "Копіює файл errors.txt у буфер обміну як Markdown для Дискорду."
|
||||
new "Копіює файл errors.txt у буфер обміну як Markdown для Discord."
|
||||
|
||||
# renpy/common/00gltest.rpy:100
|
||||
old "Force GL Renderer"
|
||||
new "Примусова візуалізація GL"
|
||||
new "Візуалізація GL"
|
||||
|
||||
# renpy/common/00gltest.rpy:105
|
||||
old "Force ANGLE Renderer"
|
||||
new "Примусова візуалізація ANGLE"
|
||||
new "Візуалізація ANGLE"
|
||||
|
||||
# renpy/common/00gltest.rpy:110
|
||||
old "Force GLES Renderer"
|
||||
new "Примусова візуалізація GLES"
|
||||
new "Візуалізація GLES"
|
||||
|
||||
# renpy/common/00gltest.rpy:116
|
||||
old "Force GL2 Renderer"
|
||||
new "Примусова візуалізація GL2"
|
||||
new "Візуалізація GL2"
|
||||
|
||||
# renpy/common/00gltest.rpy:121
|
||||
old "Force ANGLE2 Renderer"
|
||||
new "Примусова візуалізація ANGLE2"
|
||||
new "Візуалізація ANGLE2"
|
||||
|
||||
# renpy/common/00gltest.rpy:126
|
||||
old "Force GLES2 Renderer"
|
||||
new "Примусова візуалізація GLES2"
|
||||
new "Візуалізація GLES2"
|
||||
|
||||
# renpy/common/00gltest.rpy:245
|
||||
old "This game requires use of GL2 that can't be initialised."
|
||||
@@ -271,11 +271,11 @@ translate ukrainian strings:
|
||||
|
||||
# renpy/common/00gltest.rpy:273
|
||||
old "Change render options"
|
||||
new "Змінити налаштування рендерингу"
|
||||
new "Змінити параметри візцалізації"
|
||||
|
||||
# renpy/common/00gamepad.rpy:58
|
||||
old "Press or move the '[control!s]' [kind]."
|
||||
new "Натисніть або посуньте [kind] '[control!s]'"
|
||||
new "Натисніть або рухніть '[control!s]' [kind]."
|
||||
|
||||
# renpy/common/00gltest.rpy:136
|
||||
old "Enable (No Blocklist)"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,15 +11,15 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:85
|
||||
old "## Say screen"
|
||||
new "## Екран розмови"
|
||||
new "## Екран Say"
|
||||
|
||||
# gui/game/screens.rpy:87
|
||||
old "## The say screen is used to display dialogue to the player. It takes two parameters, who and what, which are the name of the speaking character and the text to be displayed, respectively. (The who parameter can be None if no name is given.)"
|
||||
new "## Екран say використовується для відображення діалогу гравцеві. Він приймає два параметри, хто і що, ім'я персонажа, що говорить, і текст, який буде відображатися відповідно. (Параметр who може мати значення None, якщо ім’я не вказано.)"
|
||||
new "## Екран Say використовується для відображення діалогу гравцеві. Він приймає два параметри, who і what, ім’я персонажа, що говорить, і текст, який буде відображатися відповідно. (Параметр who може мати значення None, якщо ім’я не вказано.)"
|
||||
|
||||
# gui/game/screens.rpy:92
|
||||
old "## This screen must create a text displayable with id \"what\", as Ren'Py uses this to manage text display. It can also create displayables with id \"who\" and id \"window\" to apply style properties."
|
||||
new "## Цей екран має створювати текст, який можна відображати з ідентифікатором \"what\", оскільки Ren'Py використовує це для керування відображенням тексту. Він також може створювати відображувані елементи з ідентифікаторами \"who\" та ідентифікаторами \"window\" для застосування властивостей стилю."
|
||||
new "## Цей екран має створювати текст, який можна відображати з ідентифікатором «what», оскільки Ren'Py використовує це для керування відображенням тексту. Він також може створювати відображувані елементи з ідентифікаторами «who» та «window» для застосування властивостей стилю."
|
||||
|
||||
# gui/game/screens.rpy:96
|
||||
old "## https://www.renpy.org/doc/html/screen_special.html#say"
|
||||
@@ -27,11 +27,11 @@ 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."
|
||||
new "## Робить поле імен доступним для стилізації через об'єкт Character."
|
||||
new "## Робить поле імені доступним для стилізації за допомогою об’єкта Character."
|
||||
|
||||
# gui/game/screens.rpy:165
|
||||
old "## Input screen"
|
||||
@@ -43,7 +43,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:170
|
||||
old "## This screen must create an input displayable with id \"input\" to accept the various input parameters."
|
||||
new "## Цей екран має створити відображуваний вхід з ідентифікатором \"input\" для прийняття різних вхідних параметрів."
|
||||
new "## Цей екран має створити відображуваний вхід з ідентифікатором «input» для прийняття різних вхідних параметрів."
|
||||
|
||||
# gui/game/screens.rpy:173
|
||||
old "## https://www.renpy.org/doc/html/screen_special.html#input"
|
||||
@@ -55,7 +55,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:202
|
||||
old "## This screen is used to display the in-game choices presented by the menu statement. The one parameter, items, is a list of objects, each with caption and action fields."
|
||||
new "## Цей екран використовується для відображення вибору в грі, представленого командою 'menu'. Один параметр, 'items', — це список об’єктів, кожен із полями заголовка та дії."
|
||||
new "## Цей екран використовується для відображення вибору в грі, представленого командою 'menu'. Один параметр, 'items' — це список об’єктів, кожен із полями заголовка та дії."
|
||||
|
||||
# gui/game/screens.rpy:206
|
||||
old "## https://www.renpy.org/doc/html/screen_special.html#choice"
|
||||
@@ -135,7 +135,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:314
|
||||
old "End Replay"
|
||||
new "Закінчити повтору"
|
||||
new "Закінчити повтор"
|
||||
|
||||
# gui/game/screens.rpy:318
|
||||
old "Main Menu"
|
||||
@@ -155,7 +155,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:329
|
||||
old "## The quit button is banned on iOS and unnecessary on Android and Web."
|
||||
new "## Кнопка виходу заборонена на iOS і непотрібна на Android і в Веб."
|
||||
new "## Кнопка виходу заборонена на iOS і непотрібна на Android і в Web."
|
||||
|
||||
# gui/game/screens.rpy:330
|
||||
old "Quit"
|
||||
@@ -191,11 +191,11 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:408
|
||||
old "## This lays out the basic common structure of a game menu screen. It's called with the screen title, and displays the background, title, and navigation."
|
||||
new "## Це описує базову структуру екрана меню гри. Він викликається за допомогою заголовка екрана та відображає фон, заголовок і навігацію."
|
||||
new "## Це описує базову структуру екрана меню гри. Він викликається за допомогою заголовка екрана та відображає тло, заголовок і навігацію."
|
||||
|
||||
# gui/game/screens.rpy:411
|
||||
old "## The scroll parameter can be None, or one of \"viewport\" or \"vpgrid\". This screen is intended to be used with one or more children, which are transcluded (placed) inside it."
|
||||
new "## Параметр прокручування може бути None або одним із \"viewport\" чи \"vpgrid\". Цей екран призначений для використання з одним або декількома об'єктами, які включені (розміщені) всередині нього."
|
||||
new "## Параметр прокручування може бути None або одним із «viewport» чи «vpgrid». Цей екран призначений для використання з одним або декількома об’єктами, які включені (розміщені) всередині нього."
|
||||
|
||||
# gui/game/screens.rpy:429
|
||||
old "## Reserve space for the navigation section."
|
||||
@@ -219,7 +219,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:546
|
||||
old "## This use statement includes the game_menu screen inside this one. The vbox child is then included inside the viewport inside the game_menu screen."
|
||||
new "## Цей оператор використання включає екран game_menu всередині цього. Потім дочірній елемент vbox включається в область перегляду всередині екрана game_menu."
|
||||
new "## Цей оператор використання включає екран «game_menu» всередині цього. Потім дочірній елемент «vbox» включається в область перегляду всередині екрана «game_menu»."
|
||||
|
||||
# gui/game/screens.rpy:556
|
||||
old "Version [config.version!t]\n"
|
||||
@@ -227,7 +227,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:558
|
||||
old "## gui.about is usually set in options.rpy."
|
||||
new "## gui.about зазвичай встановлюється в options.rpy."
|
||||
new "## «gui.about» зазвичай встановлюється в «options.rpy»."
|
||||
|
||||
# gui/game/screens.rpy:562
|
||||
old "Made with {a=https://www.renpy.org/}Ren'Py{/a} [renpy.version_only].\n\n[renpy.license!t]"
|
||||
@@ -239,7 +239,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:575
|
||||
old "## These screens are responsible for letting the player save the game and load it again. Since they share nearly everything in common, both are implemented in terms of a third screen, file_slots."
|
||||
new "## Ці екрани дозволяють гравцеві зберегти гру та завантажити її знову. Оскільки вони мають майже все спільне, обидва реалізовані в термінах третього екрана, file_slots."
|
||||
new "## Ці екрани дозволяють гравцеві зберегти гру та завантажити її знову. Оскільки вони мають майже все спільне, обидва реалізовані в термінах третього екрана, «file_slots»."
|
||||
|
||||
# gui/game/screens.rpy:579
|
||||
old "## https://www.renpy.org/doc/html/screen_special.html#save https://www.renpy.org/doc/html/screen_special.html#load"
|
||||
@@ -251,7 +251,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:598
|
||||
old "Automatic saves"
|
||||
new "Автозбереження"
|
||||
new "Автоматичні збереження"
|
||||
|
||||
# gui/game/screens.rpy:598
|
||||
old "Quick saves"
|
||||
@@ -259,7 +259,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:604
|
||||
old "## This ensures the input will get the enter event before any of the buttons do."
|
||||
new "## Це гарантує, що введення отримає подію enter раніше, ніж будь-яка кнопка."
|
||||
new "## Це гарантує, що введення отримає подію «enter» раніше, ніж будь-яка кнопка."
|
||||
|
||||
# gui/game/screens.rpy:608
|
||||
old "## The page name, which can be edited by clicking on a button."
|
||||
@@ -287,7 +287,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:660
|
||||
old "{#auto_page}A"
|
||||
new "{#auto_page}A"
|
||||
new "{#auto_page}А"
|
||||
|
||||
# gui/game/screens.rpy:663
|
||||
old "{#quick_page}Q"
|
||||
@@ -295,7 +295,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:665
|
||||
old "## range(1, 10) gives the numbers from 1 to 9."
|
||||
new "## діапазон(1, 10) дає числа від 1 до 9."
|
||||
new "## range(1, 10) дає числа від 1 до 9."
|
||||
|
||||
# gui/game/screens.rpy:669
|
||||
old ">"
|
||||
@@ -339,7 +339,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:737
|
||||
old "## Additional vboxes of type \"radio_pref\" or \"check_pref\" can be added here, to add additional creator-defined preferences."
|
||||
new "## Сюди можна долучити додаткові вікна vbox типу \"radio_pref\" або \"check_pref\", щоб долучити додаткові параметри, визначені творцем."
|
||||
new "## Сюди можна долучити додаткові вікна vbox типу «radio_pref» або «check_pref», щоб долучити додаткові параметри, визначені творцем."
|
||||
|
||||
# gui/game/screens.rpy:748
|
||||
old "Text Speed"
|
||||
@@ -347,7 +347,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:752
|
||||
old "Auto-Forward Time"
|
||||
new "Швидкість авточитання"
|
||||
new "Швидкість перемотки"
|
||||
|
||||
# gui/game/screens.rpy:759
|
||||
old "Music Volume"
|
||||
@@ -355,7 +355,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:766
|
||||
old "Sound Volume"
|
||||
new "Гучність ефектів"
|
||||
new "Гучність звук. ефектів"
|
||||
|
||||
# gui/game/screens.rpy:772
|
||||
old "Test"
|
||||
@@ -363,7 +363,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:776
|
||||
old "Voice Volume"
|
||||
new "Гучність голосу"
|
||||
new "Гучність озвучення"
|
||||
|
||||
# gui/game/screens.rpy:787
|
||||
old "Mute All"
|
||||
@@ -383,15 +383,15 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:875
|
||||
old "## Avoid predicting this screen, as it can be very large."
|
||||
new "## Уникайте прогнозування цього екрана, оскільки він може бути дуже великим."
|
||||
new "## Не намагайтеся передбачити цей екран, оскільки він може бути дуже великим."
|
||||
|
||||
# gui/game/screens.rpy:886
|
||||
old "## This lays things out properly if history_height is None."
|
||||
new "## Це правильно розкладає речі, якщо history_height дорівнює None."
|
||||
new "## Тут все буде правильно, якщо history_height дорівнює None."
|
||||
|
||||
# gui/game/screens.rpy:896
|
||||
old "## Take the color of the who text from the Character, if set."
|
||||
new "## Взяти колір тексту що від персонажа, якщо встановлено."
|
||||
new "## Бере колір тексту who з Character, якщо він заданий."
|
||||
|
||||
# gui/game/screens.rpy:905
|
||||
old "The dialogue history is empty."
|
||||
@@ -419,7 +419,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:978
|
||||
old "Gamepad"
|
||||
new "Геймпад"
|
||||
new "Ґеймпад"
|
||||
|
||||
# gui/game/screens.rpy:991
|
||||
old "Enter"
|
||||
@@ -427,7 +427,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:992
|
||||
old "Advances dialogue and activates the interface."
|
||||
new "Розвиває діалог і активує інтерфейс."
|
||||
new "Просуває діалог і вмикає інтерфейс."
|
||||
|
||||
# gui/game/screens.rpy:995
|
||||
old "Space"
|
||||
@@ -439,7 +439,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:999
|
||||
old "Arrow Keys"
|
||||
new "Стрілки"
|
||||
new "Клавіші зі стрілками"
|
||||
|
||||
# gui/game/screens.rpy:1000
|
||||
old "Navigate the interface."
|
||||
@@ -495,11 +495,11 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:1032
|
||||
old "Toggles assistive {a=https://www.renpy.org/l/voicing}self-voicing{/a}."
|
||||
new "Вмикає допоміжне {a=https://www.renpy.org/l/voicing}самоозвучення{/a}."
|
||||
new "Вмикає допоміжний {a=https://www.renpy.org/l/voicing}синтез мовлення{/a}."
|
||||
|
||||
# gui/game/screens.rpy:1036
|
||||
old "Opens the accessibility menu."
|
||||
new "Відкриває меню спеціальних можливостей."
|
||||
new "Відкриває меню доступності."
|
||||
|
||||
# gui/game/screens.rpy:1042
|
||||
old "Left Click"
|
||||
@@ -515,11 +515,11 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:1054
|
||||
old "Mouse Wheel Up\nClick Rollback Side"
|
||||
new "Колесо миші вгору\nбік повернення назад"
|
||||
new "Коліщатко миші вгору\nСторона відкату"
|
||||
|
||||
# gui/game/screens.rpy:1058
|
||||
old "Mouse Wheel Down"
|
||||
new "Колесо миші вниз"
|
||||
new "Коліщатко миші вниз"
|
||||
|
||||
# gui/game/screens.rpy:1065
|
||||
old "Right Trigger\nA/Bottom Button"
|
||||
@@ -535,15 +535,15 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:1078
|
||||
old "D-Pad, Sticks"
|
||||
new "D-Pad, Стіки"
|
||||
new "Хрестовина, Стики"
|
||||
|
||||
# gui/game/screens.rpy:1082
|
||||
old "Start, Guide"
|
||||
new "Старт, Гайд"
|
||||
new "Start, Guide"
|
||||
|
||||
# gui/game/screens.rpy:1086
|
||||
old "Y/Top Button"
|
||||
new "Кнопка Y/Top"
|
||||
new "Y/Верхня кнопка"
|
||||
|
||||
# gui/game/screens.rpy:1089
|
||||
old "Calibrate"
|
||||
@@ -559,7 +559,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:1123
|
||||
old "## The confirm screen is called when Ren'Py wants to ask the player a yes or no question."
|
||||
new "## Екран підтвердження викликається, коли Ren'Py хоче поставити гравцеві запитання «так» або «ні»."
|
||||
new "## Екран підтвердження викликається, коли Ren'Py хоче поставити гравцеві запитання «Так» або «Ні»."
|
||||
|
||||
# gui/game/screens.rpy:1126
|
||||
old "## https://www.renpy.org/doc/html/screen_special.html#confirm"
|
||||
@@ -579,7 +579,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:1157
|
||||
old "## Right-click and escape answer \"no\"."
|
||||
new "## Клацніть правою кнопкою миші та вийдіть із відповіді \"no\"."
|
||||
new "## Клацніть ПКМ та вийдіть із відповіді «Ні»."
|
||||
|
||||
# gui/game/screens.rpy:1184
|
||||
old "## Skip indicator screen"
|
||||
@@ -623,7 +623,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:1281
|
||||
old "## This screen is used for NVL-mode dialogue and menus."
|
||||
new "## Відображає діалог у vpgrid або vbox."
|
||||
new "## Цей екран використовується для діалогу та меню режиму NVL."
|
||||
|
||||
# gui/game/screens.rpy:1283
|
||||
old "## https://www.renpy.org/doc/html/screen_special.html#nvl"
|
||||
@@ -635,7 +635,7 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:1307
|
||||
old "## Displays the menu, if given. The menu may be displayed incorrectly if config.narrator_menu is set to True."
|
||||
new "## Відображає меню, якщо є. Меню може відображатися неправильно, якщо для config.narrator_menu встановлено значення True."
|
||||
new "## Відображає меню, якщо є. Меню може відображатися неправильно, якщо для config.narrator_menu задано значення True."
|
||||
|
||||
# gui/game/screens.rpy:1337
|
||||
old "## This controls the maximum number of NVL-mode entries that can be displayed at once."
|
||||
@@ -658,23 +658,19 @@ translate ukrainian strings:
|
||||
|
||||
# gui/game/screens.rpy:676
|
||||
old "Upload Sync"
|
||||
|
||||
new "Вивантажити синхронізацію"
|
||||
|
||||
# gui/game/screens.rpy:680
|
||||
old "Download Sync"
|
||||
|
||||
new "Завантажити синхронізацію"
|
||||
|
||||
# gui/game/screens.rpy:1410
|
||||
old "## Bubble screen"
|
||||
|
||||
new "## Екран з бульбашками"
|
||||
|
||||
# gui/game/screens.rpy:1412
|
||||
old "## The bubble screen is used to display dialogue to the player when using speech bubbles. The bubble screen takes the same parameters as the say screen, must create a displayable with the id of \"what\", and can create displayables with the \"namebox\", \"who\", and \"window\" ids."
|
||||
|
||||
new "## Екран бульбашок використовується для показу діалогу гравцеві під час використання мовних бульбашок. Екран бульбашок має ті самі параметри, що й екран слів, має створювати елемент відображення з ідентифікатором \"what\", а також може створювати елементи відображення з ідентифікаторами \"namebox\", \"who\" і \"window\"."
|
||||
new "## Екран бульбашок використовується для показу діалогу гравцеві під час використання мовних бульбашок. Екран бульбашок має ті самі параметри, що й екран слів, має створювати елемент відображення з ідентифікатором «what», а також може створювати елементи відображення з ідентифікаторами «namebox», «who» і «window»."
|
||||
|
||||
# gui/game/screens.rpy:1417
|
||||
old "## https://www.renpy.org/doc/html/bubble.html#bubble-screen"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# У цьому файлі міститься скрипт гри.
|
||||
|
||||
# Оголошення символів, які використовуються в цій грі. Колірний аргумент розфарбовує
|
||||
# ім'я персонажа.
|
||||
# Оголошення персонажів, які використовуються в цій грі. Колірний аргумент розфарбовує
|
||||
# ім’я персонажа.
|
||||
|
||||
define e = Character("Ейлін")
|
||||
|
||||
@@ -10,14 +10,14 @@ define e = Character("Ейлін")
|
||||
|
||||
label start:
|
||||
|
||||
# Це показує фон. За замовчуванням використовується заповнювач, але ви можете
|
||||
# додати файл (з назвою "bg room.png" або "bg room.jpg") до
|
||||
# Це показує тло. За стандратом використовується заповнювач, але ви можете
|
||||
# додати файл (з назвою «bg room.png» або «bg room.jpg») до
|
||||
# теки images, щоб показати його.
|
||||
|
||||
scene bg room
|
||||
|
||||
# Це показує спрайт персонажа. Використовується заповнювач, але ви можете
|
||||
# замінити його, додавши до зображень файл із назвою "eileen happy.png".
|
||||
# замінити його, додавши до зображень файл із назвою «eileen happy.png».
|
||||
# до теки.
|
||||
|
||||
show eileen happy
|
||||
|
||||
@@ -115,7 +115,7 @@ screen update_channel(channels):
|
||||
for c in channels:
|
||||
|
||||
if c["split_version"] != list(renpy.version_tuple) or allow_repair_update:
|
||||
$ action = [SetField(persistent, "has_update", None), SetField(persistent, "last_update_check", None), updater.Update(c["url"], simulate=UPDATE_SIMULATE, public_key=PUBLIC_KEY, confirm=False, force=allow_repair_update)]
|
||||
$ action = [SetField(persistent, "has_update", None), SetField(persistent, "last_update_check", None), updater.Update(c["url"], simulate=UPDATE_SIMULATE, public_key=PUBLIC_KEY, confirm=False, force=allow_repair_update, prefer_rpu=c.get("prefer_rpu", False))]
|
||||
|
||||
if c["channel"].startswith("Release"):
|
||||
$ current = _("• {a=https://www.renpy.org/doc/html/changelog.html}View change log{/a}")
|
||||
@@ -132,15 +132,14 @@ screen update_channel(channels):
|
||||
|
||||
hbox:
|
||||
spacing 7
|
||||
textbutton c["channel"] action action
|
||||
|
||||
|
||||
textbutton c["channel"] action action
|
||||
|
||||
add HALF_SPACER
|
||||
|
||||
$ date = _strftime(__("%B %d, %Y"), time.localtime(c["timestamp"]))
|
||||
$ pretty = c["pretty_version"]
|
||||
|
||||
text "[date] • [c[pretty_version]] [current!t]" style "l_small_text"
|
||||
text "[date] • [pretty] [current!t]" style "l_small_text"
|
||||
|
||||
add HALF_SPACER
|
||||
|
||||
|
||||
@@ -228,11 +228,11 @@ init python:
|
||||
return f_rule
|
||||
return False
|
||||
|
||||
def generate_pwa_icons(p, destination):
|
||||
def generate_web_icons(p, destination):
|
||||
"""
|
||||
Checks if the pwa_icon.png file exists in the game folder and generates
|
||||
Checks if the web-icon.png file exists in the game folder and generates
|
||||
required icons for PWA in subdirectory icons/ in the destination folder.
|
||||
If no pwa_icon.png is found, the default Ren'Py icon is used instead in
|
||||
If no web-icon.png is found, the default Ren'Py icon is used instead in
|
||||
the web folder, if exists.
|
||||
"""
|
||||
# Check if there's a custom icon in the game directory
|
||||
@@ -430,7 +430,7 @@ init python:
|
||||
|
||||
# Copy the files from WEB_PATH to destination.
|
||||
for fn in os.listdir(WEB_PATH):
|
||||
if fn in { "game.zip", "hash.txt", "index.html", "pwa_icon.png" }:
|
||||
if fn in { "game.zip", "hash.txt", "index.html", "web-icon.png" }:
|
||||
continue
|
||||
|
||||
shutil.copy(os.path.join(WEB_PATH, fn), os.path.join(destination, fn))
|
||||
@@ -466,7 +466,7 @@ init python:
|
||||
f.write(html)
|
||||
|
||||
if not PY2:
|
||||
generate_pwa_icons(p, destination)
|
||||
generate_web_icons(p, destination)
|
||||
prepare_pwa_files(p, destination)
|
||||
|
||||
# Zip up the game.
|
||||
|
||||
+319
-18
@@ -1,11 +1,11 @@
|
||||
# emoji-test.txt
|
||||
# Date: 2022-08-12, 20:24:39 GMT
|
||||
# © 2022 Unicode®, Inc.
|
||||
# Date: 2023-06-05, 21:39:54 GMT
|
||||
# © 2023 Unicode®, Inc.
|
||||
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
|
||||
# For terms of use, see https://www.unicode.org/terms_of_use.html
|
||||
#
|
||||
# Emoji Keyboard/Display Test Data for UTS #51
|
||||
# Version: 15.0
|
||||
# Version: 15.1
|
||||
#
|
||||
# For documentation and usage, see https://www.unicode.org/reports/tr51
|
||||
#
|
||||
@@ -93,6 +93,10 @@
|
||||
1F62E 200D 1F4A8 ; fully-qualified # 😮💨 E13.1 face exhaling
|
||||
1F925 ; fully-qualified # 🤥 E3.0 lying face
|
||||
1FAE8 ; fully-qualified # 🫨 E15.0 shaking face
|
||||
1F642 200D 2194 FE0F ; fully-qualified # 🙂↔️ E15.1 head shaking horizontally
|
||||
1F642 200D 2194 ; minimally-qualified # 🙂↔ E15.1 head shaking horizontally
|
||||
1F642 200D 2195 FE0F ; fully-qualified # 🙂↕️ E15.1 head shaking vertically
|
||||
1F642 200D 2195 ; minimally-qualified # 🙂↕ E15.1 head shaking vertically
|
||||
|
||||
# subgroup: face-sleepy
|
||||
1F60C ; fully-qualified # 😌 E0.6 relieved face
|
||||
@@ -244,8 +248,8 @@
|
||||
1F4AD ; fully-qualified # 💭 E1.0 thought balloon
|
||||
1F4A4 ; fully-qualified # 💤 E0.6 ZZZ
|
||||
|
||||
# Smileys & Emotion subtotal: 180
|
||||
# Smileys & Emotion subtotal: 180 w/o modifiers
|
||||
# Smileys & Emotion subtotal: 184
|
||||
# Smileys & Emotion subtotal: 184 w/o modifiers
|
||||
|
||||
# group: People & Body
|
||||
|
||||
@@ -2065,6 +2069,66 @@
|
||||
1F6B6 1F3FE 200D 2640 ; minimally-qualified # 🚶🏾♀ E4.0 woman walking: medium-dark skin tone
|
||||
1F6B6 1F3FF 200D 2640 FE0F ; fully-qualified # 🚶🏿♀️ E4.0 woman walking: dark skin tone
|
||||
1F6B6 1F3FF 200D 2640 ; minimally-qualified # 🚶🏿♀ E4.0 woman walking: dark skin tone
|
||||
1F6B6 200D 27A1 FE0F ; fully-qualified # 🚶➡️ E15.1 person walking facing right
|
||||
1F6B6 200D 27A1 ; minimally-qualified # 🚶➡ E15.1 person walking facing right
|
||||
1F6B6 1F3FB 200D 27A1 FE0F ; fully-qualified # 🚶🏻➡️ E15.1 person walking facing right: light skin tone
|
||||
1F6B6 1F3FB 200D 27A1 ; minimally-qualified # 🚶🏻➡ E15.1 person walking facing right: light skin tone
|
||||
1F6B6 1F3FC 200D 27A1 FE0F ; fully-qualified # 🚶🏼➡️ E15.1 person walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FC 200D 27A1 ; minimally-qualified # 🚶🏼➡ E15.1 person walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FD 200D 27A1 FE0F ; fully-qualified # 🚶🏽➡️ E15.1 person walking facing right: medium skin tone
|
||||
1F6B6 1F3FD 200D 27A1 ; minimally-qualified # 🚶🏽➡ E15.1 person walking facing right: medium skin tone
|
||||
1F6B6 1F3FE 200D 27A1 FE0F ; fully-qualified # 🚶🏾➡️ E15.1 person walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FE 200D 27A1 ; minimally-qualified # 🚶🏾➡ E15.1 person walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FF 200D 27A1 FE0F ; fully-qualified # 🚶🏿➡️ E15.1 person walking facing right: dark skin tone
|
||||
1F6B6 1F3FF 200D 27A1 ; minimally-qualified # 🚶🏿➡ E15.1 person walking facing right: dark skin tone
|
||||
1F6B6 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶♀️➡️ E15.1 woman walking facing right
|
||||
1F6B6 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🚶♀➡️ E15.1 woman walking facing right
|
||||
1F6B6 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🚶♀️➡ E15.1 woman walking facing right
|
||||
1F6B6 200D 2640 200D 27A1 ; minimally-qualified # 🚶♀➡ E15.1 woman walking facing right
|
||||
1F6B6 1F3FB 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏻♀️➡️ E15.1 woman walking facing right: light skin tone
|
||||
1F6B6 1F3FB 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🚶🏻♀➡️ E15.1 woman walking facing right: light skin tone
|
||||
1F6B6 1F3FB 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🚶🏻♀️➡ E15.1 woman walking facing right: light skin tone
|
||||
1F6B6 1F3FB 200D 2640 200D 27A1 ; minimally-qualified # 🚶🏻♀➡ E15.1 woman walking facing right: light skin tone
|
||||
1F6B6 1F3FC 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏼♀️➡️ E15.1 woman walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FC 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🚶🏼♀➡️ E15.1 woman walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FC 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🚶🏼♀️➡ E15.1 woman walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FC 200D 2640 200D 27A1 ; minimally-qualified # 🚶🏼♀➡ E15.1 woman walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FD 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏽♀️➡️ E15.1 woman walking facing right: medium skin tone
|
||||
1F6B6 1F3FD 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🚶🏽♀➡️ E15.1 woman walking facing right: medium skin tone
|
||||
1F6B6 1F3FD 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🚶🏽♀️➡ E15.1 woman walking facing right: medium skin tone
|
||||
1F6B6 1F3FD 200D 2640 200D 27A1 ; minimally-qualified # 🚶🏽♀➡ E15.1 woman walking facing right: medium skin tone
|
||||
1F6B6 1F3FE 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏾♀️➡️ E15.1 woman walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FE 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🚶🏾♀➡️ E15.1 woman walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FE 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🚶🏾♀️➡ E15.1 woman walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FE 200D 2640 200D 27A1 ; minimally-qualified # 🚶🏾♀➡ E15.1 woman walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FF 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏿♀️➡️ E15.1 woman walking facing right: dark skin tone
|
||||
1F6B6 1F3FF 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🚶🏿♀➡️ E15.1 woman walking facing right: dark skin tone
|
||||
1F6B6 1F3FF 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🚶🏿♀️➡ E15.1 woman walking facing right: dark skin tone
|
||||
1F6B6 1F3FF 200D 2640 200D 27A1 ; minimally-qualified # 🚶🏿♀➡ E15.1 woman walking facing right: dark skin tone
|
||||
1F6B6 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶♂️➡️ E15.1 man walking facing right
|
||||
1F6B6 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🚶♂➡️ E15.1 man walking facing right
|
||||
1F6B6 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🚶♂️➡ E15.1 man walking facing right
|
||||
1F6B6 200D 2642 200D 27A1 ; minimally-qualified # 🚶♂➡ E15.1 man walking facing right
|
||||
1F6B6 1F3FB 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏻♂️➡️ E15.1 man walking facing right: light skin tone
|
||||
1F6B6 1F3FB 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🚶🏻♂➡️ E15.1 man walking facing right: light skin tone
|
||||
1F6B6 1F3FB 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🚶🏻♂️➡ E15.1 man walking facing right: light skin tone
|
||||
1F6B6 1F3FB 200D 2642 200D 27A1 ; minimally-qualified # 🚶🏻♂➡ E15.1 man walking facing right: light skin tone
|
||||
1F6B6 1F3FC 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏼♂️➡️ E15.1 man walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FC 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🚶🏼♂➡️ E15.1 man walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FC 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🚶🏼♂️➡ E15.1 man walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FC 200D 2642 200D 27A1 ; minimally-qualified # 🚶🏼♂➡ E15.1 man walking facing right: medium-light skin tone
|
||||
1F6B6 1F3FD 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏽♂️➡️ E15.1 man walking facing right: medium skin tone
|
||||
1F6B6 1F3FD 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🚶🏽♂➡️ E15.1 man walking facing right: medium skin tone
|
||||
1F6B6 1F3FD 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🚶🏽♂️➡ E15.1 man walking facing right: medium skin tone
|
||||
1F6B6 1F3FD 200D 2642 200D 27A1 ; minimally-qualified # 🚶🏽♂➡ E15.1 man walking facing right: medium skin tone
|
||||
1F6B6 1F3FE 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏾♂️➡️ E15.1 man walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FE 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🚶🏾♂➡️ E15.1 man walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FE 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🚶🏾♂️➡ E15.1 man walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FE 200D 2642 200D 27A1 ; minimally-qualified # 🚶🏾♂➡ E15.1 man walking facing right: medium-dark skin tone
|
||||
1F6B6 1F3FF 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🚶🏿♂️➡️ E15.1 man walking facing right: dark skin tone
|
||||
1F6B6 1F3FF 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🚶🏿♂➡️ E15.1 man walking facing right: dark skin tone
|
||||
1F6B6 1F3FF 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🚶🏿♂️➡ E15.1 man walking facing right: dark skin tone
|
||||
1F6B6 1F3FF 200D 2642 200D 27A1 ; minimally-qualified # 🚶🏿♂➡ E15.1 man walking facing right: dark skin tone
|
||||
1F9CD ; fully-qualified # 🧍 E12.0 person standing
|
||||
1F9CD 1F3FB ; fully-qualified # 🧍🏻 E12.0 person standing: light skin tone
|
||||
1F9CD 1F3FC ; fully-qualified # 🧍🏼 E12.0 person standing: medium-light skin tone
|
||||
@@ -2125,60 +2189,228 @@
|
||||
1F9CE 1F3FE 200D 2640 ; minimally-qualified # 🧎🏾♀ E12.0 woman kneeling: medium-dark skin tone
|
||||
1F9CE 1F3FF 200D 2640 FE0F ; fully-qualified # 🧎🏿♀️ E12.0 woman kneeling: dark skin tone
|
||||
1F9CE 1F3FF 200D 2640 ; minimally-qualified # 🧎🏿♀ E12.0 woman kneeling: dark skin tone
|
||||
1F9CE 200D 27A1 FE0F ; fully-qualified # 🧎➡️ E15.1 person kneeling facing right
|
||||
1F9CE 200D 27A1 ; minimally-qualified # 🧎➡ E15.1 person kneeling facing right
|
||||
1F9CE 1F3FB 200D 27A1 FE0F ; fully-qualified # 🧎🏻➡️ E15.1 person kneeling facing right: light skin tone
|
||||
1F9CE 1F3FB 200D 27A1 ; minimally-qualified # 🧎🏻➡ E15.1 person kneeling facing right: light skin tone
|
||||
1F9CE 1F3FC 200D 27A1 FE0F ; fully-qualified # 🧎🏼➡️ E15.1 person kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FC 200D 27A1 ; minimally-qualified # 🧎🏼➡ E15.1 person kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FD 200D 27A1 FE0F ; fully-qualified # 🧎🏽➡️ E15.1 person kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FD 200D 27A1 ; minimally-qualified # 🧎🏽➡ E15.1 person kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FE 200D 27A1 FE0F ; fully-qualified # 🧎🏾➡️ E15.1 person kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FE 200D 27A1 ; minimally-qualified # 🧎🏾➡ E15.1 person kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FF 200D 27A1 FE0F ; fully-qualified # 🧎🏿➡️ E15.1 person kneeling facing right: dark skin tone
|
||||
1F9CE 1F3FF 200D 27A1 ; minimally-qualified # 🧎🏿➡ E15.1 person kneeling facing right: dark skin tone
|
||||
1F9CE 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎♀️➡️ E15.1 woman kneeling facing right
|
||||
1F9CE 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🧎♀➡️ E15.1 woman kneeling facing right
|
||||
1F9CE 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🧎♀️➡ E15.1 woman kneeling facing right
|
||||
1F9CE 200D 2640 200D 27A1 ; minimally-qualified # 🧎♀➡ E15.1 woman kneeling facing right
|
||||
1F9CE 1F3FB 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏻♀️➡️ E15.1 woman kneeling facing right: light skin tone
|
||||
1F9CE 1F3FB 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🧎🏻♀➡️ E15.1 woman kneeling facing right: light skin tone
|
||||
1F9CE 1F3FB 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🧎🏻♀️➡ E15.1 woman kneeling facing right: light skin tone
|
||||
1F9CE 1F3FB 200D 2640 200D 27A1 ; minimally-qualified # 🧎🏻♀➡ E15.1 woman kneeling facing right: light skin tone
|
||||
1F9CE 1F3FC 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏼♀️➡️ E15.1 woman kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FC 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🧎🏼♀➡️ E15.1 woman kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FC 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🧎🏼♀️➡ E15.1 woman kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FC 200D 2640 200D 27A1 ; minimally-qualified # 🧎🏼♀➡ E15.1 woman kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FD 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏽♀️➡️ E15.1 woman kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FD 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🧎🏽♀➡️ E15.1 woman kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FD 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🧎🏽♀️➡ E15.1 woman kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FD 200D 2640 200D 27A1 ; minimally-qualified # 🧎🏽♀➡ E15.1 woman kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FE 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏾♀️➡️ E15.1 woman kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FE 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🧎🏾♀➡️ E15.1 woman kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FE 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🧎🏾♀️➡ E15.1 woman kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FE 200D 2640 200D 27A1 ; minimally-qualified # 🧎🏾♀➡ E15.1 woman kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FF 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏿♀️➡️ E15.1 woman kneeling facing right: dark skin tone
|
||||
1F9CE 1F3FF 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🧎🏿♀➡️ E15.1 woman kneeling facing right: dark skin tone
|
||||
1F9CE 1F3FF 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🧎🏿♀️➡ E15.1 woman kneeling facing right: dark skin tone
|
||||
1F9CE 1F3FF 200D 2640 200D 27A1 ; minimally-qualified # 🧎🏿♀➡ E15.1 woman kneeling facing right: dark skin tone
|
||||
1F9CE 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎♂️➡️ E15.1 man kneeling facing right
|
||||
1F9CE 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🧎♂➡️ E15.1 man kneeling facing right
|
||||
1F9CE 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🧎♂️➡ E15.1 man kneeling facing right
|
||||
1F9CE 200D 2642 200D 27A1 ; minimally-qualified # 🧎♂➡ E15.1 man kneeling facing right
|
||||
1F9CE 1F3FB 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏻♂️➡️ E15.1 man kneeling facing right: light skin tone
|
||||
1F9CE 1F3FB 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🧎🏻♂➡️ E15.1 man kneeling facing right: light skin tone
|
||||
1F9CE 1F3FB 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🧎🏻♂️➡ E15.1 man kneeling facing right: light skin tone
|
||||
1F9CE 1F3FB 200D 2642 200D 27A1 ; minimally-qualified # 🧎🏻♂➡ E15.1 man kneeling facing right: light skin tone
|
||||
1F9CE 1F3FC 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏼♂️➡️ E15.1 man kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FC 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🧎🏼♂➡️ E15.1 man kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FC 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🧎🏼♂️➡ E15.1 man kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FC 200D 2642 200D 27A1 ; minimally-qualified # 🧎🏼♂➡ E15.1 man kneeling facing right: medium-light skin tone
|
||||
1F9CE 1F3FD 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏽♂️➡️ E15.1 man kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FD 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🧎🏽♂➡️ E15.1 man kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FD 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🧎🏽♂️➡ E15.1 man kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FD 200D 2642 200D 27A1 ; minimally-qualified # 🧎🏽♂➡ E15.1 man kneeling facing right: medium skin tone
|
||||
1F9CE 1F3FE 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏾♂️➡️ E15.1 man kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FE 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🧎🏾♂➡️ E15.1 man kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FE 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🧎🏾♂️➡ E15.1 man kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FE 200D 2642 200D 27A1 ; minimally-qualified # 🧎🏾♂➡ E15.1 man kneeling facing right: medium-dark skin tone
|
||||
1F9CE 1F3FF 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🧎🏿♂️➡️ E15.1 man kneeling facing right: dark skin tone
|
||||
1F9CE 1F3FF 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🧎🏿♂➡️ E15.1 man kneeling facing right: dark skin tone
|
||||
1F9CE 1F3FF 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🧎🏿♂️➡ E15.1 man kneeling facing right: dark skin tone
|
||||
1F9CE 1F3FF 200D 2642 200D 27A1 ; minimally-qualified # 🧎🏿♂➡ E15.1 man kneeling facing right: dark skin tone
|
||||
1F9D1 200D 1F9AF ; fully-qualified # 🧑🦯 E12.1 person with white cane
|
||||
1F9D1 1F3FB 200D 1F9AF ; fully-qualified # 🧑🏻🦯 E12.1 person with white cane: light skin tone
|
||||
1F9D1 1F3FC 200D 1F9AF ; fully-qualified # 🧑🏼🦯 E12.1 person with white cane: medium-light skin tone
|
||||
1F9D1 1F3FD 200D 1F9AF ; fully-qualified # 🧑🏽🦯 E12.1 person with white cane: medium skin tone
|
||||
1F9D1 1F3FE 200D 1F9AF ; fully-qualified # 🧑🏾🦯 E12.1 person with white cane: medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9AF ; fully-qualified # 🧑🏿🦯 E12.1 person with white cane: dark skin tone
|
||||
1F9D1 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 🧑🦯➡️ E15.1 person with white cane facing right
|
||||
1F9D1 200D 1F9AF 200D 27A1 ; minimally-qualified # 🧑🦯➡ E15.1 person with white cane facing right
|
||||
1F9D1 1F3FB 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 🧑🏻🦯➡️ E15.1 person with white cane facing right: light skin tone
|
||||
1F9D1 1F3FB 200D 1F9AF 200D 27A1 ; minimally-qualified # 🧑🏻🦯➡ E15.1 person with white cane facing right: light skin tone
|
||||
1F9D1 1F3FC 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 🧑🏼🦯➡️ E15.1 person with white cane facing right: medium-light skin tone
|
||||
1F9D1 1F3FC 200D 1F9AF 200D 27A1 ; minimally-qualified # 🧑🏼🦯➡ E15.1 person with white cane facing right: medium-light skin tone
|
||||
1F9D1 1F3FD 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 🧑🏽🦯➡️ E15.1 person with white cane facing right: medium skin tone
|
||||
1F9D1 1F3FD 200D 1F9AF 200D 27A1 ; minimally-qualified # 🧑🏽🦯➡ E15.1 person with white cane facing right: medium skin tone
|
||||
1F9D1 1F3FE 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 🧑🏾🦯➡️ E15.1 person with white cane facing right: medium-dark skin tone
|
||||
1F9D1 1F3FE 200D 1F9AF 200D 27A1 ; minimally-qualified # 🧑🏾🦯➡ E15.1 person with white cane facing right: medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 🧑🏿🦯➡️ E15.1 person with white cane facing right: dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9AF 200D 27A1 ; minimally-qualified # 🧑🏿🦯➡ E15.1 person with white cane facing right: dark skin tone
|
||||
1F468 200D 1F9AF ; fully-qualified # 👨🦯 E12.0 man with white cane
|
||||
1F468 1F3FB 200D 1F9AF ; fully-qualified # 👨🏻🦯 E12.0 man with white cane: light skin tone
|
||||
1F468 1F3FC 200D 1F9AF ; fully-qualified # 👨🏼🦯 E12.0 man with white cane: medium-light skin tone
|
||||
1F468 1F3FD 200D 1F9AF ; fully-qualified # 👨🏽🦯 E12.0 man with white cane: medium skin tone
|
||||
1F468 1F3FE 200D 1F9AF ; fully-qualified # 👨🏾🦯 E12.0 man with white cane: medium-dark skin tone
|
||||
1F468 1F3FF 200D 1F9AF ; fully-qualified # 👨🏿🦯 E12.0 man with white cane: dark skin tone
|
||||
1F468 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👨🦯➡️ E15.1 man with white cane facing right
|
||||
1F468 200D 1F9AF 200D 27A1 ; minimally-qualified # 👨🦯➡ E15.1 man with white cane facing right
|
||||
1F468 1F3FB 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👨🏻🦯➡️ E15.1 man with white cane facing right: light skin tone
|
||||
1F468 1F3FB 200D 1F9AF 200D 27A1 ; minimally-qualified # 👨🏻🦯➡ E15.1 man with white cane facing right: light skin tone
|
||||
1F468 1F3FC 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👨🏼🦯➡️ E15.1 man with white cane facing right: medium-light skin tone
|
||||
1F468 1F3FC 200D 1F9AF 200D 27A1 ; minimally-qualified # 👨🏼🦯➡ E15.1 man with white cane facing right: medium-light skin tone
|
||||
1F468 1F3FD 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👨🏽🦯➡️ E15.1 man with white cane facing right: medium skin tone
|
||||
1F468 1F3FD 200D 1F9AF 200D 27A1 ; minimally-qualified # 👨🏽🦯➡ E15.1 man with white cane facing right: medium skin tone
|
||||
1F468 1F3FE 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👨🏾🦯➡️ E15.1 man with white cane facing right: medium-dark skin tone
|
||||
1F468 1F3FE 200D 1F9AF 200D 27A1 ; minimally-qualified # 👨🏾🦯➡ E15.1 man with white cane facing right: medium-dark skin tone
|
||||
1F468 1F3FF 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👨🏿🦯➡️ E15.1 man with white cane facing right: dark skin tone
|
||||
1F468 1F3FF 200D 1F9AF 200D 27A1 ; minimally-qualified # 👨🏿🦯➡ E15.1 man with white cane facing right: dark skin tone
|
||||
1F469 200D 1F9AF ; fully-qualified # 👩🦯 E12.0 woman with white cane
|
||||
1F469 1F3FB 200D 1F9AF ; fully-qualified # 👩🏻🦯 E12.0 woman with white cane: light skin tone
|
||||
1F469 1F3FC 200D 1F9AF ; fully-qualified # 👩🏼🦯 E12.0 woman with white cane: medium-light skin tone
|
||||
1F469 1F3FD 200D 1F9AF ; fully-qualified # 👩🏽🦯 E12.0 woman with white cane: medium skin tone
|
||||
1F469 1F3FE 200D 1F9AF ; fully-qualified # 👩🏾🦯 E12.0 woman with white cane: medium-dark skin tone
|
||||
1F469 1F3FF 200D 1F9AF ; fully-qualified # 👩🏿🦯 E12.0 woman with white cane: dark skin tone
|
||||
1F469 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👩🦯➡️ E15.1 woman with white cane facing right
|
||||
1F469 200D 1F9AF 200D 27A1 ; minimally-qualified # 👩🦯➡ E15.1 woman with white cane facing right
|
||||
1F469 1F3FB 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👩🏻🦯➡️ E15.1 woman with white cane facing right: light skin tone
|
||||
1F469 1F3FB 200D 1F9AF 200D 27A1 ; minimally-qualified # 👩🏻🦯➡ E15.1 woman with white cane facing right: light skin tone
|
||||
1F469 1F3FC 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👩🏼🦯➡️ E15.1 woman with white cane facing right: medium-light skin tone
|
||||
1F469 1F3FC 200D 1F9AF 200D 27A1 ; minimally-qualified # 👩🏼🦯➡ E15.1 woman with white cane facing right: medium-light skin tone
|
||||
1F469 1F3FD 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👩🏽🦯➡️ E15.1 woman with white cane facing right: medium skin tone
|
||||
1F469 1F3FD 200D 1F9AF 200D 27A1 ; minimally-qualified # 👩🏽🦯➡ E15.1 woman with white cane facing right: medium skin tone
|
||||
1F469 1F3FE 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👩🏾🦯➡️ E15.1 woman with white cane facing right: medium-dark skin tone
|
||||
1F469 1F3FE 200D 1F9AF 200D 27A1 ; minimally-qualified # 👩🏾🦯➡ E15.1 woman with white cane facing right: medium-dark skin tone
|
||||
1F469 1F3FF 200D 1F9AF 200D 27A1 FE0F ; fully-qualified # 👩🏿🦯➡️ E15.1 woman with white cane facing right: dark skin tone
|
||||
1F469 1F3FF 200D 1F9AF 200D 27A1 ; minimally-qualified # 👩🏿🦯➡ E15.1 woman with white cane facing right: dark skin tone
|
||||
1F9D1 200D 1F9BC ; fully-qualified # 🧑🦼 E12.1 person in motorized wheelchair
|
||||
1F9D1 1F3FB 200D 1F9BC ; fully-qualified # 🧑🏻🦼 E12.1 person in motorized wheelchair: light skin tone
|
||||
1F9D1 1F3FC 200D 1F9BC ; fully-qualified # 🧑🏼🦼 E12.1 person in motorized wheelchair: medium-light skin tone
|
||||
1F9D1 1F3FD 200D 1F9BC ; fully-qualified # 🧑🏽🦼 E12.1 person in motorized wheelchair: medium skin tone
|
||||
1F9D1 1F3FE 200D 1F9BC ; fully-qualified # 🧑🏾🦼 E12.1 person in motorized wheelchair: medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9BC ; fully-qualified # 🧑🏿🦼 E12.1 person in motorized wheelchair: dark skin tone
|
||||
1F9D1 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 🧑🦼➡️ E15.1 person in motorized wheelchair facing right
|
||||
1F9D1 200D 1F9BC 200D 27A1 ; minimally-qualified # 🧑🦼➡ E15.1 person in motorized wheelchair facing right
|
||||
1F9D1 1F3FB 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 🧑🏻🦼➡️ E15.1 person in motorized wheelchair facing right: light skin tone
|
||||
1F9D1 1F3FB 200D 1F9BC 200D 27A1 ; minimally-qualified # 🧑🏻🦼➡ E15.1 person in motorized wheelchair facing right: light skin tone
|
||||
1F9D1 1F3FC 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 🧑🏼🦼➡️ E15.1 person in motorized wheelchair facing right: medium-light skin tone
|
||||
1F9D1 1F3FC 200D 1F9BC 200D 27A1 ; minimally-qualified # 🧑🏼🦼➡ E15.1 person in motorized wheelchair facing right: medium-light skin tone
|
||||
1F9D1 1F3FD 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 🧑🏽🦼➡️ E15.1 person in motorized wheelchair facing right: medium skin tone
|
||||
1F9D1 1F3FD 200D 1F9BC 200D 27A1 ; minimally-qualified # 🧑🏽🦼➡ E15.1 person in motorized wheelchair facing right: medium skin tone
|
||||
1F9D1 1F3FE 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 🧑🏾🦼➡️ E15.1 person in motorized wheelchair facing right: medium-dark skin tone
|
||||
1F9D1 1F3FE 200D 1F9BC 200D 27A1 ; minimally-qualified # 🧑🏾🦼➡ E15.1 person in motorized wheelchair facing right: medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 🧑🏿🦼➡️ E15.1 person in motorized wheelchair facing right: dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9BC 200D 27A1 ; minimally-qualified # 🧑🏿🦼➡ E15.1 person in motorized wheelchair facing right: dark skin tone
|
||||
1F468 200D 1F9BC ; fully-qualified # 👨🦼 E12.0 man in motorized wheelchair
|
||||
1F468 1F3FB 200D 1F9BC ; fully-qualified # 👨🏻🦼 E12.0 man in motorized wheelchair: light skin tone
|
||||
1F468 1F3FC 200D 1F9BC ; fully-qualified # 👨🏼🦼 E12.0 man in motorized wheelchair: medium-light skin tone
|
||||
1F468 1F3FD 200D 1F9BC ; fully-qualified # 👨🏽🦼 E12.0 man in motorized wheelchair: medium skin tone
|
||||
1F468 1F3FE 200D 1F9BC ; fully-qualified # 👨🏾🦼 E12.0 man in motorized wheelchair: medium-dark skin tone
|
||||
1F468 1F3FF 200D 1F9BC ; fully-qualified # 👨🏿🦼 E12.0 man in motorized wheelchair: dark skin tone
|
||||
1F468 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👨🦼➡️ E15.1 man in motorized wheelchair facing right
|
||||
1F468 200D 1F9BC 200D 27A1 ; minimally-qualified # 👨🦼➡ E15.1 man in motorized wheelchair facing right
|
||||
1F468 1F3FB 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👨🏻🦼➡️ E15.1 man in motorized wheelchair facing right: light skin tone
|
||||
1F468 1F3FB 200D 1F9BC 200D 27A1 ; minimally-qualified # 👨🏻🦼➡ E15.1 man in motorized wheelchair facing right: light skin tone
|
||||
1F468 1F3FC 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👨🏼🦼➡️ E15.1 man in motorized wheelchair facing right: medium-light skin tone
|
||||
1F468 1F3FC 200D 1F9BC 200D 27A1 ; minimally-qualified # 👨🏼🦼➡ E15.1 man in motorized wheelchair facing right: medium-light skin tone
|
||||
1F468 1F3FD 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👨🏽🦼➡️ E15.1 man in motorized wheelchair facing right: medium skin tone
|
||||
1F468 1F3FD 200D 1F9BC 200D 27A1 ; minimally-qualified # 👨🏽🦼➡ E15.1 man in motorized wheelchair facing right: medium skin tone
|
||||
1F468 1F3FE 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👨🏾🦼➡️ E15.1 man in motorized wheelchair facing right: medium-dark skin tone
|
||||
1F468 1F3FE 200D 1F9BC 200D 27A1 ; minimally-qualified # 👨🏾🦼➡ E15.1 man in motorized wheelchair facing right: medium-dark skin tone
|
||||
1F468 1F3FF 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👨🏿🦼➡️ E15.1 man in motorized wheelchair facing right: dark skin tone
|
||||
1F468 1F3FF 200D 1F9BC 200D 27A1 ; minimally-qualified # 👨🏿🦼➡ E15.1 man in motorized wheelchair facing right: dark skin tone
|
||||
1F469 200D 1F9BC ; fully-qualified # 👩🦼 E12.0 woman in motorized wheelchair
|
||||
1F469 1F3FB 200D 1F9BC ; fully-qualified # 👩🏻🦼 E12.0 woman in motorized wheelchair: light skin tone
|
||||
1F469 1F3FC 200D 1F9BC ; fully-qualified # 👩🏼🦼 E12.0 woman in motorized wheelchair: medium-light skin tone
|
||||
1F469 1F3FD 200D 1F9BC ; fully-qualified # 👩🏽🦼 E12.0 woman in motorized wheelchair: medium skin tone
|
||||
1F469 1F3FE 200D 1F9BC ; fully-qualified # 👩🏾🦼 E12.0 woman in motorized wheelchair: medium-dark skin tone
|
||||
1F469 1F3FF 200D 1F9BC ; fully-qualified # 👩🏿🦼 E12.0 woman in motorized wheelchair: dark skin tone
|
||||
1F469 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👩🦼➡️ E15.1 woman in motorized wheelchair facing right
|
||||
1F469 200D 1F9BC 200D 27A1 ; minimally-qualified # 👩🦼➡ E15.1 woman in motorized wheelchair facing right
|
||||
1F469 1F3FB 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👩🏻🦼➡️ E15.1 woman in motorized wheelchair facing right: light skin tone
|
||||
1F469 1F3FB 200D 1F9BC 200D 27A1 ; minimally-qualified # 👩🏻🦼➡ E15.1 woman in motorized wheelchair facing right: light skin tone
|
||||
1F469 1F3FC 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👩🏼🦼➡️ E15.1 woman in motorized wheelchair facing right: medium-light skin tone
|
||||
1F469 1F3FC 200D 1F9BC 200D 27A1 ; minimally-qualified # 👩🏼🦼➡ E15.1 woman in motorized wheelchair facing right: medium-light skin tone
|
||||
1F469 1F3FD 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👩🏽🦼➡️ E15.1 woman in motorized wheelchair facing right: medium skin tone
|
||||
1F469 1F3FD 200D 1F9BC 200D 27A1 ; minimally-qualified # 👩🏽🦼➡ E15.1 woman in motorized wheelchair facing right: medium skin tone
|
||||
1F469 1F3FE 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👩🏾🦼➡️ E15.1 woman in motorized wheelchair facing right: medium-dark skin tone
|
||||
1F469 1F3FE 200D 1F9BC 200D 27A1 ; minimally-qualified # 👩🏾🦼➡ E15.1 woman in motorized wheelchair facing right: medium-dark skin tone
|
||||
1F469 1F3FF 200D 1F9BC 200D 27A1 FE0F ; fully-qualified # 👩🏿🦼➡️ E15.1 woman in motorized wheelchair facing right: dark skin tone
|
||||
1F469 1F3FF 200D 1F9BC 200D 27A1 ; minimally-qualified # 👩🏿🦼➡ E15.1 woman in motorized wheelchair facing right: dark skin tone
|
||||
1F9D1 200D 1F9BD ; fully-qualified # 🧑🦽 E12.1 person in manual wheelchair
|
||||
1F9D1 1F3FB 200D 1F9BD ; fully-qualified # 🧑🏻🦽 E12.1 person in manual wheelchair: light skin tone
|
||||
1F9D1 1F3FC 200D 1F9BD ; fully-qualified # 🧑🏼🦽 E12.1 person in manual wheelchair: medium-light skin tone
|
||||
1F9D1 1F3FD 200D 1F9BD ; fully-qualified # 🧑🏽🦽 E12.1 person in manual wheelchair: medium skin tone
|
||||
1F9D1 1F3FE 200D 1F9BD ; fully-qualified # 🧑🏾🦽 E12.1 person in manual wheelchair: medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9BD ; fully-qualified # 🧑🏿🦽 E12.1 person in manual wheelchair: dark skin tone
|
||||
1F9D1 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 🧑🦽➡️ E15.1 person in manual wheelchair facing right
|
||||
1F9D1 200D 1F9BD 200D 27A1 ; minimally-qualified # 🧑🦽➡ E15.1 person in manual wheelchair facing right
|
||||
1F9D1 1F3FB 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 🧑🏻🦽➡️ E15.1 person in manual wheelchair facing right: light skin tone
|
||||
1F9D1 1F3FB 200D 1F9BD 200D 27A1 ; minimally-qualified # 🧑🏻🦽➡ E15.1 person in manual wheelchair facing right: light skin tone
|
||||
1F9D1 1F3FC 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 🧑🏼🦽➡️ E15.1 person in manual wheelchair facing right: medium-light skin tone
|
||||
1F9D1 1F3FC 200D 1F9BD 200D 27A1 ; minimally-qualified # 🧑🏼🦽➡ E15.1 person in manual wheelchair facing right: medium-light skin tone
|
||||
1F9D1 1F3FD 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 🧑🏽🦽➡️ E15.1 person in manual wheelchair facing right: medium skin tone
|
||||
1F9D1 1F3FD 200D 1F9BD 200D 27A1 ; minimally-qualified # 🧑🏽🦽➡ E15.1 person in manual wheelchair facing right: medium skin tone
|
||||
1F9D1 1F3FE 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 🧑🏾🦽➡️ E15.1 person in manual wheelchair facing right: medium-dark skin tone
|
||||
1F9D1 1F3FE 200D 1F9BD 200D 27A1 ; minimally-qualified # 🧑🏾🦽➡ E15.1 person in manual wheelchair facing right: medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 🧑🏿🦽➡️ E15.1 person in manual wheelchair facing right: dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9BD 200D 27A1 ; minimally-qualified # 🧑🏿🦽➡ E15.1 person in manual wheelchair facing right: dark skin tone
|
||||
1F468 200D 1F9BD ; fully-qualified # 👨🦽 E12.0 man in manual wheelchair
|
||||
1F468 1F3FB 200D 1F9BD ; fully-qualified # 👨🏻🦽 E12.0 man in manual wheelchair: light skin tone
|
||||
1F468 1F3FC 200D 1F9BD ; fully-qualified # 👨🏼🦽 E12.0 man in manual wheelchair: medium-light skin tone
|
||||
1F468 1F3FD 200D 1F9BD ; fully-qualified # 👨🏽🦽 E12.0 man in manual wheelchair: medium skin tone
|
||||
1F468 1F3FE 200D 1F9BD ; fully-qualified # 👨🏾🦽 E12.0 man in manual wheelchair: medium-dark skin tone
|
||||
1F468 1F3FF 200D 1F9BD ; fully-qualified # 👨🏿🦽 E12.0 man in manual wheelchair: dark skin tone
|
||||
1F468 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👨🦽➡️ E15.1 man in manual wheelchair facing right
|
||||
1F468 200D 1F9BD 200D 27A1 ; minimally-qualified # 👨🦽➡ E15.1 man in manual wheelchair facing right
|
||||
1F468 1F3FB 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👨🏻🦽➡️ E15.1 man in manual wheelchair facing right: light skin tone
|
||||
1F468 1F3FB 200D 1F9BD 200D 27A1 ; minimally-qualified # 👨🏻🦽➡ E15.1 man in manual wheelchair facing right: light skin tone
|
||||
1F468 1F3FC 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👨🏼🦽➡️ E15.1 man in manual wheelchair facing right: medium-light skin tone
|
||||
1F468 1F3FC 200D 1F9BD 200D 27A1 ; minimally-qualified # 👨🏼🦽➡ E15.1 man in manual wheelchair facing right: medium-light skin tone
|
||||
1F468 1F3FD 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👨🏽🦽➡️ E15.1 man in manual wheelchair facing right: medium skin tone
|
||||
1F468 1F3FD 200D 1F9BD 200D 27A1 ; minimally-qualified # 👨🏽🦽➡ E15.1 man in manual wheelchair facing right: medium skin tone
|
||||
1F468 1F3FE 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👨🏾🦽➡️ E15.1 man in manual wheelchair facing right: medium-dark skin tone
|
||||
1F468 1F3FE 200D 1F9BD 200D 27A1 ; minimally-qualified # 👨🏾🦽➡ E15.1 man in manual wheelchair facing right: medium-dark skin tone
|
||||
1F468 1F3FF 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👨🏿🦽➡️ E15.1 man in manual wheelchair facing right: dark skin tone
|
||||
1F468 1F3FF 200D 1F9BD 200D 27A1 ; minimally-qualified # 👨🏿🦽➡ E15.1 man in manual wheelchair facing right: dark skin tone
|
||||
1F469 200D 1F9BD ; fully-qualified # 👩🦽 E12.0 woman in manual wheelchair
|
||||
1F469 1F3FB 200D 1F9BD ; fully-qualified # 👩🏻🦽 E12.0 woman in manual wheelchair: light skin tone
|
||||
1F469 1F3FC 200D 1F9BD ; fully-qualified # 👩🏼🦽 E12.0 woman in manual wheelchair: medium-light skin tone
|
||||
1F469 1F3FD 200D 1F9BD ; fully-qualified # 👩🏽🦽 E12.0 woman in manual wheelchair: medium skin tone
|
||||
1F469 1F3FE 200D 1F9BD ; fully-qualified # 👩🏾🦽 E12.0 woman in manual wheelchair: medium-dark skin tone
|
||||
1F469 1F3FF 200D 1F9BD ; fully-qualified # 👩🏿🦽 E12.0 woman in manual wheelchair: dark skin tone
|
||||
1F469 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👩🦽➡️ E15.1 woman in manual wheelchair facing right
|
||||
1F469 200D 1F9BD 200D 27A1 ; minimally-qualified # 👩🦽➡ E15.1 woman in manual wheelchair facing right
|
||||
1F469 1F3FB 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👩🏻🦽➡️ E15.1 woman in manual wheelchair facing right: light skin tone
|
||||
1F469 1F3FB 200D 1F9BD 200D 27A1 ; minimally-qualified # 👩🏻🦽➡ E15.1 woman in manual wheelchair facing right: light skin tone
|
||||
1F469 1F3FC 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👩🏼🦽➡️ E15.1 woman in manual wheelchair facing right: medium-light skin tone
|
||||
1F469 1F3FC 200D 1F9BD 200D 27A1 ; minimally-qualified # 👩🏼🦽➡ E15.1 woman in manual wheelchair facing right: medium-light skin tone
|
||||
1F469 1F3FD 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👩🏽🦽➡️ E15.1 woman in manual wheelchair facing right: medium skin tone
|
||||
1F469 1F3FD 200D 1F9BD 200D 27A1 ; minimally-qualified # 👩🏽🦽➡ E15.1 woman in manual wheelchair facing right: medium skin tone
|
||||
1F469 1F3FE 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👩🏾🦽➡️ E15.1 woman in manual wheelchair facing right: medium-dark skin tone
|
||||
1F469 1F3FE 200D 1F9BD 200D 27A1 ; minimally-qualified # 👩🏾🦽➡ E15.1 woman in manual wheelchair facing right: medium-dark skin tone
|
||||
1F469 1F3FF 200D 1F9BD 200D 27A1 FE0F ; fully-qualified # 👩🏿🦽➡️ E15.1 woman in manual wheelchair facing right: dark skin tone
|
||||
1F469 1F3FF 200D 1F9BD 200D 27A1 ; minimally-qualified # 👩🏿🦽➡ E15.1 woman in manual wheelchair facing right: dark skin tone
|
||||
1F3C3 ; fully-qualified # 🏃 E0.6 person running
|
||||
1F3C3 1F3FB ; fully-qualified # 🏃🏻 E1.0 person running: light skin tone
|
||||
1F3C3 1F3FC ; fully-qualified # 🏃🏼 E1.0 person running: medium-light skin tone
|
||||
@@ -2209,6 +2441,66 @@
|
||||
1F3C3 1F3FE 200D 2640 ; minimally-qualified # 🏃🏾♀ E4.0 woman running: medium-dark skin tone
|
||||
1F3C3 1F3FF 200D 2640 FE0F ; fully-qualified # 🏃🏿♀️ E4.0 woman running: dark skin tone
|
||||
1F3C3 1F3FF 200D 2640 ; minimally-qualified # 🏃🏿♀ E4.0 woman running: dark skin tone
|
||||
1F3C3 200D 27A1 FE0F ; fully-qualified # 🏃➡️ E15.1 person running facing right
|
||||
1F3C3 200D 27A1 ; minimally-qualified # 🏃➡ E15.1 person running facing right
|
||||
1F3C3 1F3FB 200D 27A1 FE0F ; fully-qualified # 🏃🏻➡️ E15.1 person running facing right: light skin tone
|
||||
1F3C3 1F3FB 200D 27A1 ; minimally-qualified # 🏃🏻➡ E15.1 person running facing right: light skin tone
|
||||
1F3C3 1F3FC 200D 27A1 FE0F ; fully-qualified # 🏃🏼➡️ E15.1 person running facing right: medium-light skin tone
|
||||
1F3C3 1F3FC 200D 27A1 ; minimally-qualified # 🏃🏼➡ E15.1 person running facing right: medium-light skin tone
|
||||
1F3C3 1F3FD 200D 27A1 FE0F ; fully-qualified # 🏃🏽➡️ E15.1 person running facing right: medium skin tone
|
||||
1F3C3 1F3FD 200D 27A1 ; minimally-qualified # 🏃🏽➡ E15.1 person running facing right: medium skin tone
|
||||
1F3C3 1F3FE 200D 27A1 FE0F ; fully-qualified # 🏃🏾➡️ E15.1 person running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FE 200D 27A1 ; minimally-qualified # 🏃🏾➡ E15.1 person running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FF 200D 27A1 FE0F ; fully-qualified # 🏃🏿➡️ E15.1 person running facing right: dark skin tone
|
||||
1F3C3 1F3FF 200D 27A1 ; minimally-qualified # 🏃🏿➡ E15.1 person running facing right: dark skin tone
|
||||
1F3C3 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃♀️➡️ E15.1 woman running facing right
|
||||
1F3C3 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🏃♀➡️ E15.1 woman running facing right
|
||||
1F3C3 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🏃♀️➡ E15.1 woman running facing right
|
||||
1F3C3 200D 2640 200D 27A1 ; minimally-qualified # 🏃♀➡ E15.1 woman running facing right
|
||||
1F3C3 1F3FB 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏻♀️➡️ E15.1 woman running facing right: light skin tone
|
||||
1F3C3 1F3FB 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🏃🏻♀➡️ E15.1 woman running facing right: light skin tone
|
||||
1F3C3 1F3FB 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🏃🏻♀️➡ E15.1 woman running facing right: light skin tone
|
||||
1F3C3 1F3FB 200D 2640 200D 27A1 ; minimally-qualified # 🏃🏻♀➡ E15.1 woman running facing right: light skin tone
|
||||
1F3C3 1F3FC 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏼♀️➡️ E15.1 woman running facing right: medium-light skin tone
|
||||
1F3C3 1F3FC 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🏃🏼♀➡️ E15.1 woman running facing right: medium-light skin tone
|
||||
1F3C3 1F3FC 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🏃🏼♀️➡ E15.1 woman running facing right: medium-light skin tone
|
||||
1F3C3 1F3FC 200D 2640 200D 27A1 ; minimally-qualified # 🏃🏼♀➡ E15.1 woman running facing right: medium-light skin tone
|
||||
1F3C3 1F3FD 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏽♀️➡️ E15.1 woman running facing right: medium skin tone
|
||||
1F3C3 1F3FD 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🏃🏽♀➡️ E15.1 woman running facing right: medium skin tone
|
||||
1F3C3 1F3FD 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🏃🏽♀️➡ E15.1 woman running facing right: medium skin tone
|
||||
1F3C3 1F3FD 200D 2640 200D 27A1 ; minimally-qualified # 🏃🏽♀➡ E15.1 woman running facing right: medium skin tone
|
||||
1F3C3 1F3FE 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏾♀️➡️ E15.1 woman running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FE 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🏃🏾♀➡️ E15.1 woman running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FE 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🏃🏾♀️➡ E15.1 woman running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FE 200D 2640 200D 27A1 ; minimally-qualified # 🏃🏾♀➡ E15.1 woman running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FF 200D 2640 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏿♀️➡️ E15.1 woman running facing right: dark skin tone
|
||||
1F3C3 1F3FF 200D 2640 200D 27A1 FE0F ; minimally-qualified # 🏃🏿♀➡️ E15.1 woman running facing right: dark skin tone
|
||||
1F3C3 1F3FF 200D 2640 FE0F 200D 27A1 ; minimally-qualified # 🏃🏿♀️➡ E15.1 woman running facing right: dark skin tone
|
||||
1F3C3 1F3FF 200D 2640 200D 27A1 ; minimally-qualified # 🏃🏿♀➡ E15.1 woman running facing right: dark skin tone
|
||||
1F3C3 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃♂️➡️ E15.1 man running facing right
|
||||
1F3C3 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🏃♂➡️ E15.1 man running facing right
|
||||
1F3C3 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🏃♂️➡ E15.1 man running facing right
|
||||
1F3C3 200D 2642 200D 27A1 ; minimally-qualified # 🏃♂➡ E15.1 man running facing right
|
||||
1F3C3 1F3FB 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏻♂️➡️ E15.1 man running facing right: light skin tone
|
||||
1F3C3 1F3FB 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🏃🏻♂➡️ E15.1 man running facing right: light skin tone
|
||||
1F3C3 1F3FB 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🏃🏻♂️➡ E15.1 man running facing right: light skin tone
|
||||
1F3C3 1F3FB 200D 2642 200D 27A1 ; minimally-qualified # 🏃🏻♂➡ E15.1 man running facing right: light skin tone
|
||||
1F3C3 1F3FC 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏼♂️➡️ E15.1 man running facing right: medium-light skin tone
|
||||
1F3C3 1F3FC 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🏃🏼♂➡️ E15.1 man running facing right: medium-light skin tone
|
||||
1F3C3 1F3FC 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🏃🏼♂️➡ E15.1 man running facing right: medium-light skin tone
|
||||
1F3C3 1F3FC 200D 2642 200D 27A1 ; minimally-qualified # 🏃🏼♂➡ E15.1 man running facing right: medium-light skin tone
|
||||
1F3C3 1F3FD 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏽♂️➡️ E15.1 man running facing right: medium skin tone
|
||||
1F3C3 1F3FD 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🏃🏽♂➡️ E15.1 man running facing right: medium skin tone
|
||||
1F3C3 1F3FD 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🏃🏽♂️➡ E15.1 man running facing right: medium skin tone
|
||||
1F3C3 1F3FD 200D 2642 200D 27A1 ; minimally-qualified # 🏃🏽♂➡ E15.1 man running facing right: medium skin tone
|
||||
1F3C3 1F3FE 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏾♂️➡️ E15.1 man running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FE 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🏃🏾♂➡️ E15.1 man running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FE 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🏃🏾♂️➡ E15.1 man running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FE 200D 2642 200D 27A1 ; minimally-qualified # 🏃🏾♂➡ E15.1 man running facing right: medium-dark skin tone
|
||||
1F3C3 1F3FF 200D 2642 FE0F 200D 27A1 FE0F ; fully-qualified # 🏃🏿♂️➡️ E15.1 man running facing right: dark skin tone
|
||||
1F3C3 1F3FF 200D 2642 200D 27A1 FE0F ; minimally-qualified # 🏃🏿♂➡️ E15.1 man running facing right: dark skin tone
|
||||
1F3C3 1F3FF 200D 2642 FE0F 200D 27A1 ; minimally-qualified # 🏃🏿♂️➡ E15.1 man running facing right: dark skin tone
|
||||
1F3C3 1F3FF 200D 2642 200D 27A1 ; minimally-qualified # 🏃🏿♂➡ E15.1 man running facing right: dark skin tone
|
||||
1F483 ; fully-qualified # 💃 E0.6 woman dancing
|
||||
1F483 1F3FB ; fully-qualified # 💃🏻 E1.0 woman dancing: light skin tone
|
||||
1F483 1F3FC ; fully-qualified # 💃🏼 E1.0 woman dancing: medium-light skin tone
|
||||
@@ -3244,7 +3536,6 @@
|
||||
1F469 1F3FF 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏿❤👩🏾 E13.1 couple with heart: woman, woman, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏿❤️👩🏿 E13.1 couple with heart: woman, woman, dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏿❤👩🏿 E13.1 couple with heart: woman, woman, dark skin tone
|
||||
1F46A ; fully-qualified # 👪 E0.6 family
|
||||
1F468 200D 1F469 200D 1F466 ; fully-qualified # 👨👩👦 E2.0 family: man, woman, boy
|
||||
1F468 200D 1F469 200D 1F467 ; fully-qualified # 👨👩👧 E2.0 family: man, woman, girl
|
||||
1F468 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👨👩👧👦 E2.0 family: man, woman, girl, boy
|
||||
@@ -3277,10 +3568,15 @@
|
||||
1F464 ; fully-qualified # 👤 E0.6 bust in silhouette
|
||||
1F465 ; fully-qualified # 👥 E1.0 busts in silhouette
|
||||
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
|
||||
1F46A ; fully-qualified # 👪 E0.6 family
|
||||
1F9D1 200D 1F9D1 200D 1F9D2 ; fully-qualified # 🧑🧑🧒 E15.1 family: adult, adult, child
|
||||
1F9D1 200D 1F9D1 200D 1F9D2 200D 1F9D2 ; fully-qualified # 🧑🧑🧒🧒 E15.1 family: adult, adult, child, child
|
||||
1F9D1 200D 1F9D2 ; fully-qualified # 🧑🧒 E15.1 family: adult, child
|
||||
1F9D1 200D 1F9D2 200D 1F9D2 ; fully-qualified # 🧑🧒🧒 E15.1 family: adult, child, child
|
||||
1F463 ; fully-qualified # 👣 E0.6 footprints
|
||||
|
||||
# People & Body subtotal: 2998
|
||||
# People & Body subtotal: 508 w/o modifiers
|
||||
# People & Body subtotal: 3290
|
||||
# People & Body subtotal: 560 w/o modifiers
|
||||
|
||||
# group: Component
|
||||
|
||||
@@ -3395,6 +3691,7 @@
|
||||
1FABD ; fully-qualified # 🪽 E15.0 wing
|
||||
1F426 200D 2B1B ; fully-qualified # 🐦⬛ E15.0 black bird
|
||||
1FABF ; fully-qualified # 🪿 E15.0 goose
|
||||
1F426 200D 1F525 ; fully-qualified # 🐦🔥 E15.1 phoenix
|
||||
|
||||
# subgroup: animal-amphibian
|
||||
1F438 ; fully-qualified # 🐸 E0.6 frog
|
||||
@@ -3477,8 +3774,8 @@
|
||||
1FABA ; fully-qualified # 🪺 E14.0 nest with eggs
|
||||
1F344 ; fully-qualified # 🍄 E0.6 mushroom
|
||||
|
||||
# Animals & Nature subtotal: 159
|
||||
# Animals & Nature subtotal: 159 w/o modifiers
|
||||
# Animals & Nature subtotal: 160
|
||||
# Animals & Nature subtotal: 160 w/o modifiers
|
||||
|
||||
# group: Food & Drink
|
||||
|
||||
@@ -3488,6 +3785,7 @@
|
||||
1F349 ; fully-qualified # 🍉 E0.6 watermelon
|
||||
1F34A ; fully-qualified # 🍊 E0.6 tangerine
|
||||
1F34B ; fully-qualified # 🍋 E1.0 lemon
|
||||
1F34B 200D 1F7E9 ; fully-qualified # 🍋🟩 E15.1 lime
|
||||
1F34C ; fully-qualified # 🍌 E0.6 banana
|
||||
1F34D ; fully-qualified # 🍍 E0.6 pineapple
|
||||
1F96D ; fully-qualified # 🥭 E11.0 mango
|
||||
@@ -3522,6 +3820,7 @@
|
||||
1F330 ; fully-qualified # 🌰 E0.6 chestnut
|
||||
1FADA ; fully-qualified # 🫚 E15.0 ginger root
|
||||
1FADB ; fully-qualified # 🫛 E15.0 pea pod
|
||||
1F344 200D 1F7EB ; fully-qualified # 🍄🟫 E15.1 brown mushroom
|
||||
|
||||
# subgroup: food-prepared
|
||||
1F35E ; fully-qualified # 🍞 E0.6 bread
|
||||
@@ -3633,8 +3932,8 @@
|
||||
1FAD9 ; fully-qualified # 🫙 E14.0 jar
|
||||
1F3FA ; fully-qualified # 🏺 E1.0 amphora
|
||||
|
||||
# Food & Drink subtotal: 135
|
||||
# Food & Drink subtotal: 135 w/o modifiers
|
||||
# Food & Drink subtotal: 137
|
||||
# Food & Drink subtotal: 137 w/o modifiers
|
||||
|
||||
# group: Travel & Places
|
||||
|
||||
@@ -4321,6 +4620,8 @@
|
||||
2696 ; unqualified # ⚖ E1.0 balance scale
|
||||
1F9AF ; fully-qualified # 🦯 E12.0 white cane
|
||||
1F517 ; fully-qualified # 🔗 E0.6 link
|
||||
26D3 FE0F 200D 1F4A5 ; fully-qualified # ⛓️💥 E15.1 broken chain
|
||||
26D3 200D 1F4A5 ; unqualified # ⛓💥 E15.1 broken chain
|
||||
26D3 FE0F ; fully-qualified # ⛓️ E0.7 chains
|
||||
26D3 ; unqualified # ⛓ E0.7 chains
|
||||
1FA9D ; fully-qualified # 🪝 E13.0 hook
|
||||
@@ -4389,8 +4690,8 @@
|
||||
1FAA7 ; fully-qualified # 🪧 E13.0 placard
|
||||
1FAAA ; fully-qualified # 🪪 E14.0 identification card
|
||||
|
||||
# Objects subtotal: 310
|
||||
# Objects subtotal: 310 w/o modifiers
|
||||
# Objects subtotal: 312
|
||||
# Objects subtotal: 312 w/o modifiers
|
||||
|
||||
# group: Symbols
|
||||
|
||||
@@ -4979,7 +5280,7 @@
|
||||
1F1F9 1F1F2 ; fully-qualified # 🇹🇲 E2.0 flag: Turkmenistan
|
||||
1F1F9 1F1F3 ; fully-qualified # 🇹🇳 E2.0 flag: Tunisia
|
||||
1F1F9 1F1F4 ; fully-qualified # 🇹🇴 E2.0 flag: Tonga
|
||||
1F1F9 1F1F7 ; fully-qualified # 🇹🇷 E2.0 flag: Turkey
|
||||
1F1F9 1F1F7 ; fully-qualified # 🇹🇷 E2.0 flag: Türkiye
|
||||
1F1F9 1F1F9 ; fully-qualified # 🇹🇹 E2.0 flag: Trinidad & Tobago
|
||||
1F1F9 1F1FB ; fully-qualified # 🇹🇻 E2.0 flag: Tuvalu
|
||||
1F1F9 1F1FC ; fully-qualified # 🇹🇼 E2.0 flag: Taiwan
|
||||
@@ -5016,9 +5317,9 @@
|
||||
# Flags subtotal: 275 w/o modifiers
|
||||
|
||||
# Status Counts
|
||||
# fully-qualified : 3655
|
||||
# minimally-qualified : 827
|
||||
# unqualified : 242
|
||||
# fully-qualified : 3773
|
||||
# minimally-qualified : 1009
|
||||
# unqualified : 243
|
||||
# component : 9
|
||||
|
||||
#EOF
|
||||
|
||||
@@ -134,7 +134,7 @@ style_properties = sorted_dict(
|
||||
alt=None,
|
||||
altruby_style=None,
|
||||
antialias=None,
|
||||
vertical=None,
|
||||
axis=None,
|
||||
background='renpy.easy.displayable_or_none',
|
||||
bar_invert=None,
|
||||
bar_resizing=None,
|
||||
@@ -169,6 +169,7 @@ style_properties = sorted_dict(
|
||||
hover_sound=None,
|
||||
hyperlink_functions=None,
|
||||
italic=None,
|
||||
instance=None,
|
||||
justify=None,
|
||||
kerning=None,
|
||||
key_events=None,
|
||||
@@ -212,6 +213,7 @@ style_properties = sorted_dict(
|
||||
top_margin=None,
|
||||
top_padding=None,
|
||||
underline=None,
|
||||
vertical=None,
|
||||
xanchor='expand_anchor',
|
||||
xfill=None,
|
||||
xfit=None,
|
||||
|
||||
@@ -397,6 +397,12 @@ cdef extern from "pyfreetype.h":
|
||||
FT_LOAD_NO_AUTOHINT
|
||||
FT_LOAD_COLOR
|
||||
|
||||
FT_LOAD_TARGET_NORMAL
|
||||
FT_LOAD_TARGET_LIGHT
|
||||
FT_LOAD_TARGET_MONO
|
||||
FT_LOAD_TARGET_LCD
|
||||
FT_LOAD_TARGET_LCD_V
|
||||
|
||||
FT_Error FT_Load_Glyph(FT_Face face, FT_UInt glyph_index, FT_Int32 flags)
|
||||
FT_Error FT_Load_Char(FT_Face face, FT_ULong char_code, FT_Int32 flags)
|
||||
|
||||
@@ -517,7 +523,7 @@ cdef extern from "pyfreetype.h":
|
||||
FT_Error FT_Bitmap_Embolden(FT_Library lib, FT_Bitmap *bitmap, FT_Pos xStrength, FT_Pos yStrength)
|
||||
FT_Error FT_Bitmap_Convert(FT_Library lib, FT_Bitmap *source, FT_Bitmap *target, FT_Int alignment)
|
||||
|
||||
# Additions by Tom Rothame.
|
||||
# Additions by Tom Rothamel.
|
||||
|
||||
cdef struct FT_MemoryRec_
|
||||
ctypedef FT_MemoryRec_ *FT_Memory
|
||||
@@ -607,3 +613,42 @@ cdef extern from "pyfreetype.h":
|
||||
cdef FT_Long FT_CEIL(FT_Long)
|
||||
cdef FT_Long FT_FLOOR(FT_Long)
|
||||
cdef FT_Long FT_ROUND(FT_Long)
|
||||
|
||||
# Multiple Masters
|
||||
|
||||
cdef struct FT_Var_Axis_:
|
||||
FT_String* name
|
||||
|
||||
FT_Fixed minimum
|
||||
FT_Fixed default "def"
|
||||
FT_Fixed maximum
|
||||
|
||||
FT_ULong tag
|
||||
FT_UInt strid
|
||||
|
||||
ctypedef FT_Var_Axis_ FT_Var_Axis
|
||||
|
||||
cdef struct FT_Var_Named_Style_:
|
||||
FT_Fixed* coords
|
||||
FT_UInt strid
|
||||
FT_UInt psid
|
||||
|
||||
ctypedef FT_Var_Named_Style_ FT_Var_Named_Style
|
||||
|
||||
cdef struct FT_MM_Var_:
|
||||
FT_UInt num_axis
|
||||
FT_UInt num_designs
|
||||
FT_UInt num_namedstyles
|
||||
FT_Var_Axis* axis
|
||||
FT_Var_Named_Style* namedstyle
|
||||
|
||||
ctypedef FT_MM_Var_ FT_MM_Var
|
||||
|
||||
FT_Error FT_Get_MM_Var(FT_Face face, FT_MM_Var **amaster)
|
||||
FT_Error FT_Done_MM_Var(FT_Library library, FT_MM_Var *amaster)
|
||||
|
||||
FT_Error FT_Set_Named_Instance(FT_Face, FT_UInt instance_index)
|
||||
|
||||
FT_Error FT_Set_Var_Design_Coordinates( FT_Face face,
|
||||
FT_UInt num_coords,
|
||||
FT_Fixed* coords )
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include FT_OUTLINE_H
|
||||
#include FT_BITMAP_H
|
||||
#include FT_STROKER_H
|
||||
#include FT_MULTIPLE_MASTERS_H
|
||||
|
||||
#define FT_FLOOR(X) ((X & -64) >> 6)
|
||||
#define FT_CEIL(X) (((X + 63) & -64) >> 6)
|
||||
|
||||
+3
-7
@@ -278,9 +278,6 @@ class Backup(_object):
|
||||
# A map from module to the set of names in that module.
|
||||
self.names = { }
|
||||
|
||||
if mobile:
|
||||
return
|
||||
|
||||
for m in sys.modules.values():
|
||||
if m is None:
|
||||
continue
|
||||
@@ -571,8 +568,7 @@ def import_all():
|
||||
|
||||
global backup
|
||||
|
||||
if not mobile:
|
||||
backup = Backup()
|
||||
backup = Backup()
|
||||
|
||||
post_import()
|
||||
|
||||
@@ -618,8 +614,8 @@ def reload_all():
|
||||
returned.
|
||||
"""
|
||||
|
||||
if mobile:
|
||||
raise Exception("Reloading is not supported on mobile platforms.")
|
||||
# if mobile:
|
||||
# raise Exception("Reloading is not supported on mobile platforms.")
|
||||
|
||||
import renpy
|
||||
|
||||
|
||||
+79
-1
@@ -121,11 +121,82 @@ def mac_start(fn):
|
||||
|
||||
def popen_del(self, *args, **kwargs):
|
||||
"""
|
||||
Fix an issue where the __del__ method of popen doesn't wor,
|
||||
Fix an issue where the __del__ method of popen doesn't work.
|
||||
"""
|
||||
|
||||
return
|
||||
|
||||
def get_alternate_base(basedir, always=False):
|
||||
"""
|
||||
:undocumented:
|
||||
|
||||
Tries to find an alternate base directory. This exists in a writable
|
||||
location, and is intended for use by a game that downloads its assets
|
||||
to the device (generally for ios or android, where the assets may be
|
||||
too big for the app store).
|
||||
"""
|
||||
|
||||
# Determine the alternate base directory location.
|
||||
|
||||
if renpy.android:
|
||||
altbase = os.path.join(os.environ["ANDROID_PRIVATE"], "base")
|
||||
|
||||
elif renpy.ios:
|
||||
from pyobjus import autoclass # type: ignore
|
||||
from pyobjus.objc_py_types import enum # type: ignore
|
||||
|
||||
NSSearchPathDirectory = enum("NSSearchPathDirectory", NSApplicationSupportDirectory=14)
|
||||
NSSearchPathDomainMask = enum("NSSearchPathDomainMask", NSUserDomainMask=1)
|
||||
|
||||
NSFileManager = autoclass('NSFileManager')
|
||||
manager = NSFileManager.defaultManager()
|
||||
url = manager.URLsForDirectory_inDomains_(
|
||||
NSSearchPathDirectory.NSApplicationSupportDirectory,
|
||||
NSSearchPathDomainMask.NSUserDomainMask,
|
||||
).lastObject()
|
||||
|
||||
# url.path seems to change type based on iOS version, for some reason.
|
||||
try:
|
||||
altbase = url.path().UTF8String()
|
||||
except Exception:
|
||||
altbase = url.path.UTF8String()
|
||||
|
||||
if isinstance(altbase, bytes):
|
||||
altbase = altbase.decode("utf-8")
|
||||
|
||||
else:
|
||||
altbase = os.path.join(basedir, "base")
|
||||
|
||||
if always:
|
||||
return altbase
|
||||
|
||||
# Check to see if there's a game in there created with the
|
||||
# current version of Ren'Py.
|
||||
|
||||
def ver(s):
|
||||
"""
|
||||
Returns the first three components of a version string.
|
||||
"""
|
||||
|
||||
return tuple(int(i) for i in s.split(".")[:3])
|
||||
|
||||
import json
|
||||
|
||||
version_json = os.path.join(altbase, "update", "version.json")
|
||||
|
||||
if not os.path.exists(version_json):
|
||||
return basedir
|
||||
|
||||
with open(version_json, "r") as f:
|
||||
modules = json.load(f)
|
||||
|
||||
for v in modules.values():
|
||||
if ver(v["renpy_version"]) != ver(renpy.version_only):
|
||||
return basedir
|
||||
|
||||
return altbase
|
||||
|
||||
|
||||
def bootstrap(renpy_base):
|
||||
|
||||
global renpy
|
||||
@@ -189,6 +260,13 @@ def bootstrap(renpy_base):
|
||||
sys.stderr.write("Base directory %r does not exist. Giving up.\n" % (basedir,))
|
||||
sys.exit(1)
|
||||
|
||||
# Potentially use an alternate base directory.
|
||||
try:
|
||||
basedir = get_alternate_base(basedir)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Make game/ on Android.
|
||||
if renpy.android:
|
||||
if not os.path.exists(basedir + "/game"):
|
||||
|
||||
@@ -492,6 +492,9 @@ def display_say(
|
||||
|
||||
# Clears out transients.
|
||||
renpy.exports.with_statement(None)
|
||||
|
||||
renpy.exports.checkpoint(True, hard=checkpoint)
|
||||
|
||||
return
|
||||
|
||||
# If we're not interacting, call the noniteractive_callbacks.
|
||||
|
||||
+222
-274
@@ -21,10 +21,7 @@
|
||||
|
||||
init -1600 python:
|
||||
|
||||
##########################################################################
|
||||
# Functions that set variables or fields.
|
||||
|
||||
__FieldNotFound = object()
|
||||
__Sentinel = object()
|
||||
|
||||
def _get_field(obj, name, kind):
|
||||
|
||||
@@ -34,372 +31,323 @@ init -1600 python:
|
||||
rv = obj
|
||||
|
||||
for i in name.split("."):
|
||||
rv = getattr(rv, i, __FieldNotFound)
|
||||
if rv is __FieldNotFound:
|
||||
raise NameError("The {} {} does not exist.".format(kind, name))
|
||||
rv = getattr(rv, i, __Sentinel)
|
||||
if rv is __Sentinel:
|
||||
raise Exception("The {!r} {} does not exist.".format(name, kind))
|
||||
|
||||
return rv
|
||||
|
||||
def _set_field(obj, name, value, kind):
|
||||
fields, _, attr = name.rpartition(".")
|
||||
obj = _get_field(obj, fields, kind)
|
||||
|
||||
try:
|
||||
obj = _get_field(obj, fields, kind)
|
||||
setattr(obj, attr, value)
|
||||
except Exception:
|
||||
raise NameError("The {} {} does not exist.".format(kind, name))
|
||||
raise Exception("The {!r} {} cannot be set.".format(name, kind))
|
||||
|
||||
@renpy.pure
|
||||
class SetField(Action, FieldEquality):
|
||||
|
||||
init -1600 python hide:
|
||||
|
||||
Accessor = Manager = python_object
|
||||
# clarify the role of each class without adding to the mro
|
||||
# and without having ancestors placed between RevertableObject and python_object in the mro
|
||||
|
||||
# Accessor mixins : manage how the data is accessed
|
||||
# defines a __call__ which gets the value to set from the value_to_set() method
|
||||
# defines a current_value() method which returns the current value of the accessed data or raise an exception
|
||||
# may define the `kind` class attribute, which is used in error messages
|
||||
class Field(Accessor):
|
||||
"""
|
||||
:doc: data_action
|
||||
:args: (object, field, value)
|
||||
|
||||
Causes the a field on an object to be set to a given value.
|
||||
`object` is the object, `field` is a string giving the name of the
|
||||
field to set, and `value` is the value to set it to.
|
||||
An Accessor mixin class for Actions setting a field on an object.
|
||||
"""
|
||||
|
||||
identity_fields = [ "object" ]
|
||||
equality_fields = [ "field", "value" ]
|
||||
|
||||
identity_fields = ("object",)
|
||||
equality_fields = ("field",)
|
||||
kind = "field"
|
||||
|
||||
def __init__(self, object, field, value, kind="field"):
|
||||
def __init__(self, object, field, *args, **kwargs):
|
||||
super(Field, self).__init__(*args, **kwargs)
|
||||
self.object = object
|
||||
self.field = field
|
||||
self.value = value
|
||||
self.kind = kind
|
||||
|
||||
def __call__(self):
|
||||
_set_field(self.object, self.field, self.value, self.kind)
|
||||
_set_field(self.object, self.field, self.value_to_set(), self.kind)
|
||||
renpy.restart_interaction()
|
||||
|
||||
def get_selected(self):
|
||||
return _get_field(self.object, self.field, self.kind) == self.value
|
||||
def current_value(self):
|
||||
return _get_field(self.object, self.field, self.kind)
|
||||
|
||||
@renpy.pure
|
||||
def SetVariable(name, value):
|
||||
class Variable(Field):
|
||||
"""
|
||||
:doc: data_action
|
||||
|
||||
Causes the variable called `name` to be set to `value`.
|
||||
|
||||
The `name` argument must be a string, and can be a simple name like "strength", or
|
||||
one with dots separating the variable from fields, like "hero.strength"
|
||||
or "persistent.show_cutscenes".
|
||||
An Accessor mixin class for Actions setting a global variable.
|
||||
"""
|
||||
|
||||
return SetField(store, name, value, kind="variable")
|
||||
kind = "global variable"
|
||||
|
||||
@renpy.pure
|
||||
class SetDict(Action, FieldEquality):
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
super(Variable, self).__init__(store, name, *args, **kwargs)
|
||||
|
||||
class Dict(Accessor):
|
||||
"""
|
||||
:doc: data_action
|
||||
|
||||
Causes the value of `key` in `dict` to be set to `value`.
|
||||
This also works with lists, where `key` is the index at which
|
||||
the value will be set.
|
||||
An Accessor mixin class for Actions setting an index on a sequence or a key on a mapping.
|
||||
"""
|
||||
|
||||
identity_fields = [ "dict" ]
|
||||
equality_fields = [ "key", "value" ]
|
||||
identity_fields = ("dict",)
|
||||
equality_fields = ("key",)
|
||||
kind = "key or index"
|
||||
|
||||
def __init__(self, dict, key, value):
|
||||
def __init__(self, dict, key, *args, **kwargs):
|
||||
super(Dict, self).__init__(*args, **kwargs)
|
||||
self.dict = dict
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
def __call__(self):
|
||||
self.dict[self.key] = self.value
|
||||
self.dict[self.key] = self.value_to_set()
|
||||
renpy.restart_interaction()
|
||||
|
||||
def get_selected(self):
|
||||
if self.key not in self.dict:
|
||||
return False
|
||||
def current_value(self):
|
||||
key = self.key
|
||||
try:
|
||||
return self.dict[self.key]
|
||||
except LookupError as e:
|
||||
raise Exception("The {!r} {} does not exist.".format(key, self.kind)) # from e # PY3 only
|
||||
|
||||
return self.dict[self.key] == self.value
|
||||
|
||||
|
||||
@renpy.pure
|
||||
class SetScreenVariable(Action, FieldEquality):
|
||||
class ScreenVariable(Accessor):
|
||||
"""
|
||||
:doc: data_action
|
||||
|
||||
Causes the variable called `name` associated with the current screen
|
||||
to be set to `value`.
|
||||
In a ``use``\ d screen, this action sets the variable in the context
|
||||
of the screen containing the ``use``\ d one(s).
|
||||
To set variables within a ``use``\ d screen, and only in that
|
||||
case, use :func:`SetLocalVariable` instead.
|
||||
An Accessor mixin class for Actions setting a screen variable.
|
||||
"""
|
||||
|
||||
identity_fields = [ "value" ]
|
||||
equality_fields = [ "name" ]
|
||||
identity_fields = ()
|
||||
equality_fields = ("name",)
|
||||
kind = "screen variable"
|
||||
|
||||
def __init__(self, name, value):
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
super(ScreenVariable, self).__init__(*args, **kwargs)
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def __call__(self):
|
||||
|
||||
cs = renpy.current_screen()
|
||||
|
||||
if cs is None:
|
||||
return
|
||||
|
||||
cs.scope[self.name] = self.value
|
||||
cs.scope[self.name] = self.value_to_set()
|
||||
renpy.restart_interaction()
|
||||
|
||||
def get_selected(self):
|
||||
|
||||
def current_value(self):
|
||||
cs = renpy.current_screen()
|
||||
|
||||
if cs is None:
|
||||
return False
|
||||
raise Exception("No current screen.")
|
||||
|
||||
if self.name not in cs.scope:
|
||||
return False
|
||||
|
||||
return cs.scope[self.name] == self.value
|
||||
|
||||
# Not pure.
|
||||
def SetLocalVariable(name, value):
|
||||
"""
|
||||
:doc: data_action
|
||||
|
||||
Causes the variable called `name` to be set to `value` in the current
|
||||
local context.
|
||||
|
||||
This function is only useful in a screen that has been ``use``\ d by
|
||||
another screen, as it provides a way of setting the value of a
|
||||
variable inside the used screen. In all other cases,
|
||||
:func:`SetScreenVariable` should be preferred, as it allows more
|
||||
of the screen to be cached.
|
||||
|
||||
For more information, see :ref:`sl-use`.
|
||||
|
||||
This must be created in the context that the variable is set
|
||||
in - it can't be passed in from somewhere else.
|
||||
"""
|
||||
|
||||
return SetDict(sys._getframe(1).f_locals, name, value)
|
||||
|
||||
|
||||
@renpy.pure
|
||||
class ToggleField(Action, FieldEquality):
|
||||
"""
|
||||
:doc: data_action
|
||||
:args: (object, field, true_value=None, false_value=None)
|
||||
|
||||
Toggles `field` on `object`. Toggling means to invert the boolean
|
||||
value of that field when the action is performed.
|
||||
|
||||
`true_value`
|
||||
If not None, then this is the true value we use.
|
||||
`false_value`
|
||||
If not None, then this is the false value we use.
|
||||
"""
|
||||
|
||||
identity_fields = [ "object"]
|
||||
equality_fields = [ "field", "true_value", "false_value" ]
|
||||
|
||||
kind = "field"
|
||||
|
||||
def __init__(self, object, field, true_value=None, false_value=None, kind="field"):
|
||||
self.object = object
|
||||
self.field = field
|
||||
self.true_value = true_value
|
||||
self.false_value = false_value
|
||||
self.kind = kind
|
||||
|
||||
def __call__(self):
|
||||
value = _get_field(self.object, self.field, self.kind)
|
||||
|
||||
if self.true_value is not None:
|
||||
value = (value == self.true_value)
|
||||
|
||||
value = not value
|
||||
|
||||
if self.true_value is not None:
|
||||
if value:
|
||||
value = self.true_value
|
||||
else:
|
||||
value = self.false_value
|
||||
|
||||
_set_field(self.object, self.field, value, self.kind)
|
||||
renpy.restart_interaction()
|
||||
|
||||
def get_selected(self):
|
||||
rv = _get_field(self.object, self.field, self.kind)
|
||||
|
||||
if self.true_value is not None:
|
||||
rv = (rv == self.true_value)
|
||||
rv = cs.scope.get(self.name, __Sentinel)
|
||||
if rv is __Sentinel:
|
||||
raise Exception("The {!r} {} does not exist.".format(self.name, self.kind))
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
@renpy.pure
|
||||
def ToggleVariable(variable, true_value=None, false_value=None):
|
||||
class LocalVariable(Dict):
|
||||
"""
|
||||
:doc: data_action
|
||||
|
||||
Toggles the variable whose name is given in `variable`.
|
||||
|
||||
The `variable` argument must be a string, and can be a simple name like "strength", or
|
||||
one with dots separating the variable from fields, like "hero.strength"
|
||||
or "persistent.show_cutscenes".
|
||||
|
||||
|
||||
`true_value`
|
||||
If not None, then this is the true value used.
|
||||
`false_value`
|
||||
If not None, then this is the false value used.
|
||||
An Accessor mixin class for Actions setting a local variable.
|
||||
"""
|
||||
|
||||
return ToggleField(store, variable, true_value=true_value, false_value=false_value, kind="variable")
|
||||
kind = "local variable"
|
||||
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
super(LocalVariable, self).__init__(sys._getframe(1).f_locals,
|
||||
name,
|
||||
*args, **kwargs)
|
||||
|
||||
@renpy.pure
|
||||
class ToggleDict(Action, FieldEquality):
|
||||
# Manager mixins : manage what value gets written
|
||||
# defines a value_to_set() method which returns the value to be set
|
||||
# may define get_selected and such methods
|
||||
# both of these may call the current_value() method
|
||||
class Set(Manager):
|
||||
"""
|
||||
:doc: data_action
|
||||
|
||||
Toggles the value of `key` in `dict`. It also works on
|
||||
lists, in which case `key` is the index of the value to toggle.
|
||||
Toggling means to invert the value when the action is performed.
|
||||
|
||||
`true_value`
|
||||
If not None, then this is the true value used.
|
||||
`false_value`
|
||||
If not None, then this is the false value used.
|
||||
A Manager mixin class for Actions setting a single value
|
||||
"""
|
||||
|
||||
identity_fields = [ "dict", ]
|
||||
equality_fields = [ "key", "true_value", "false_value" ]
|
||||
identity_fields = ()
|
||||
equality_fields = ("value",)
|
||||
|
||||
def __init__(self, dict, key, true_value=None, false_value=None):
|
||||
self.dict = dict
|
||||
self.key = key
|
||||
self.true_value = true_value
|
||||
self.false_value = false_value
|
||||
def __init__(self, value, *args, **kwargs):
|
||||
super(Set, self).__init__(*args, **kwargs)
|
||||
self.value = value
|
||||
|
||||
def __call__(self):
|
||||
value = self.dict[self.key]
|
||||
|
||||
if self.true_value is not None:
|
||||
value = (value == self.true_value)
|
||||
|
||||
value = not value
|
||||
|
||||
if self.true_value is not None:
|
||||
if value:
|
||||
value = self.true_value
|
||||
else:
|
||||
value = self.false_value
|
||||
|
||||
self.dict[self.key] = value
|
||||
renpy.restart_interaction()
|
||||
def value_to_set(self):
|
||||
return self.value
|
||||
|
||||
def get_selected(self):
|
||||
try:
|
||||
rv = self.dict[self.key]
|
||||
except (KeyError, IndexError):
|
||||
val = self.current_value()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if self.true_value is not None:
|
||||
rv = (rv == self.true_value)
|
||||
return val == self.value
|
||||
|
||||
return rv
|
||||
|
||||
# Not pure.
|
||||
def ToggleLocalVariable(name, true_value=None, false_value=None):
|
||||
class Toggle(Manager):
|
||||
"""
|
||||
:doc: data_action
|
||||
|
||||
Toggles the value of the variable called `name` in the current local context.
|
||||
|
||||
This function is only useful in a screen that has been ``use``\ d by
|
||||
another screen, as it provides a way of setting the value of a
|
||||
variable inside the used screen. In all other cases,
|
||||
:func:`ToggleScreenVariable` should be preferred, as it allows more
|
||||
of the screen to be cached.
|
||||
|
||||
For more information, see :ref:`sl-use`.
|
||||
|
||||
This must be created in the context that the variable is set
|
||||
in - it can't be passed in from somewhere else.
|
||||
|
||||
`true_value`
|
||||
If not None, then this is the true value used.
|
||||
`false_value`
|
||||
If not None, then this is the false value used.
|
||||
A Manager mixin class for Actions toggling between two values.
|
||||
"""
|
||||
|
||||
return ToggleDict(sys._getframe(1).f_locals, name, true_value=true_value, false_value=false_value)
|
||||
identity_fields = ()
|
||||
equality_fields = ("true_value", "false_value")
|
||||
|
||||
@renpy.pure
|
||||
class ToggleScreenVariable(Action, FieldEquality):
|
||||
"""
|
||||
:doc: data_action
|
||||
|
||||
Toggles the value of the variable called `name` in the current screen.
|
||||
In a ``use``\ d screen, this action accesses and sets the given variable
|
||||
in the context of the screen containing the ``use``\ d one(s).
|
||||
To access and set variables within a ``use``\ d screen, and only in that
|
||||
case, use :func:`ToggleLocalVariable` instead.
|
||||
|
||||
`true_value`
|
||||
If not None, then this is the true value used.
|
||||
`false_value`
|
||||
If not None, then this is the false value used.
|
||||
"""
|
||||
|
||||
equality_fields = [ "name", "true_value", "false_value" ]
|
||||
|
||||
def __init__(self, name, true_value=None, false_value=None):
|
||||
self.name = name
|
||||
def __init__(self, true_value=None, false_value=None, *args, **kwargs):
|
||||
super(Toggle, self).__init__(*args, **kwargs)
|
||||
self.true_value = true_value
|
||||
self.false_value = false_value
|
||||
|
||||
def __call__(self):
|
||||
cs = renpy.current_screen()
|
||||
def value_to_set(self):
|
||||
value = self.current_value()
|
||||
|
||||
if cs is None:
|
||||
return
|
||||
|
||||
value = cs.scope[self.name]
|
||||
|
||||
if self.true_value is not None:
|
||||
value = (value == self.true_value)
|
||||
tv = self.true_value
|
||||
if tv is not None:
|
||||
value = (value == tv)
|
||||
|
||||
value = not value
|
||||
|
||||
if self.true_value is not None:
|
||||
if tv is not None:
|
||||
if value:
|
||||
value = self.true_value
|
||||
return tv
|
||||
else:
|
||||
value = self.false_value
|
||||
return self.false_value
|
||||
|
||||
cs.scope[self.name] = value
|
||||
renpy.restart_interaction()
|
||||
return value
|
||||
|
||||
def get_selected(self):
|
||||
cs = renpy.current_screen()
|
||||
|
||||
if cs is None:
|
||||
try:
|
||||
val = self.current_value()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if self.name not in cs.scope:
|
||||
tv = self.true_value
|
||||
if tv is not None:
|
||||
return val == tv
|
||||
|
||||
return bool(val)
|
||||
|
||||
class Cycle(Manager):
|
||||
"""
|
||||
A Manager mixin class for Actions cycling through a list of values.
|
||||
|
||||
Warning : the provided values must not be duplicated, otherwise some values can never be set.
|
||||
"""
|
||||
|
||||
identity_fields = ()
|
||||
equality_fields = ("values", "loop")
|
||||
|
||||
def __init__(self, values, *args, **kwargs):
|
||||
if kwargs.pop("reverse", False):
|
||||
values = values[::-1]
|
||||
# the reversed builtin returns a non-indexable iterator
|
||||
# and we only support sequences anyway - iterators are not picklable
|
||||
self.loop = kwargs.pop("loop", True)
|
||||
|
||||
super(Cycle, self).__init__(*args, **kwargs)
|
||||
|
||||
self.values = renpy.easy.to_tuple(values)
|
||||
|
||||
def value_to_set(self):
|
||||
values = self.values
|
||||
value = self.current_value()
|
||||
|
||||
if value in values:
|
||||
idx = values.index(value) + 1
|
||||
|
||||
lnv = len(values)
|
||||
if self.loop and idx >= lnv:
|
||||
idx -= lnv
|
||||
|
||||
else:
|
||||
idx = 0
|
||||
|
||||
return values[idx]
|
||||
|
||||
def get_sensitive(self):
|
||||
values = self.values
|
||||
if not values:
|
||||
return False
|
||||
|
||||
rv = cs.scope[self.name]
|
||||
if self.loop:
|
||||
return True
|
||||
|
||||
if self.true_value is not None:
|
||||
rv = (rv == self.true_value)
|
||||
try:
|
||||
value = self.current_value()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if value in values:
|
||||
idx = values.index(value)+1
|
||||
if idx >= len(values):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
class Increment(Manager):
|
||||
"""
|
||||
A Manager mixin class for Actions incrementing a value by a set amount.
|
||||
"""
|
||||
|
||||
identity_fields = ()
|
||||
equality_fields = ("amount",)
|
||||
|
||||
def __init__(self, amount=1, *args, **kwargs):
|
||||
super(Increment, self).__init__(*args, **kwargs)
|
||||
self.amount = amount
|
||||
|
||||
def value_to_set(self):
|
||||
return self.current_value() + self.amount
|
||||
|
||||
def get_sensitive(self):
|
||||
try:
|
||||
value = self.current_value()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
try:
|
||||
value + self.amount
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
return rv
|
||||
for accessor in (Field, Variable, Dict, ScreenVariable, LocalVariable):
|
||||
for manager in (Set, Toggle, Cycle, Increment):
|
||||
name = manager.__name__ + accessor.__name__
|
||||
doc = ":doc: generated_data_action"
|
||||
|
||||
if config.generating_documentation:
|
||||
from inspect import signature, Signature, Parameter
|
||||
|
||||
params = []
|
||||
for bas in (accessor, manager):
|
||||
sig = signature(bas.__init__)
|
||||
for k, param in enumerate(sig.parameters.values()):
|
||||
if k and (param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)):
|
||||
params.append(param)
|
||||
|
||||
if manager is Cycle:
|
||||
params.append(Parameter("reverse", Parameter.KEYWORD_ONLY, default=False))
|
||||
params.append(Parameter("loop", Parameter.KEYWORD_ONLY, default=True))
|
||||
|
||||
doc += "\n:args: " + str(Signature(parameters=params)) + "\n\nSee :ref:`data-actions`."
|
||||
|
||||
clsdict = python_dict(
|
||||
identity_fields=manager.identity_fields+accessor.identity_fields,
|
||||
equality_fields=manager.equality_fields+accessor.equality_fields,
|
||||
__doc__=doc,
|
||||
)
|
||||
|
||||
cls = type(name, (accessor, manager, Action, FieldEquality), clsdict)
|
||||
|
||||
if accessor is not LocalVariable:
|
||||
renpy.pure(name)
|
||||
setattr(store, name, cls)
|
||||
|
||||
|
||||
init -1600 python:
|
||||
|
||||
@renpy.pure
|
||||
class AddToSet(Action, FieldEquality):
|
||||
|
||||
@@ -36,13 +36,15 @@ init -1500 python:
|
||||
class ShowMenu(Action, DictEquality):
|
||||
"""
|
||||
:doc: menu_action
|
||||
:args: (screen=_game_menu_screen, *args, _transition=config.intra_transition, **kwargs)
|
||||
|
||||
Causes us to enter the game menu, if we're not there already. If we
|
||||
are in the game menu, then this shows a screen or jumps to a label.
|
||||
|
||||
`screen` is usually the name of a screen, which is shown using
|
||||
the screen mechanism. If the screen doesn't exist, then "_screen"
|
||||
is appended to it, and that label is jumped to.
|
||||
the screen mechanism with the ``*args`` and ``**kwargs`` passed to
|
||||
the screen. If the screen doesn't exist, then "_screen" is appended
|
||||
to it, and that label is jumped to, ignoring `args` and `kwargs`.
|
||||
|
||||
If the optional keyword argument `_transition` is given, the
|
||||
menu will change screens using the provided transition.
|
||||
|
||||
@@ -893,6 +893,38 @@ init -1500 python:
|
||||
import pygame.scrap
|
||||
pygame.scrap.put(pygame.SCRAP_TEXT, self.s.encode("utf-8"))
|
||||
|
||||
@renpy.pure
|
||||
class EditFile(Action):
|
||||
"""
|
||||
:doc: other_action
|
||||
|
||||
Requests Ren'Py to open the given file in a text editor, if possible.
|
||||
This will work on some platforms but not others.
|
||||
|
||||
`filename`
|
||||
If given, the filename to open. If None, the current filename
|
||||
and line number are used, with `line` being ignored.
|
||||
|
||||
`line`
|
||||
The line number to open the file at.
|
||||
"""
|
||||
|
||||
def __init__(self, filename=None, line=1):
|
||||
self.filename = filename
|
||||
self.line = line
|
||||
|
||||
def __call__(self):
|
||||
|
||||
filename = self.filename
|
||||
line = self.line
|
||||
|
||||
if filename is None:
|
||||
filename, line = renpy.get_filename_line()
|
||||
|
||||
try:
|
||||
renpy.launch_editor([ filename ], line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
init -1500:
|
||||
|
||||
|
||||
+152
-216
@@ -112,59 +112,26 @@ init -1500 python:
|
||||
self.old_value = other.value
|
||||
self.start_time = None
|
||||
|
||||
@renpy.pure
|
||||
class DictValue(BarValue, FieldEquality):
|
||||
"""
|
||||
:doc: value
|
||||
|
||||
A value that allows the user to adjust the value of a key
|
||||
in a dict.
|
||||
|
||||
`dict`
|
||||
The dict.
|
||||
`key`
|
||||
The key.
|
||||
`range`
|
||||
The range to adjust over.
|
||||
`max_is_zero`
|
||||
If True, then when the value of a key is zero, the value of the
|
||||
bar will be range, and all other values will be shifted down by 1.
|
||||
This works both ways - when the bar is set to the maximum, the
|
||||
value of a key is set to 0.
|
||||
|
||||
`style`
|
||||
The styles of the bar created.
|
||||
`offset`
|
||||
An offset to add to the value.
|
||||
`step`
|
||||
The amount to change the bar by. If None, defaults to 1/10th of
|
||||
the bar.
|
||||
`action`
|
||||
If not None, an action to call when the field has changed.
|
||||
"""
|
||||
|
||||
class __GenericValue(BarValue, FieldEquality):
|
||||
# pickle defaults
|
||||
offset = 0
|
||||
action = None
|
||||
force_step = False
|
||||
|
||||
identity_fields = [ 'dict' ]
|
||||
equality_fields = [ 'key', 'range', 'max_is_zero', 'style', 'offset', 'step', 'action', 'force_step' ]
|
||||
identity_fields = ()
|
||||
equality_fields = ('range', 'max_is_zero', 'style', 'offset', 'step', 'action', 'force_step')
|
||||
|
||||
def __init__(self, dict, key, range, max_is_zero=False, style="bar", offset=0, step=None, action=None, force_step=False):
|
||||
self.dict = dict
|
||||
self.key = key
|
||||
def __init__(self, range, max_is_zero=False, style="bar", offset=0, step=None, action=None, force_step=False):
|
||||
self.range = range
|
||||
self.max_is_zero = max_is_zero
|
||||
self.style = style
|
||||
self.offset = offset
|
||||
self.force_step = force_step
|
||||
|
||||
if step is None:
|
||||
if isinstance(range, float):
|
||||
step = range / 10.0
|
||||
else:
|
||||
step = max(range / 10, 1)
|
||||
|
||||
step = max(range // 10, 1)
|
||||
self.step = step
|
||||
self.action = action
|
||||
|
||||
@@ -178,14 +145,13 @@ init -1500 python:
|
||||
|
||||
value += self.offset
|
||||
|
||||
self.dict[self.key] = value
|
||||
self.set_value(value)
|
||||
renpy.restart_interaction()
|
||||
|
||||
return renpy.run(self.action)
|
||||
|
||||
def get_adjustment(self):
|
||||
|
||||
value = self.dict[self.key]
|
||||
value = self.get_value()
|
||||
|
||||
value -= self.offset
|
||||
|
||||
@@ -207,10 +173,46 @@ init -1500 python:
|
||||
return self.style, "v" + self.style
|
||||
|
||||
@renpy.pure
|
||||
class FieldValue(BarValue, FieldEquality):
|
||||
class DictValue(__GenericValue):
|
||||
"""
|
||||
:doc: value
|
||||
:args: (self, object, field, range, max_is_zero=False, style="bar", offset=0, step=None, action=None, force_step=False)
|
||||
:args: {args}
|
||||
|
||||
A bar value that allows the user to adjust the value of a key in
|
||||
a dict, or of an element at a particular index in a list.
|
||||
|
||||
`dict`
|
||||
The dict, or the list.
|
||||
`key`
|
||||
The key, or the index when using a list.
|
||||
"""
|
||||
|
||||
kind = "key or index"
|
||||
|
||||
identity_fields = ('dict',)
|
||||
equality_fields = __GenericValue.equality_fields + ('key',)
|
||||
|
||||
def __init__(self, dict, key, *args, **kwargs):
|
||||
self.dict = dict
|
||||
self.key = key
|
||||
super(DictValue, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_value(self):
|
||||
value = self.dict[self.key]
|
||||
|
||||
return value
|
||||
|
||||
def set_value(self, value):
|
||||
try:
|
||||
self.dict[self.key] = value
|
||||
except LookupError as e:
|
||||
raise Exception("The {!r} {} does not exist".format(self.key, self.kind)) # from e # PY3 only
|
||||
|
||||
@renpy.pure
|
||||
class FieldValue(__GenericValue):
|
||||
"""
|
||||
:doc: value
|
||||
:args: {args}
|
||||
|
||||
A bar value that allows the user to adjust the value of a field
|
||||
on an object.
|
||||
@@ -218,223 +220,157 @@ init -1500 python:
|
||||
`object`
|
||||
The object.
|
||||
`field`
|
||||
The field, a string.
|
||||
`range`
|
||||
The range to adjust over.
|
||||
`max_is_zero`
|
||||
If True, then when the field is zero, the value of the
|
||||
bar will be range, and all other values will be shifted
|
||||
down by 1. This works both ways - when the bar is set to
|
||||
the maximum, the field is set to 0.
|
||||
|
||||
This is used internally, for some preferences.
|
||||
`style`
|
||||
The styles of the bar created.
|
||||
`offset`
|
||||
An offset to add to the value.
|
||||
`step`
|
||||
The amount to change the bar by. If None, defaults to 1/10th of
|
||||
the bar.
|
||||
`action`
|
||||
If not None, an action to call when the field has changed.
|
||||
The field name, a string.
|
||||
"""
|
||||
|
||||
offset = 0
|
||||
action = None
|
||||
force_step = False
|
||||
kind = "field"
|
||||
|
||||
identity_fields = [ 'object', ]
|
||||
equality_fields = [ 'range', 'max_is_zero', 'style', 'offset', 'step', 'action', 'force_step', 'field' ]
|
||||
identity_fields = ('object',)
|
||||
equality_fields = __GenericValue.equality_fields + ("field",)
|
||||
|
||||
def __init__(self, object, field, range, max_is_zero=False, style="bar", offset=0, step=None, action=None, force_step=False, kind="field"):
|
||||
def __init__(self, object, field, *args, **kwargs):
|
||||
self.object = object
|
||||
self.field = field
|
||||
self.range = range
|
||||
self.max_is_zero = max_is_zero
|
||||
self.style = style
|
||||
self.offset = offset
|
||||
self.force_step = force_step
|
||||
self.kind = kind
|
||||
super(FieldValue, self).__init__(*args, **kwargs)
|
||||
|
||||
if step is None:
|
||||
if isinstance(range, float):
|
||||
step = range / 10.0
|
||||
else:
|
||||
step = max(range / 10, 1)
|
||||
def get_value(self):
|
||||
value = _get_field(self.object, self.field, self.kind)
|
||||
|
||||
self.step = step
|
||||
self.action = action
|
||||
return value
|
||||
|
||||
def changed(self, value):
|
||||
|
||||
if self.max_is_zero:
|
||||
if value == self.range:
|
||||
value = 0
|
||||
else:
|
||||
value = value + 1
|
||||
|
||||
value += self.offset
|
||||
|
||||
_set_field(self.object, self.field, value, "field")
|
||||
renpy.restart_interaction()
|
||||
|
||||
return renpy.run(self.action)
|
||||
|
||||
def get_adjustment(self):
|
||||
|
||||
value = _get_field(self.object, self.field, "field")
|
||||
|
||||
value -= self.offset
|
||||
|
||||
if self.max_is_zero:
|
||||
if value == 0:
|
||||
value = self.range
|
||||
else:
|
||||
value = value - 1
|
||||
|
||||
return ui.adjustment(
|
||||
range=self.range,
|
||||
value=value,
|
||||
changed=self.changed,
|
||||
step=self.step,
|
||||
force_step=self.force_step,
|
||||
)
|
||||
|
||||
def get_style(self):
|
||||
return self.style, "v" + self.style
|
||||
def set_value(self, value):
|
||||
_set_field(self.object, self.field, value, self.kind)
|
||||
|
||||
@renpy.pure
|
||||
def VariableValue(variable, range, max_is_zero=False, style="bar", offset=0, step=None, action=None, force_step=False):
|
||||
class VariableValue(FieldValue):
|
||||
"""
|
||||
:doc: value
|
||||
:args: {args}
|
||||
|
||||
A bar value that allows the user to adjust the value of a variable
|
||||
in the default store.
|
||||
A bar value that allows the user to adjust the value of a variable in
|
||||
the default store.
|
||||
|
||||
`variable`
|
||||
The `variable` parameter must be a string, and can be a simple name like "strength", or
|
||||
one with dots separating the variable from fields, like "hero.strength"
|
||||
or "persistent.show_cutscenes".
|
||||
`range`
|
||||
The range to adjust over.
|
||||
`max_is_zero`
|
||||
If True, then when the field is zero, the value of the
|
||||
bar will be range, and all other values will be shifted
|
||||
down by 1. This works both ways - when the bar is set to
|
||||
the maximum, the field is set to 0.
|
||||
|
||||
This is used internally, for some preferences.
|
||||
`style`
|
||||
The styles of the bar created.
|
||||
`offset`
|
||||
An offset to add to the value.
|
||||
`step`
|
||||
The amount to change the bar by. If None, defaults to 1/10th of
|
||||
the bar.
|
||||
`action`
|
||||
If not None, an action to call when the field has changed.
|
||||
The `variable` parameter must be a string, and can be a simple
|
||||
name like "strength", or one with dots separating the variable
|
||||
from fields, like "hero.strength" or "persistent.show_cutscenes".
|
||||
"""
|
||||
|
||||
return FieldValue(store, variable, range, max_is_zero=max_is_zero, style=style, offset=offset, step=step, action=action, force_step=force_step, kind="variable")
|
||||
kind = "variable"
|
||||
|
||||
def __init__(self, variable, *args, **kwargs):
|
||||
super(VariableValue, self).__init__(store, variable, *args, **kwargs)
|
||||
|
||||
@renpy.pure
|
||||
class ScreenVariableValue(BarValue, FieldEquality):
|
||||
class ScreenVariableValue(__GenericValue):
|
||||
"""
|
||||
:doc: value
|
||||
:args: {args}
|
||||
|
||||
A bar value that adjusts the value of a variable in a screen.
|
||||
|
||||
In a ``use``\ d screen, this targets a variable in the context of
|
||||
the screen containing the ``use``\ d one(s). To target variables
|
||||
within a ``use``\ d screen, and only in that case, use
|
||||
:func:`LocalVariableValue` instead.
|
||||
|
||||
`variable`
|
||||
A string giving the name of the variable to adjust.
|
||||
"""
|
||||
|
||||
kind = "screen variable" # not used in error messages, only in doc-gen
|
||||
|
||||
identity_fields = ()
|
||||
equality_fields = __GenericValue.equality_fields + ('variable',)
|
||||
|
||||
def __init__(self, variable, *args, **kwargs):
|
||||
self.variable = variable
|
||||
super(ScreenVariableValue, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_value(self):
|
||||
cs = renpy.current_screen()
|
||||
|
||||
if cs is None:
|
||||
raise Exception("No current screen.")
|
||||
if self.variable not in cs.scope:
|
||||
raise Exception("The {!r} variable is not defined in the {} screen.".format(self.variable, cs.screen_name))
|
||||
|
||||
value = cs.scope[self.variable]
|
||||
|
||||
return value
|
||||
|
||||
def set_value(self, value):
|
||||
cs = renpy.current_screen()
|
||||
cs.scope[self.variable] = value
|
||||
|
||||
# unpure
|
||||
class LocalVariableValue(DictValue):
|
||||
"""
|
||||
:doc: value
|
||||
:args: {args}
|
||||
|
||||
A bar value that adjusts the value of a variable in a ``use``\ d
|
||||
screen.
|
||||
|
||||
To target a variable in a top-level screen, prefer using
|
||||
:func:`ScreenVariableValue`.
|
||||
|
||||
For more information, see :ref:`sl-use`.
|
||||
|
||||
This must be created in the context that the variable is set in
|
||||
- it can't be passed in from somewhere else.
|
||||
|
||||
`variable`
|
||||
A string giving the name of the variable to adjust.
|
||||
"""
|
||||
|
||||
kind = "local variable"
|
||||
|
||||
def __init__(self, variable, *args, **kwargs):
|
||||
super(LocalVariableValue, self).__init__(sys._getframe(1).f_locals, variable, *args, **kwargs)
|
||||
|
||||
init -1500 python hide:
|
||||
if config.generating_documentation:
|
||||
import inspect
|
||||
import itertools
|
||||
|
||||
generic_params = tuple(inspect.signature(__GenericValue.__init__).parameters.values())[1:]
|
||||
suffix = inspect.cleandoc("""
|
||||
`range`
|
||||
The range to adjust over.
|
||||
`max_is_zero`
|
||||
If True, then when the field is zero, the value of the
|
||||
bar will be range, and all other values will be shifted
|
||||
down by 1. This works both ways - when the bar is set to
|
||||
the maximum, the field is set to 0.
|
||||
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
|
||||
by 1. This works both ways - when the bar is set to the maximum,
|
||||
the value of the {kind} is set to 0.
|
||||
|
||||
This is used internally, for some preferences.
|
||||
`style`
|
||||
The styles of the bar created.
|
||||
The styles of the created bar.
|
||||
`offset`
|
||||
An offset to add to the value.
|
||||
`step`
|
||||
The amount to change the bar by. If None, defaults to 1/10th of
|
||||
the bar.
|
||||
`action`
|
||||
If not None, an action to call when the field has changed.
|
||||
"""
|
||||
If not None, an action to call when the {kind}'s value is changed.
|
||||
""")
|
||||
|
||||
action = None
|
||||
offset = 0
|
||||
force_step = False
|
||||
for value in (DictValue, FieldValue, VariableValue, ScreenVariableValue, LocalVariableValue):
|
||||
docstr = inspect.cleandoc(value.__doc__)
|
||||
|
||||
identity_fields = [ ]
|
||||
equality_fields = [ 'variable', 'max_is_zero', 'style', 'offset', 'step', 'action', 'force_step' ]
|
||||
params = []
|
||||
for param in itertools.islice(inspect.signature(value.__init__).parameters.values(), 1, None):
|
||||
if param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
|
||||
params.append(param)
|
||||
|
||||
def __init__(self, variable, range, max_is_zero=False, style="bar", offset=0, step=None, action=None, force_step=False):
|
||||
self.variable = variable
|
||||
self.range = range
|
||||
self.max_is_zero = max_is_zero
|
||||
self.style = style
|
||||
self.offset = offset
|
||||
self.force_step = force_step
|
||||
params.extend(generic_params)
|
||||
|
||||
if step is None:
|
||||
if isinstance(range, float):
|
||||
step = range / 10.0
|
||||
else:
|
||||
step = max(range / 10, 1)
|
||||
value.__doc__ = (docstr+"\n"+suffix).format(
|
||||
args=inspect.Signature(parameters=params),
|
||||
kind=value.kind)
|
||||
|
||||
self.step = step
|
||||
self.action = action
|
||||
|
||||
def changed(self, value):
|
||||
|
||||
cs = renpy.current_screen()
|
||||
|
||||
if self.max_is_zero:
|
||||
if value == self.range:
|
||||
value = 0
|
||||
else:
|
||||
value = value + 1
|
||||
|
||||
value += self.offset
|
||||
|
||||
cs.scope[self.variable] = value
|
||||
renpy.restart_interaction()
|
||||
|
||||
return renpy.run(self.action)
|
||||
|
||||
def get_adjustment(self):
|
||||
|
||||
cs = renpy.current_screen()
|
||||
|
||||
if (cs is None) or (self.variable not in cs.scope):
|
||||
raise Exception("{} is not defined in the {} screen.".format(self.variable, cs.screen_name))
|
||||
|
||||
value = cs.scope[self.variable]
|
||||
|
||||
value -= self.offset
|
||||
|
||||
if self.max_is_zero:
|
||||
if value == 0:
|
||||
value = self.range
|
||||
else:
|
||||
value = value - 1
|
||||
|
||||
return ui.adjustment(
|
||||
range=self.range,
|
||||
value=value,
|
||||
changed=self.changed,
|
||||
step=self.step,
|
||||
force_step=self.force_step,
|
||||
)
|
||||
|
||||
def get_style(self):
|
||||
return self.style, "v" + self.style
|
||||
init -1500 python:
|
||||
|
||||
@renpy.pure
|
||||
class MixerValue(BarValue, DictEquality):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2004-2023 Tom Rothamel <pytom@bishoujo.us>
|
||||
# Copyright 2004-2023 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
|
||||
@@ -159,6 +159,7 @@ init -1500 python in build:
|
||||
("*.dll", None),
|
||||
("*.manifest", None),
|
||||
("*.keystore", None),
|
||||
("update.pem", None),
|
||||
|
||||
("lib/", None),
|
||||
("renpy/", None),
|
||||
@@ -207,6 +208,7 @@ init -1500 python in build:
|
||||
("web-presplash.png", "web"),
|
||||
("web-presplash.jpg", "web"),
|
||||
("web-presplash.webp", "web"),
|
||||
("web-icon.png", "web"),
|
||||
("progressive_download.txt", "web"),
|
||||
|
||||
("steam_appid.txt", None),
|
||||
@@ -506,6 +508,9 @@ init -1500 python in build:
|
||||
# Should the sdk-fonts directory be renamed to game?
|
||||
_sdk_fonts = False
|
||||
|
||||
# Which update formats should be built?
|
||||
update_formats = [ "rpu" ]
|
||||
|
||||
# This function is called by the json_dump command to dump the build data
|
||||
# into the json file.
|
||||
def dump():
|
||||
@@ -589,6 +594,8 @@ init -1500 python in build:
|
||||
|
||||
rv["_sdk_fonts"] = _sdk_fonts
|
||||
|
||||
rv["update_formats"] = update_formats
|
||||
|
||||
return rv
|
||||
|
||||
init 1500 python in build:
|
||||
|
||||
@@ -299,7 +299,6 @@ init -1100 python:
|
||||
config.simple_box_reverse = True
|
||||
build.itch_channels = list(build.itch_channels.items())
|
||||
style.default.shaper = "freetype"
|
||||
style.default.hinting = "auto"
|
||||
|
||||
# The version of Ren'Py this script is intended for, or
|
||||
# None if it's intended for the current version.
|
||||
@@ -411,7 +410,7 @@ init 1100 python hide:
|
||||
config.layers.append('screens')
|
||||
|
||||
if "Fullscreen" in config.translations:
|
||||
fs = _("Fullscreen")
|
||||
fs = __("Fullscreen")
|
||||
config.translations.setdefault("Fullscreen 4:3", fs + " 4:3")
|
||||
config.translations.setdefault("Fullscreen 16:9", fs + " 16:9")
|
||||
config.translations.setdefault("Fullscreen 16:10", fs + " 16:10")
|
||||
|
||||
@@ -593,7 +593,7 @@ init -1500 python in _console:
|
||||
else:
|
||||
break
|
||||
|
||||
if s.rstrip().endswith(":"):
|
||||
if s.partition("#")[0].rstrip().endswith(":"):
|
||||
rv += " "
|
||||
|
||||
if not s.rstrip():
|
||||
@@ -612,7 +612,7 @@ init -1500 python in _console:
|
||||
self.lines.append(line)
|
||||
|
||||
indent = get_indent(line)
|
||||
if indent:
|
||||
if indent or line.startswith("@") or line.endswith("\\"):
|
||||
self.lines.append(indent)
|
||||
return
|
||||
|
||||
@@ -717,7 +717,7 @@ init -1500 python in _console:
|
||||
pass
|
||||
else:
|
||||
result = renpy.python.py_eval(code)
|
||||
if persistent._console_short:
|
||||
if persistent._console_short and not getattr(result, "_console_always_long", False):
|
||||
he.result = aRepr.repr(result)
|
||||
else:
|
||||
he.result = repr(result)
|
||||
@@ -1103,21 +1103,25 @@ screen _console:
|
||||
|
||||
has vbox
|
||||
|
||||
$ last_line = ""
|
||||
|
||||
for line in lines:
|
||||
hbox:
|
||||
spacing 4
|
||||
|
||||
if line[:1] != " ":
|
||||
if (line[:1] != " ") and (last_line[:1] != "@") and (last_line[-1:] != "\\"):
|
||||
text "> " style "_console_prompt"
|
||||
else:
|
||||
text "... " style "_console_prompt"
|
||||
|
||||
text "[line!q]" style "_console_input_text"
|
||||
|
||||
$ last_line = line
|
||||
|
||||
hbox:
|
||||
spacing 4
|
||||
|
||||
if default[:1] != " ":
|
||||
if (default[:1] != " ") and (last_line[:1] != "@") and (last_line[-1:] != "\\"):
|
||||
text "> " style "_console_prompt"
|
||||
else:
|
||||
text "... " style "_console_prompt"
|
||||
|
||||
@@ -280,6 +280,10 @@ init python in director:
|
||||
|
||||
return show_director
|
||||
|
||||
def after_load():
|
||||
state.mode = "lines"
|
||||
|
||||
config.after_load_callbacks.append(after_load)
|
||||
|
||||
def interact():
|
||||
"""
|
||||
@@ -1766,6 +1770,7 @@ screen director_transform(state):
|
||||
null height 14
|
||||
|
||||
text _("Click to set transform, right click to add to transform list.")
|
||||
text _("Customize director.transforms to add more transforms.")
|
||||
|
||||
use director_footer(state)
|
||||
|
||||
@@ -1786,6 +1791,8 @@ screen director_behind(state):
|
||||
style "director_button"
|
||||
ypadding 0
|
||||
|
||||
null height 14
|
||||
|
||||
text _("Click to set, right click to add to behind list.")
|
||||
|
||||
use director_footer(state)
|
||||
@@ -1806,6 +1813,11 @@ screen director_with(state):
|
||||
style "director_button"
|
||||
ypadding 0
|
||||
|
||||
null height 14
|
||||
|
||||
text _("Click to set.")
|
||||
text _("Customize director.transitions to add more transitions.")
|
||||
|
||||
use director_footer(state)
|
||||
|
||||
|
||||
@@ -1824,6 +1836,11 @@ screen director_channel(state):
|
||||
style "director_button"
|
||||
ypadding 0
|
||||
|
||||
null height 14
|
||||
|
||||
text _("Click to set.")
|
||||
text _("Customize director.audio_channels to add more channels.")
|
||||
|
||||
use director_footer(state)
|
||||
|
||||
|
||||
|
||||
+140
-87
@@ -79,22 +79,27 @@ init -1510 python:
|
||||
|
||||
@renpy.pure
|
||||
class InputValue(renpy.object.Object):
|
||||
"""
|
||||
Subclassable by creators, documented in Sphinx.
|
||||
"""
|
||||
|
||||
default = True
|
||||
editable = True
|
||||
returnable = False
|
||||
disable_on_enter = False
|
||||
|
||||
def get_text(self):
|
||||
raise Exception("Not implemented.")
|
||||
raise NotImplementedError
|
||||
|
||||
def set_text(self, s):
|
||||
raise Exception("Not implemented.")
|
||||
raise NotImplementedError
|
||||
|
||||
def enter(self):
|
||||
if self.returnable:
|
||||
return self.get_text()
|
||||
else:
|
||||
return None
|
||||
elif self.disable_on_enter:
|
||||
renpy.run(self.Disable())
|
||||
raise renpy.IgnoreEvent
|
||||
|
||||
def Enable(self):
|
||||
if self.editable:
|
||||
@@ -114,70 +119,41 @@ init -1510 python:
|
||||
else:
|
||||
return None
|
||||
|
||||
@renpy.pure
|
||||
class VariableInputValue(InputValue, FieldEquality):
|
||||
class __GenericInputValue(InputValue, FieldEquality):
|
||||
"""
|
||||
Not subclassable by creators, not documented, meant to factorize
|
||||
common features of the documented input value classes.
|
||||
"""
|
||||
|
||||
equality_fields = ("default", "returnable", "disable_on_enter")
|
||||
|
||||
def __init__(self, default=True, returnable=False, disable_on_enter=False):
|
||||
self.default = default
|
||||
self.returnable = returnable
|
||||
self.disable_on_enter = disable_on_enter
|
||||
|
||||
class ScreenVariableInputValue(__GenericInputValue):
|
||||
"""
|
||||
:doc: input_value
|
||||
:args: {args}
|
||||
|
||||
An input value that updates `variable`.
|
||||
An input value that updates a variable in a screen.
|
||||
|
||||
In a ``use``\ d screen, this targets a variable in the context of the
|
||||
screen containing the ``use``\ d one(s). To target variables within a
|
||||
``use``\ d screen, and only in that case, use
|
||||
:func:`LocalVariableInputValue` instead.
|
||||
|
||||
`variable`
|
||||
A string giving the name of the variable to update.
|
||||
|
||||
The `variable` parameter must be a string, and can be a simple name like "strength", or
|
||||
one with dots separating the variable from fields, like "hero.strength"
|
||||
or "persistent.show_cutscenes".
|
||||
|
||||
`default`
|
||||
If true, this input can be editable by default.
|
||||
|
||||
`returnable`
|
||||
If true, the value of this input will be returned when the
|
||||
user presses enter.
|
||||
"""
|
||||
|
||||
identity_fields = [ ]
|
||||
equality_fields = [ "variable", "returnable" ]
|
||||
identity_fields = ("screen",)
|
||||
equality_fields = __GenericInputValue.equality_fields+("variable",)
|
||||
|
||||
def __init__(self, variable, default=True, returnable=False):
|
||||
def __init__(self, variable, *args, **kwargs):
|
||||
super(ScreenVariableInputValue, self).__init__(*args, **kwargs)
|
||||
self.variable = variable
|
||||
|
||||
self.default = default
|
||||
self.returnable = returnable
|
||||
|
||||
def get_text(self):
|
||||
return _get_field(store, self.variable, "variable")
|
||||
|
||||
def set_text(self, s):
|
||||
_set_field(store, self.variable, s, "variable")
|
||||
renpy.restart_interaction()
|
||||
|
||||
class ScreenVariableInputValue(InputValue, FieldEquality):
|
||||
"""
|
||||
:doc: input_value
|
||||
|
||||
An input value that updates variable.
|
||||
|
||||
`variable`
|
||||
A string giving the name of the variable to update.
|
||||
|
||||
`default`
|
||||
If true, this input can be editable by default.
|
||||
|
||||
`returnable`
|
||||
If true, the value of this input will be returned when the
|
||||
user presses enter.
|
||||
"""
|
||||
|
||||
identity_fields = [ 'screen' ]
|
||||
equality_fields = [ "variable", "returnable" ]
|
||||
|
||||
def __init__(self, variable, default=True, returnable=False):
|
||||
self.variable = variable
|
||||
|
||||
self.default = default
|
||||
self.returnable = returnable
|
||||
|
||||
self.screen = renpy.current_screen()
|
||||
|
||||
def get_text(self):
|
||||
@@ -190,68 +166,145 @@ init -1510 python:
|
||||
renpy.restart_interaction()
|
||||
|
||||
@renpy.pure
|
||||
class FieldInputValue(InputValue, FieldEquality):
|
||||
class FieldInputValue(__GenericInputValue):
|
||||
"""
|
||||
:doc: input_value
|
||||
:args: {args}
|
||||
|
||||
An input value that updates `field` on `object`.
|
||||
|
||||
`field`
|
||||
A string giving the name of the field.
|
||||
|
||||
`default`
|
||||
If true, this input can be editable by default.
|
||||
|
||||
`returnable`
|
||||
If true, the value of this input will be returned when the
|
||||
user presses enter.
|
||||
"""
|
||||
|
||||
identity_fields = [ "object"]
|
||||
equality_fields = [ "field", "returnable" ]
|
||||
identity_fields = ("object",)
|
||||
equality_fields = __GenericInputValue.equality_fields+("field",)
|
||||
|
||||
def __init__(self, object, field, default=True, returnable=False):
|
||||
kind = "field"
|
||||
|
||||
def __init__(self, object, field, *args, **kwargs):
|
||||
super(FieldInputValue, self).__init__(*args, **kwargs)
|
||||
self.object = object
|
||||
self.field = field
|
||||
|
||||
self.default = default
|
||||
self.returnable = returnable
|
||||
|
||||
def get_text(self):
|
||||
return _get_field(self.object, self.field, "field")
|
||||
return _get_field(self.object, self.field, self.kind)
|
||||
|
||||
def set_text(self, s):
|
||||
_set_field(self.object, self.field, s, "field")
|
||||
_set_field(self.object, self.field, s, self.kind)
|
||||
renpy.restart_interaction()
|
||||
|
||||
@renpy.pure
|
||||
class DictInputValue(InputValue, FieldEquality):
|
||||
class VariableInputValue(FieldInputValue):
|
||||
"""
|
||||
:doc: input_value
|
||||
:args: {args}
|
||||
|
||||
An input value that updates `key` in `dict`.
|
||||
An input value that updates `variable`.
|
||||
|
||||
`default`
|
||||
If true, this input can be editable by default.
|
||||
`variable`
|
||||
A string giving the name of the variable to update.
|
||||
|
||||
`returnable`
|
||||
If true, the value of this input will be returned when the
|
||||
user presses enter.
|
||||
The `variable` parameter must be a string, and can be a simple name like "strength", or
|
||||
one with dots separating the variable from fields, like "hero.strength"
|
||||
or "persistent.show_cutscenes".
|
||||
"""
|
||||
|
||||
identity_fields = [ "dict", "key" ]
|
||||
equality_fields = [ "returnable" ]
|
||||
__version__ = 1
|
||||
|
||||
def __init__(self, dict, key, default=True, returnable=False):
|
||||
kind = "variable"
|
||||
|
||||
def after_upgrade(self, version):
|
||||
if version < 1:
|
||||
self.field = self.variable
|
||||
self.object = store
|
||||
del self.variable
|
||||
|
||||
def __init__(self, variable, *args, **kwargs):
|
||||
super(VariableInputValue, self).__init__(store, variable, *args, **kwargs)
|
||||
|
||||
@renpy.pure
|
||||
class DictInputValue(__GenericInputValue):
|
||||
"""
|
||||
:doc: input_value
|
||||
:args: {args}
|
||||
|
||||
An input value that updates ``dict[key]``.
|
||||
|
||||
`dict` may be a dict object or a list.
|
||||
"""
|
||||
|
||||
identity_fields = ("dict",)
|
||||
equality_fields = __GenericInputValue.equality_fields+("key",)
|
||||
|
||||
def __init__(self, dict, key, *args, **kwargs):
|
||||
super(DictInputValue, self).__init__(*args, **kwargs)
|
||||
self.dict = dict
|
||||
self.key = key
|
||||
|
||||
self.default = default
|
||||
self.returnable = returnable
|
||||
|
||||
def get_text(self):
|
||||
return self.dict[self.key]
|
||||
|
||||
def set_text(self, s):
|
||||
self.dict[self.key] = s
|
||||
renpy.restart_interaction()
|
||||
|
||||
# not pure
|
||||
class LocalVariableInputValue(DictInputValue):
|
||||
"""
|
||||
:doc: input_value
|
||||
:args: {args}
|
||||
|
||||
An input value that updates a local variable in a ``use``\ d screen.
|
||||
|
||||
To target a variable in a top-level screen, prefer using
|
||||
:func:`ScreenVariableInputValue`.
|
||||
|
||||
For more information, see :ref:`sl-use`.
|
||||
|
||||
This must be created in the context that the variable is set in - it
|
||||
can't be passed in from somewhere else.
|
||||
|
||||
`variable`
|
||||
A string giving the name of the variable to update.
|
||||
"""
|
||||
|
||||
def __init__(self, variable, *args, **kwargs):
|
||||
super(LocalVariableInputValue, self).__init__(sys._getframe(1).f_locals, variable, *args, **kwargs)
|
||||
|
||||
def get_text(self):
|
||||
try:
|
||||
return super(LocalVariableInputValue, self).get_text()
|
||||
except LookupError:
|
||||
raise Exception("The {!r} local variable does not exist.".format(self.key)) # from e # PY3 only
|
||||
|
||||
init -1510 python hide:
|
||||
if config.generating_documentation:
|
||||
import inspect
|
||||
import itertools
|
||||
|
||||
generic_params = tuple(inspect.signature(__GenericInputValue.__init__).parameters.values())[1:]
|
||||
suffix = inspect.cleandoc("""
|
||||
`default`
|
||||
If true, this input can be editable by default.
|
||||
`returnable`
|
||||
If true, the value of this input will be returned when the
|
||||
user presses enter.
|
||||
`disable_on_enter`
|
||||
If true, and if `returnable` is not true, pressing enter
|
||||
will disable this input.
|
||||
""")
|
||||
|
||||
for ivalue in (ScreenVariableInputValue, FieldInputValue, VariableInputValue, DictInputValue, LocalVariableInputValue):
|
||||
docstr = inspect.cleandoc(ivalue.__doc__)
|
||||
|
||||
params = []
|
||||
for param in itertools.islice(inspect.signature(ivalue.__init__).parameters.values(), 1, None):
|
||||
if param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
|
||||
params.append(param)
|
||||
|
||||
params.extend(generic_params)
|
||||
|
||||
ivalue.__doc__ = (docstr+"\n"+suffix).format(
|
||||
args=inspect.Signature(parameters=params),
|
||||
)
|
||||
|
||||
@@ -883,6 +883,11 @@ python early in layeredimage:
|
||||
ll = l.subblock_lexer()
|
||||
|
||||
while ll.advance():
|
||||
if ll.keyword("pass"):
|
||||
ll.expect_eol()
|
||||
ll.expect_noblock("pass")
|
||||
continue
|
||||
|
||||
line(ll)
|
||||
ll.expect_eol()
|
||||
ll.expect_noblock('attribute')
|
||||
@@ -917,18 +922,23 @@ python early in layeredimage:
|
||||
|
||||
if not l.match(':'):
|
||||
l.expect_eol()
|
||||
l.expect_noblock('attribute')
|
||||
l.expect_noblock('always')
|
||||
return
|
||||
|
||||
l.expect_block('attribute')
|
||||
l.expect_block('always')
|
||||
l.expect_eol()
|
||||
|
||||
ll = l.subblock_lexer()
|
||||
|
||||
while ll.advance():
|
||||
if ll.keyword("pass"):
|
||||
ll.expect_eol()
|
||||
ll.expect_noblock("pass")
|
||||
continue
|
||||
|
||||
line(ll)
|
||||
ll.expect_eol()
|
||||
ll.expect_noblock('attribute')
|
||||
ll.expect_noblock('always')
|
||||
|
||||
if a.image is None:
|
||||
l.error("The always statement must have a displayable.")
|
||||
@@ -954,6 +964,11 @@ python early in layeredimage:
|
||||
ll = l.subblock_lexer()
|
||||
|
||||
while ll.advance():
|
||||
if ll.keyword("pass"):
|
||||
ll.expect_eol()
|
||||
ll.expect_noblock("pass")
|
||||
continue
|
||||
|
||||
if ll.keyword("attribute"):
|
||||
parse_attribute(ll, rv)
|
||||
continue
|
||||
@@ -987,7 +1002,12 @@ python early in layeredimage:
|
||||
rv = RawCondition(condition)
|
||||
|
||||
while ll.advance():
|
||||
|
||||
# not necessary : the if/elif/else blocks require a displayable,
|
||||
# so they can't be empty in the first place anyway
|
||||
# if ll.keyword("pass"):
|
||||
# ll.expect_eol()
|
||||
# ll.expect_noblock("pass")
|
||||
# continue
|
||||
|
||||
while True:
|
||||
|
||||
@@ -1078,6 +1098,11 @@ python early in layeredimage:
|
||||
parse_always(ll, rv)
|
||||
ll.advance()
|
||||
|
||||
elif ll.keyword("pass"):
|
||||
ll.expect_noblock("pass")
|
||||
ll.expect_eol()
|
||||
ll.advance()
|
||||
|
||||
else:
|
||||
|
||||
while parse_property(ll, rv, [ "image_format", "format_function", "attribute_function", "offer_screen", "at" ] +
|
||||
|
||||
@@ -363,14 +363,14 @@ init 1050 python hide:
|
||||
raise Exception("bubble.properties[{!r}] contains a value that can't be serialized to JSON: {!r}".format(k, i))
|
||||
|
||||
if bubble.active:
|
||||
config.overlay_screens.append("_bubble_editor")
|
||||
config.always_shown_screens.append("_bubble_editor")
|
||||
|
||||
|
||||
|
||||
screen _bubble_editor():
|
||||
zorder 1050
|
||||
|
||||
if bubble.shown.value:
|
||||
if bubble.shown.value and not _menu:
|
||||
|
||||
drag:
|
||||
draggable True
|
||||
|
||||
@@ -560,7 +560,7 @@ init -1499 python in _renpysteam:
|
||||
################################################################## Keyboard
|
||||
|
||||
# True to show the keyboard once, False otherwise.
|
||||
keyboard_mode = "always"
|
||||
keyboard_mode = "once"
|
||||
|
||||
# True if this is the start of a new interaction, and so the keyboard
|
||||
# should be shown if a text box appears.
|
||||
@@ -570,12 +570,16 @@ init -1499 python in _renpysteam:
|
||||
keyboard_showing = None
|
||||
|
||||
# Should the layers be shifted so the baseline is in view?
|
||||
keyboard_shift = True
|
||||
keyboard_shift = False
|
||||
|
||||
# Where the basline is shifted to on the screen. This is a floating point number,
|
||||
# Where the baseline is shifted to on the screen. This is a floating point number,
|
||||
# with 0.0 being the top of the screen and 1.0 being the bottom.
|
||||
keyboard_baseline = 0.5
|
||||
|
||||
# The textarea given to steam. This is scaled using the usual
|
||||
# position rules.
|
||||
keyboard_text_area = (0.0, 0.5, 1.0, 0.5)
|
||||
|
||||
def prime_keyboard():
|
||||
global keyboard_primed
|
||||
keyboard_primed = True
|
||||
@@ -600,10 +604,19 @@ init -1499 python in _renpysteam:
|
||||
_KeyboardShift.text_rect = keyboard_text_rect
|
||||
|
||||
if keyboard_primed and (keyboard_showing is None) and keyboard_text_rect:
|
||||
x, y, w, h = (int(i) for i in keyboard_text_rect)
|
||||
|
||||
if keyboard_shift:
|
||||
y = int(renpy.exports.get_physical_size()[1] * keyboard_baseline) - h
|
||||
pw, ph = renpy.exports.get_physical_size()
|
||||
|
||||
def scale(n, available):
|
||||
if type(n) == float:
|
||||
n = n * available
|
||||
|
||||
return int(n)
|
||||
|
||||
x = scale(keyboard_text_area[0], pw)
|
||||
y = scale(keyboard_text_area[1], ph)
|
||||
w = scale(keyboard_text_area[2], pw)
|
||||
h = scale(keyboard_text_area[3], ph)
|
||||
|
||||
steamapi.SteamUtils().ShowFloatingGamepadTextInput(
|
||||
steamapi.k_EFloatingGamepadTextInputModeModeSingleLine,
|
||||
|
||||
@@ -168,7 +168,7 @@ init -1800:
|
||||
ruby_style style.ruby_text
|
||||
altruby_style style.altruby_text
|
||||
# hyperlink_functions is set in 00defaults.rpy
|
||||
hinting "bytecode"
|
||||
hinting True
|
||||
adjust_spacing True
|
||||
emoji_font "TwemojiCOLRv0.ttf"
|
||||
prefer_emoji True
|
||||
|
||||
@@ -210,7 +210,7 @@ init -1100 python in _sync:
|
||||
f.write(content)
|
||||
|
||||
fetch_id = emscripten.run_script_int(
|
||||
"""fetchFile("PUT", "{url}", "/sync.data", null)""".format(url=url))
|
||||
"""fetchFile("PUT", "{url}", "/sync.data", null, "application/octet-string")""".format(url=url))
|
||||
|
||||
status = "PENDING"
|
||||
message = "Pending."
|
||||
|
||||
+289
-33
@@ -253,7 +253,7 @@ init -1500 python in updater:
|
||||
# The update was cancelled.
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
def __init__(self, url, base=None, force=False, public_key=None, simulate=None, add=[], restart=True, check_only=False, confirm=True, patch=True):
|
||||
def __init__(self, url, base=None, force=False, public_key=None, simulate=None, add=[], restart=True, check_only=False, confirm=True, patch=True, prefer_rpu=False, size_only=False, allow_empty=False):
|
||||
"""
|
||||
Takes the same arguments as update().
|
||||
"""
|
||||
@@ -312,6 +312,12 @@ init -1500 python in updater:
|
||||
# Do we prompt for confirmation?
|
||||
self.confirm = confirm
|
||||
|
||||
# Should rpu updates be preferred?
|
||||
self.prefer_rpu = prefer_rpu
|
||||
|
||||
# Should the update be allowed even if current.json is empty?
|
||||
self.allow_empty = allow_empty
|
||||
|
||||
# The base path of the game that we're updating, and the path to the update
|
||||
# directory underneath it.
|
||||
|
||||
@@ -344,6 +350,9 @@ init -1500 python in updater:
|
||||
# where each file is moved from <file>.new to <file>.
|
||||
self.moves = [ ]
|
||||
|
||||
if self.allow_empty:
|
||||
os.makedirs(self.updatedir, exist_ok=True)
|
||||
|
||||
if public_key is not None:
|
||||
with renpy.open_file(public_key, False) as f:
|
||||
self.public_key = rsa.PublicKey.load_pkcs1(f.read())
|
||||
@@ -415,14 +424,11 @@ init -1500 python in updater:
|
||||
Performs the update.
|
||||
"""
|
||||
|
||||
if getattr(renpy, "mobile", False):
|
||||
raise UpdateError(_("The Ren'Py Updater is not supported on mobile devices."))
|
||||
|
||||
self.load_state()
|
||||
self.test_write()
|
||||
self.check_updates()
|
||||
|
||||
pretty_version = self.check_versions()
|
||||
self.pretty_version = self.check_versions()
|
||||
|
||||
if not self.modules:
|
||||
self.can_cancel = False
|
||||
@@ -432,12 +438,61 @@ init -1500 python in updater:
|
||||
renpy.restart_interaction()
|
||||
return
|
||||
|
||||
persistent._update_version[self.url] = pretty_version
|
||||
persistent._update_version[self.url] = self.pretty_version
|
||||
|
||||
if self.check_only:
|
||||
renpy.restart_interaction()
|
||||
return
|
||||
|
||||
# Confirm goes here.
|
||||
|
||||
# Disable autoreload.
|
||||
renpy.set_autoreload(False)
|
||||
|
||||
self.new_state = dict(self.current_state)
|
||||
renpy.restart_interaction()
|
||||
|
||||
self.progress = 0.0
|
||||
self.state = self.PREPARING
|
||||
|
||||
import os
|
||||
|
||||
has_rpu = False
|
||||
has_zsync = False
|
||||
|
||||
prefer_rpu = self.prefer_rpu or "RPU_UPDATE" in os.environ
|
||||
|
||||
for i in self.modules:
|
||||
|
||||
for d in self.updates:
|
||||
if "rpu_url" in self.updates[d]:
|
||||
has_rpu = True
|
||||
if "zsync_url" in self.updates[d]:
|
||||
has_zsync = True
|
||||
|
||||
if has_rpu and has_zsync:
|
||||
|
||||
if prefer_rpu:
|
||||
self.rpu_update()
|
||||
else:
|
||||
self.zsync_update()
|
||||
|
||||
elif has_rpu:
|
||||
self.rpu_update()
|
||||
|
||||
elif has_zsync:
|
||||
self.zsync_update()
|
||||
|
||||
else:
|
||||
raise UpdateError(_("No update methods found."))
|
||||
|
||||
|
||||
def prompt_confirm(self):
|
||||
"""
|
||||
Prompts the user to confirm the update. Returns if the update
|
||||
should proceed, or raises UpdateCancelled if it should not.
|
||||
"""
|
||||
|
||||
if self.confirm and (not self.add):
|
||||
|
||||
# Confirm with the user that the update is available.
|
||||
@@ -445,7 +500,7 @@ init -1500 python in updater:
|
||||
self.can_cancel = True
|
||||
self.can_proceed = True
|
||||
self.state = self.UPDATE_AVAILABLE
|
||||
self.version = pretty_version
|
||||
self.version = self.pretty_version
|
||||
|
||||
renpy.restart_interaction()
|
||||
|
||||
@@ -461,15 +516,131 @@ init -1500 python in updater:
|
||||
self.can_cancel = True
|
||||
self.can_proceed = False
|
||||
|
||||
# Disable autoreload.
|
||||
renpy.set_autoreload(False)
|
||||
def fetch_files_rpu(self, module):
|
||||
"""
|
||||
Fetches the rpu file list for the given module.
|
||||
"""
|
||||
|
||||
import requests, zlib
|
||||
|
||||
url = urlparse.urljoin(self.url, self.updates[module]["rpu_url"])
|
||||
|
||||
try:
|
||||
resp = requests.get(url)
|
||||
resp.raise_for_status()
|
||||
except Exception as e:
|
||||
raise UpdateError(__("Could not download file list: ") + str(e))
|
||||
|
||||
if hashlib.sha256(resp.content).hexdigest() != self.updates[module]["rpu_digest"]:
|
||||
raise UpdateError(__("File list digest does not match."))
|
||||
|
||||
data = zlib.decompress(resp.content)
|
||||
|
||||
from renpy.update.common import FileList
|
||||
return FileList.from_json(json.loads(data))
|
||||
|
||||
def rpu_progress(self, state, progress):
|
||||
"""
|
||||
Called by the rpu code to update the progress.
|
||||
"""
|
||||
|
||||
old_state = self.state
|
||||
|
||||
self.state = state
|
||||
self.progress = progress
|
||||
|
||||
if state != old_state or progress == 1.0 or progress == 0.0:
|
||||
renpy.restart_interaction()
|
||||
|
||||
def rpu_update(self):
|
||||
"""
|
||||
Perform an update using the .rpu files.
|
||||
"""
|
||||
|
||||
from renpy.update.common import FileList
|
||||
from renpy.update.update import Update
|
||||
|
||||
# 1. Load the current files.
|
||||
|
||||
target_file_lists = [ ]
|
||||
|
||||
for i in self.modules:
|
||||
target_file_lists.append(FileList.from_current_json(self.current_state[i]))
|
||||
|
||||
# 2. Fetch the file lists.
|
||||
|
||||
source_file_lists = [ ]
|
||||
module_lists = { }
|
||||
|
||||
for i in self.modules:
|
||||
fl = self.fetch_files_rpu(i)
|
||||
module_lists[i] = fl
|
||||
source_file_lists.append(fl)
|
||||
|
||||
# 3. Compute the update, and confirm it.
|
||||
|
||||
u = Update(
|
||||
urlparse.urljoin(self.url, "rpu"),
|
||||
source_file_lists,
|
||||
self.base,
|
||||
target_file_lists,
|
||||
progress_callback=self.rpu_progress,
|
||||
logfile=self.log
|
||||
)
|
||||
|
||||
self.prompt_confirm()
|
||||
self.can_cancel = False
|
||||
|
||||
# 4. Remove the version.json file.
|
||||
|
||||
version_json = os.path.join(self.updatedir, "version.json")
|
||||
|
||||
if os.path.exists(version_json):
|
||||
os.unlink(version_json)
|
||||
|
||||
# 5. Apply the update.
|
||||
u.update()
|
||||
|
||||
# 6. Update the new state.
|
||||
for i in self.modules:
|
||||
d = module_lists[i].to_current_json()
|
||||
d["version"] = self.updates[i]["version"]
|
||||
d["renpy_version"] = self.updates[i]["renpy_version"]
|
||||
d["pretty_version"] = self.updates[i]["pretty_version"]
|
||||
self.new_state[i] = d
|
||||
|
||||
# 7. Finish up.
|
||||
|
||||
self.message = None
|
||||
self.progress = None
|
||||
self.can_proceed = True
|
||||
self.can_cancel = False
|
||||
|
||||
persistent._update_version[self.url] = None
|
||||
|
||||
# 8. Write the version.json file.
|
||||
version_state = { }
|
||||
|
||||
for i in self.modules:
|
||||
version_state[i] = {
|
||||
"version" : self.updates[i]["version"],
|
||||
"renpy_version" : self.updates[i]["renpy_version"],
|
||||
"pretty_version" : self.updates[i]["pretty_version"]
|
||||
}
|
||||
|
||||
with open(os.path.join(self.updatedir, "version.json"), "w") as f:
|
||||
json.dump(version_state, f)
|
||||
|
||||
if self.restart:
|
||||
self.state = self.DONE
|
||||
else:
|
||||
self.state = self.DONE_NO_RESTART
|
||||
|
||||
# Perform the update.
|
||||
self.new_state = dict(self.current_state)
|
||||
renpy.restart_interaction()
|
||||
|
||||
self.progress = 0.0
|
||||
self.state = self.PREPARING
|
||||
def zsync_update(self):
|
||||
|
||||
self.prompt_confirm()
|
||||
|
||||
if self.patch:
|
||||
for i in self.modules:
|
||||
@@ -735,6 +906,10 @@ init -1500 python in updater:
|
||||
fn = os.path.join(self.updatedir, "current.json")
|
||||
|
||||
if not os.path.exists(fn):
|
||||
if self.allow_empty:
|
||||
self.current_state = { }
|
||||
return
|
||||
|
||||
raise UpdateError(_("Either this project does not support updating, or the update status file was deleted."))
|
||||
|
||||
with open(fn, "r") as f:
|
||||
@@ -763,36 +938,106 @@ init -1500 python in updater:
|
||||
fn = os.path.join(self.updatedir, "updates.json")
|
||||
urlretrieve(self.url, fn)
|
||||
|
||||
with open(fn, "r") as f:
|
||||
self.updates = json.load(f)
|
||||
|
||||
with open(fn, "rb") as f:
|
||||
updates_json = f.read()
|
||||
|
||||
if self.public_key is not None:
|
||||
fn = os.path.join(self.updatedir, "updates.json.sig")
|
||||
urlretrieve(self.url + ".sig", fn)
|
||||
# Was updates.json verified?
|
||||
verified = False
|
||||
|
||||
with open(fn, "rb") as f:
|
||||
import codecs
|
||||
signature = codecs.decode(f.read(), "base64")
|
||||
# Does updates.json need to be verified?
|
||||
require_verified = False
|
||||
|
||||
# New-style ECDSA signature.
|
||||
key = os.path.join(config.basedir, "update", "key.pem")
|
||||
|
||||
if not os.path.exists(key):
|
||||
key = os.path.join(self.updatedir, "key.pem")
|
||||
|
||||
if os.path.exists(key):
|
||||
require_verified = True
|
||||
|
||||
self.log.write("Verifying with ECDSA.\n")
|
||||
|
||||
try:
|
||||
rsa.verify(updates_json, signature, self.public_key)
|
||||
except Exception:
|
||||
raise UpdateError(_("Could not verify update signature."))
|
||||
|
||||
if "monkeypatch" in self.updates:
|
||||
future.utils.exec_(self.updates["monkeypatch"], globals(), globals())
|
||||
import ecdsa
|
||||
verifying_key = ecdsa.VerifyingKey.from_pem(open(key, "rb").read())
|
||||
|
||||
url = urlparse.urljoin(self.url, "updates.ecdsa")
|
||||
f = urlopen(url)
|
||||
|
||||
while True:
|
||||
signature = f.read(64)
|
||||
if not signature:
|
||||
break
|
||||
|
||||
if verifying_key.verify(signature, updates_json):
|
||||
verified = True
|
||||
|
||||
self.log.write("Verified with ECDSA.\n")
|
||||
|
||||
except Exception:
|
||||
if self.log:
|
||||
import traceback
|
||||
traceback.print_exc(None, self.log)
|
||||
|
||||
# Old-style RSA signature.
|
||||
if self.public_key is not None:
|
||||
require_verified = True
|
||||
|
||||
self.log.write("Verifying with RSA.\n")
|
||||
|
||||
try:
|
||||
|
||||
fn = os.path.join(self.updatedir, "updates.json.sig")
|
||||
urlretrieve(self.url + ".sig", fn)
|
||||
|
||||
with open(fn, "rb") as f:
|
||||
import codecs
|
||||
signature = codecs.decode(f.read(), "base64")
|
||||
|
||||
rsa.verify(updates_json, signature, self.public_key)
|
||||
verified = True
|
||||
|
||||
self.log.write("Verified with RSA.\n")
|
||||
|
||||
except Exception:
|
||||
if self.log:
|
||||
import traceback
|
||||
traceback.print_exc(None, self.log)
|
||||
|
||||
if require_verified and not verified:
|
||||
raise UpdateError(_("Could not verify update signature."))
|
||||
|
||||
self.updates = json.loads(updates_json)
|
||||
|
||||
if verified and "monkeypatch" in self.updates:
|
||||
future.utils.exec_(self.updates["monkeypatch"], globals(), globals())
|
||||
|
||||
def add_dlc_state(self, name):
|
||||
url = urlparse.urljoin(self.url, self.updates[name]["json_url"])
|
||||
f = urlopen(url)
|
||||
d = json.load(f)
|
||||
|
||||
has_rpu = "rpu_url" in self.updates[name]
|
||||
has_zsync = "zsync_url" in self.updates[name]
|
||||
|
||||
prefer_rpu = self.prefer_rpu or "RPU_UPDATE" in os.environ
|
||||
|
||||
if has_rpu and has_zsync:
|
||||
if prefer_rpu:
|
||||
has_zsync = False
|
||||
else:
|
||||
has_rpu = False
|
||||
|
||||
if has_rpu:
|
||||
fl = self.fetch_files_rpu(name)
|
||||
d = { name : fl.to_current_json() }
|
||||
else:
|
||||
url = urlparse.urljoin(self.url, self.updates[name]["json_url"])
|
||||
f = urlopen(url)
|
||||
d = json.load(f)
|
||||
|
||||
d[name]["version"] = 0
|
||||
self.current_state.update(d)
|
||||
|
||||
|
||||
def check_versions(self):
|
||||
"""
|
||||
Decides what modules need to be updated, if any.
|
||||
@@ -1448,7 +1693,7 @@ init -1500 python in updater:
|
||||
return not not get_installed_packages(base)
|
||||
|
||||
|
||||
def update(url, base=None, force=False, public_key=None, simulate=None, add=[], restart=True, confirm=True, patch=True):
|
||||
def update(url, base=None, force=False, public_key=None, simulate=None, add=[], restart=True, confirm=True, patch=True, prefer_rpu=False, allow_empty=False):
|
||||
"""
|
||||
:doc: updater
|
||||
|
||||
@@ -1494,12 +1739,23 @@ init -1500 python in updater:
|
||||
changed data. If false, Ren'Py will download a complete copy of
|
||||
the game, and update from that. This is set to false automatically
|
||||
when the url does not begin with "http:".
|
||||
|
||||
This is ignored if the RPU update format is being used.
|
||||
|
||||
`prefer_rpu`
|
||||
If True, Ren'Py will prefer the RPU format for updates, if both
|
||||
zsync and RPU are available.
|
||||
|
||||
`allow_empty`
|
||||
If True, Ren'Py will allow the update to proceed even if the
|
||||
base directory does not contain update information. (`add` must
|
||||
be provided in this case.)
|
||||
"""
|
||||
|
||||
global installed_packages_cache
|
||||
installed_packages_cache = None
|
||||
|
||||
u = Updater(url=url, base=base, force=force, public_key=public_key, simulate=simulate, add=add, restart=restart, confirm=confirm, patch=patch)
|
||||
u = Updater(url=url, base=base, force=force, public_key=public_key, simulate=simulate, add=add, restart=restart, confirm=confirm, patch=patch, prefer_rpu=prefer_rpu, allow_empty=allow_empty)
|
||||
ui.timer(.1, repeat=True, action=renpy.restart_interaction)
|
||||
renpy.call_screen("updater", u=u)
|
||||
|
||||
|
||||
@@ -77,6 +77,13 @@ screen _developer:
|
||||
textbutton _("Speech Bubble Editor (Shift+B)"):
|
||||
action [ SetField(bubble.shown, "value", True), Hide("_developer") ]
|
||||
|
||||
if not renpy.get_screen("_filename_and_line"):
|
||||
textbutton _("Show Filename and Line"):
|
||||
action Show("_filename_and_line")
|
||||
else:
|
||||
textbutton _("Hide Filename and Line"):
|
||||
action Hide("_filename_and_line")
|
||||
|
||||
hbox:
|
||||
spacing gui._scale(25)
|
||||
|
||||
@@ -591,6 +598,50 @@ screen _translation_identifier():
|
||||
timer 10.0 action SetScreenVariable("show_help", False)
|
||||
|
||||
|
||||
screen _filename_and_line():
|
||||
|
||||
zorder 1500
|
||||
style_prefix ""
|
||||
|
||||
# Should help be shown?
|
||||
default help = True
|
||||
|
||||
# The filename and line to show.
|
||||
$ filename, line = renpy.get_filename_line()
|
||||
|
||||
timer 3 action SetScreenVariable("help", False)
|
||||
|
||||
drag:
|
||||
draggable True
|
||||
focus_mask None
|
||||
xpos 0
|
||||
ypos 0
|
||||
|
||||
frame:
|
||||
style "empty"
|
||||
background "#0004"
|
||||
xpadding 5
|
||||
ypadding 5
|
||||
xminimum 200
|
||||
|
||||
has vbox
|
||||
|
||||
textbutton "[filename]:[line]":
|
||||
padding (0, 0)
|
||||
text_color "#fff"
|
||||
text_hover_color "#bdf"
|
||||
text_size 14
|
||||
|
||||
action EditFile(filename, line)
|
||||
|
||||
if help:
|
||||
null height 10
|
||||
|
||||
text _("Click to open in editor."):
|
||||
size 14
|
||||
color "#fff"
|
||||
|
||||
|
||||
init 1000 python hide:
|
||||
if config.transparent_tile:
|
||||
tile = im.Tile("_transparent_tile.png", (config.screen_width, config.screen_height))
|
||||
|
||||
@@ -85,7 +85,7 @@ init label _errorhandling:
|
||||
slow_cps None
|
||||
slow_cps_multiplier 1.0
|
||||
slow_abortable False
|
||||
hinting "bytecode"
|
||||
hinting True
|
||||
adjust_spacing True
|
||||
emoji_font "TwemojiCOLRv0.ttf"
|
||||
prefer_emoji True
|
||||
|
||||
@@ -1398,6 +1398,12 @@ ex_rollback_classes = [ ]
|
||||
# Should we revert to the old behavior of box_reverse?
|
||||
simple_box_reverse = False
|
||||
|
||||
# A map from font name to the hinting for the font.
|
||||
font_hinting = { None : "auto" }
|
||||
|
||||
# Should we execute costly tasks which are
|
||||
# avoidable when not generating the documentation ?
|
||||
generating_documentation = False
|
||||
|
||||
del os
|
||||
del collections
|
||||
|
||||
@@ -2127,7 +2127,9 @@ class Bar(renpy.display.displayable.Displayable):
|
||||
|
||||
self.value = value
|
||||
adjustment = value.get_adjustment()
|
||||
renpy.game.interface.timeout(0)
|
||||
|
||||
if renpy.game.interface is not None:
|
||||
renpy.game.interface.timeout(0)
|
||||
|
||||
tooltip = value.get_tooltip()
|
||||
if tooltip is not None:
|
||||
|
||||
@@ -1937,8 +1937,6 @@ class Interface(object):
|
||||
|
||||
pygame.event.set_blocked(i)
|
||||
|
||||
pygame.event.set_blocked(pygame.TEXTINPUT)
|
||||
|
||||
# Fix a problem with fullscreen and maximized.
|
||||
if renpy.game.preferences.fullscreen:
|
||||
renpy.game.preferences.maximized = False
|
||||
@@ -2035,7 +2033,7 @@ class Interface(object):
|
||||
if i in renderers:
|
||||
gl2_renderers.append(i + "2")
|
||||
|
||||
if renpy.config.gl2:
|
||||
if renpy.config.gl2 or renpy.macintosh:
|
||||
renderers = gl2_renderers + renderers
|
||||
|
||||
# Prevent a performance warning if the renderer
|
||||
@@ -2133,7 +2131,6 @@ class Interface(object):
|
||||
# Stop the resizing.
|
||||
pygame.key.stop_text_input() # @UndefinedVariable
|
||||
pygame.key.set_text_input_rect(None) # @UndefinedVariable
|
||||
pygame.event.set_blocked(pygame.TEXTINPUT)
|
||||
self.text_rect = None
|
||||
self.old_text_rect = None
|
||||
self.display_reset = False
|
||||
@@ -2919,7 +2916,6 @@ class Interface(object):
|
||||
rect = (x0, y0, x1 - x0, y1 - y0)
|
||||
|
||||
pygame.key.set_text_input_rect(rect) # @UndefinedVariable
|
||||
pygame.event.set_allowed(pygame.TEXTINPUT)
|
||||
|
||||
if not self.old_text_rect or not_shown:
|
||||
pygame.key.start_text_input() # @UndefinedVariable
|
||||
@@ -2936,7 +2932,6 @@ class Interface(object):
|
||||
if self.old_text_rect:
|
||||
pygame.key.stop_text_input() # @UndefinedVariable
|
||||
pygame.key.set_text_input_rect(None) # @UndefinedVariable
|
||||
pygame.event.set_blocked(pygame.TEXTINPUT)
|
||||
|
||||
if self.touch_keyboard:
|
||||
renpy.exports.hide_screen('_touch_keyboard', layer='screens')
|
||||
|
||||
+78
-43
@@ -712,8 +712,9 @@ class Image(ImageBase):
|
||||
"""
|
||||
|
||||
is_svg = False
|
||||
dpi = 96
|
||||
|
||||
def __init__(self, filename, **properties):
|
||||
def __init__(self, filename, dpi=96, **properties):
|
||||
"""
|
||||
@param filename: The filename that the image will be loaded from.
|
||||
"""
|
||||
@@ -731,7 +732,10 @@ class Image(ImageBase):
|
||||
|
||||
super(Image, self).__init__(filename, **properties)
|
||||
self.filename = filename
|
||||
self.dpi = dpi
|
||||
|
||||
self.is_svg = filename.lower().endswith(".svg")
|
||||
self.pixel_perfect = self.is_svg
|
||||
|
||||
def _repr_info(self):
|
||||
return repr(self.filename)
|
||||
@@ -771,19 +775,23 @@ class Image(ImageBase):
|
||||
# avoid size-related exceptions (e.g. Crop on a smaller placeholder)
|
||||
surf = renpy.display.pgrender.transform_scale(surf, force_size)
|
||||
|
||||
self.is_svg = filename.lower().endswith(".svg")
|
||||
self.pixel_perfect = self.is_svg
|
||||
|
||||
if self.is_svg:
|
||||
width, height = surf.get_size()
|
||||
|
||||
width = int(width * renpy.display.draw.draw_per_virt)
|
||||
height = int(height * renpy.display.draw.draw_per_virt)
|
||||
width = int(width * renpy.display.draw.draw_per_virt * self.dpi / 96)
|
||||
height = int(height * renpy.display.draw.draw_per_virt * self.dpi / 96)
|
||||
|
||||
filelike = renpy.loader.load(self.filename, directory="images")
|
||||
if filename != self.filename:
|
||||
|
||||
with filelike as f:
|
||||
surf = renpy.display.pgrender.load_image(filelike, filename, size=(width, height))
|
||||
# This should only run for placeholder images.
|
||||
surf = renpy.display.pgrender.transform_scale(surf, (width, height))
|
||||
|
||||
else:
|
||||
|
||||
filelike = renpy.loader.load(self.filename, directory="images")
|
||||
|
||||
with filelike as f:
|
||||
surf = renpy.display.pgrender.load_image(filelike, filename, size=(width, height))
|
||||
|
||||
return surf
|
||||
|
||||
@@ -1012,8 +1020,9 @@ class FactorScale(ImageBase):
|
||||
|
||||
image logo doubled = im.FactorScale("logo.png", 1.5)
|
||||
|
||||
The same effect can now be achieved with the :tpref:`zoom` or the
|
||||
:tpref:`xzoom` and :tpref:`yzoom` transform properties.
|
||||
.. deprecated:: 7.4.0
|
||||
Use the :tpref:`zoom`, or the
|
||||
:tpref:`xzoom` and :tpref:`yzoom` transform properties.
|
||||
"""
|
||||
|
||||
def __init__(self, im, width, height=None, bilinear=True, **properties):
|
||||
@@ -1073,9 +1082,9 @@ class Flip(ImageBase):
|
||||
|
||||
image eileen flip = im.Flip("eileen_happy.png", vertical=True)
|
||||
|
||||
The same effect can now be achieved by setting
|
||||
:tpref:`xzoom` (for horizontal flip)
|
||||
or :tpref:`yzoom` (for vertical flip) to a negative value.
|
||||
.. deprecated:: 7.4.0
|
||||
Set :tpref:`xzoom` (for horizontal flip)
|
||||
or :tpref:`yzoom` (for vertical flip) to a negative value.
|
||||
"""
|
||||
|
||||
def __init__(self, im, horizontal=False, vertical=False, **properties):
|
||||
@@ -1165,7 +1174,8 @@ class Crop(ImageBase):
|
||||
|
||||
image logo crop = im.Crop("logo.png", (0, 0, 100, 307))
|
||||
|
||||
The same effect can now be achieved by setting the :tpref:`crop` transform property.
|
||||
.. deprecated:: 7.4.0
|
||||
Use the :tpref:`crop` transform property.
|
||||
"""
|
||||
|
||||
def __init__(self, im, x, y=None, w=None, h=None, **properties):
|
||||
@@ -1365,7 +1375,8 @@ class Blur(ImageBase):
|
||||
|
||||
image logo blurred = im.Blur("logo.png", 1.5)
|
||||
|
||||
The same effect can now be achieved with the :tpref:`blur` transform property.
|
||||
.. deprecated:: 7.4.0
|
||||
Use the :tpref:`blur` transform property.
|
||||
"""
|
||||
|
||||
def __init__(self, im, xrad, yrad=None, **properties):
|
||||
@@ -1427,6 +1438,10 @@ class MatrixColor(ImageBase):
|
||||
|
||||
The components of the transformed color are clamped to the
|
||||
range [0.0, 1.0].
|
||||
|
||||
.. deprecated:: 7.4.0
|
||||
Use ``Transform(im, matrixcolor=matrix, **properties)``.
|
||||
See :func:`Transform` and :tpref:`matrixcolor`.
|
||||
"""
|
||||
|
||||
def __init__(self, im, matrix, **properties):
|
||||
@@ -1475,6 +1490,9 @@ class matrix(tuple):
|
||||
`matrix` is a 20 or 25 element list or tuple. If it is 20 elements
|
||||
long, it is padded with (0, 0, 0, 0, 1) to make a 5x5 matrix,
|
||||
suitable for multiplication.
|
||||
|
||||
.. deprecated:: 7.4.0
|
||||
Use :class:`Matrix`.
|
||||
"""
|
||||
|
||||
def __new__(cls, *args):
|
||||
@@ -1567,8 +1585,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
Returns an identity matrix, one that does not change color or
|
||||
alpha.
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is IdentityMatrix().
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`IdentityMatrix() <IdentityMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
return matrix(1, 0, 0, 0, 0,
|
||||
@@ -1598,8 +1617,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
mostly sensitive to green, more of the green channel is
|
||||
kept then the other two channels.
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is SaturationMatrix(value, desat).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`SaturationMatrix(value, desat) <SaturationMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
r, g, b = desat
|
||||
@@ -1622,8 +1642,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
grayscale). This is equivalent to calling
|
||||
im.matrix.saturation(0).
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is SaturationMatrix(0).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`SaturationMatrix(0) <SaturationMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
return matrix.saturation(0.0)
|
||||
@@ -1641,8 +1662,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
the value of the red channel is 100, the transformed color
|
||||
will have a red value of 50.)
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is TintMatrix(Color((r, g, b))).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`TintMatrix(Color((r, g, b))) <TintMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
return matrix(r, 0, 0, 0, 0,
|
||||
@@ -1659,8 +1681,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
Returns an im.matrix that inverts the red, green, and blue
|
||||
channels of the image without changing the alpha channel.
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is InvertMatrix(1.0).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`InvertMatrix(1.0) <InvertMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
return matrix(-1, 0, 0, 0, 1,
|
||||
@@ -1681,8 +1704,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
a number between -1 and 1, with -1 the darkest possible
|
||||
image and 1 the brightest.
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is BrightnessMatrix(b).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`BrightnessMatrix(b) <BrightnessMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
return matrix(1, 0, 0, 0, b,
|
||||
@@ -1699,8 +1723,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
Returns an im.matrix that alters the opacity of an image. An
|
||||
`o` of 0.0 is fully transparent, while 1.0 is fully opaque.
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is OpacityMatrix(o).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`OpacityMatrix(o) <OpacityMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
return matrix(1, 0, 0, 0, 0,
|
||||
@@ -1718,8 +1743,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
be greater than 0.0, with values between 0.0 and 1.0 decreasing contrast, and
|
||||
values greater than 1.0 increasing contrast.
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is ContrastMatrix(c).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`ContrastMatrix(c) <ContrastMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
return matrix.brightness(-.5) * matrix.tint(c, c, c) * matrix.brightness(.5)
|
||||
@@ -1734,8 +1760,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
Returns an im.matrix that rotates the hue by `h` degrees, while
|
||||
preserving luminosity.
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is HueMatrix(h).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`HueMatrix(h) <HueMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
h = h * math.pi / 180
|
||||
@@ -1769,8 +1796,9 @@ im.matrix(%f, %f, %f, %f, %f.
|
||||
im.matrix.colorize("#f00", "#00f"))
|
||||
|
||||
|
||||
A suitable equivalent for the :tpref:`matrixcolor` transform property
|
||||
is ColorizeMatrix(black_color, white_color).
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`ColorizeMatrix(black_color, white_color) <ColorizeMatrix>`
|
||||
with the :tpref:`matrixcolor` transform property.
|
||||
"""
|
||||
|
||||
(r0, g0, b0, _a0) = renpy.easy.color(black_color) # type: ignore
|
||||
@@ -1797,8 +1825,9 @@ def Grayscale(im, desat=(0.2126, 0.7152, 0.0722), **properties):
|
||||
An image manipulator that creates a desaturated version of the image
|
||||
manipulator `im`.
|
||||
|
||||
The same effect can now be achieved by supplying SaturationMatrix(0)
|
||||
to the :tpref:`matrixcolor` transform property.
|
||||
.. deprecated:: 7.4.0
|
||||
Set the :tpref:`matrixcolor` transform property to
|
||||
:func:`SaturationMatrix(0) <SaturationMatrix>`.
|
||||
"""
|
||||
|
||||
return MatrixColor(im, matrix.saturation(0.0, desat), **properties)
|
||||
@@ -1812,8 +1841,9 @@ def Sepia(im, tint=(1.0, .94, .76), desat=(0.2126, 0.7152, 0.0722), **properties
|
||||
An image manipulator that creates a sepia-toned version of the image
|
||||
manipulator `im`.
|
||||
|
||||
The same effect can now be achieved by supplying SepiaMatrix()
|
||||
to the :tpref:`matrixcolor` transform property.
|
||||
.. deprecated:: 7.4.0
|
||||
Set the :tpref:`matrixcolor` transform property to
|
||||
:func:`SepiaMatrix() <SepiaMatrix>`
|
||||
"""
|
||||
|
||||
return MatrixColor(im, matrix.saturation(0.0, desat) * matrix.tint(tint[0], tint[1], tint[2]), **properties)
|
||||
@@ -1854,8 +1884,8 @@ class Tile(ImageBase):
|
||||
If not None, a (width, height) tuple. If None, this defaults to
|
||||
(:var:`config.screen_width`, :var:`config.screen_height`).
|
||||
|
||||
The same effect can now be achieved using the :func:`Tile`
|
||||
displayable, with ``Tile(im, size=size)``.
|
||||
.. deprecated:: 7.4.0
|
||||
Use :func:`Tile(im, xysize=size, **properties) <Tile>`.
|
||||
"""
|
||||
|
||||
def __init__(self, im, size=None, **properties):
|
||||
@@ -1951,7 +1981,7 @@ def image(arg, loose=False, **properties):
|
||||
"""
|
||||
:doc: im_image
|
||||
:name: Image
|
||||
:args: (filename, *, optimize_bounds=True, oversample=1, **properties)
|
||||
:args: (filename, *, optimize_bounds=True, oversample=1, dpi=96, **properties)
|
||||
|
||||
Loads an image from a file. `filename` is a
|
||||
string giving the name of the file.
|
||||
@@ -1970,6 +2000,11 @@ def image(arg, loose=False, **properties):
|
||||
with more pixels than its logical size would imply. For example, if
|
||||
an image file is 2048x2048 and oversample is 2, then the image will
|
||||
be treated as a 1024x1024 image for the purpose of layout.
|
||||
|
||||
`dpi`
|
||||
The DPI of an SVG image. This defaults to 96, but that can be
|
||||
increased to render the SVG larger, and decreased to render
|
||||
it smaller.
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
@@ -2424,6 +2424,7 @@ class NearRect(Container):
|
||||
class Layer(AdjustTimes):
|
||||
"""
|
||||
:doc: disp_layer
|
||||
:args: (layer, *, clipping=True, **properties)
|
||||
|
||||
This allows a layer to be shown as a displayable on another layer.
|
||||
Intended for use with detached layers.
|
||||
@@ -2436,6 +2437,9 @@ class Layer(AdjustTimes):
|
||||
`clipping`
|
||||
If False, the layer's contents may exceed its bounds, otherwise
|
||||
anything exceeding the bounds will be trimmed.
|
||||
|
||||
An entry in config.layer_clipping will cause this option to be
|
||||
ignored, and clipping to occur as specified by that config.
|
||||
"""
|
||||
|
||||
# Used to store layer_transitions when processing this layer.
|
||||
|
||||
@@ -901,7 +901,7 @@ class Transform(Container):
|
||||
self.active = True
|
||||
|
||||
if self.state.last_events != self.state.events:
|
||||
if self.state.events:
|
||||
if self.state.events and renpy.game.interface is not None:
|
||||
renpy.game.interface.timeout(0)
|
||||
self.state.last_events = self.state.events
|
||||
|
||||
@@ -1109,7 +1109,7 @@ class ATLTransform(renpy.atl.ATLTransformBase, Transform):
|
||||
self.active = True
|
||||
|
||||
if self.state.last_events != self.state.events:
|
||||
if self.state.events:
|
||||
if self.state.events and renpy.game.interface is not None:
|
||||
renpy.game.interface.timeout(0)
|
||||
self.state.last_events = self.state.events
|
||||
|
||||
|
||||
+43
-44
@@ -249,33 +249,43 @@ def get_movie_texture_web(channel, mask_channel, side_mask, mipmap):
|
||||
return tex, new
|
||||
|
||||
|
||||
def render_movie(channel, width, height, group=None):
|
||||
tex, _new = get_movie_texture(channel)
|
||||
def resize_movie(r, width, height):
|
||||
"""
|
||||
A utility function to resize a Render or texture to the given
|
||||
dimensions.
|
||||
"""
|
||||
|
||||
if group is not None:
|
||||
if tex is None:
|
||||
tex = group_texture.get(group, None)
|
||||
else:
|
||||
group_texture[group] = tex
|
||||
|
||||
if tex is None:
|
||||
if r is None:
|
||||
return None
|
||||
|
||||
sw, sh = tex.get_size()
|
||||
rv = renpy.display.render.Render(width, height)
|
||||
|
||||
scale = min(1.0 * width / sw, 1.0 * height / sh)
|
||||
sw, sh = r.get_size()
|
||||
|
||||
if not (sw and sh):
|
||||
return rv
|
||||
|
||||
scale = min(1.0 * width / sw, 1.0 * height / sh) # type: float
|
||||
|
||||
dw = scale * sw
|
||||
dh = scale * sh
|
||||
|
||||
rv = renpy.display.render.Render(width, height)
|
||||
rv.forward = renpy.display.matrix.Matrix2D(1.0 / scale, 0.0, 0.0, 1.0 / scale)
|
||||
rv.reverse = renpy.display.matrix.Matrix2D(scale, 0.0, 0.0, scale)
|
||||
rv.blit(tex, (int((width - dw) / 2), int((height - dh) / 2)))
|
||||
rv.blit(r, (int((width - dw) / 2), int((height - dh) / 2)))
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def render_movie(channel, width, height):
|
||||
"""
|
||||
Called from the Draw objects to render and scale a fullscreen movie.
|
||||
"""
|
||||
|
||||
tex, _new = get_movie_texture(channel)
|
||||
return resize_movie(tex, width, height)
|
||||
|
||||
|
||||
def default_play_callback(old, new): # @UnusedVariable
|
||||
|
||||
renpy.audio.music.play(new._play, channel=new.channel, loop=new.loop, synchro_start=True)
|
||||
@@ -539,42 +549,31 @@ class Movie(renpy.display.displayable.Displayable):
|
||||
|
||||
return rv
|
||||
|
||||
if self.size is None:
|
||||
|
||||
tex, _ = get_movie_texture(self.channel, self.mask_channel, self.side_mask, self.style.mipmap)
|
||||
|
||||
if self.group is not None:
|
||||
if tex is None:
|
||||
tex = group_texture.get(self.group, None)
|
||||
else:
|
||||
group_texture[self.group] = tex
|
||||
|
||||
if (not not_playing) and (tex is not None):
|
||||
width, height = tex.get_size()
|
||||
|
||||
rv = renpy.display.render.Render(width, height)
|
||||
rv.blit(tex, (0, 0))
|
||||
|
||||
elif (not not_playing) and (self.start_image is not None):
|
||||
surf = renpy.display.render.render(self.start_image, width, height, st, at)
|
||||
w, h = surf.get_size()
|
||||
rv = renpy.display.render.Render(w, h)
|
||||
rv.blit(surf, (0, 0))
|
||||
tex, _ = get_movie_texture(self.channel, self.mask_channel, self.side_mask, self.style.mipmap)
|
||||
|
||||
if self.group is not None:
|
||||
if tex is None:
|
||||
tex = group_texture.get(self.group, None)
|
||||
else:
|
||||
rv = renpy.display.render.Render(0, 0)
|
||||
group_texture[self.group] = tex
|
||||
|
||||
if (not not_playing) and (tex is not None):
|
||||
width, height = tex.get_size()
|
||||
|
||||
rv = renpy.display.render.Render(width, height)
|
||||
rv.blit(tex, (0, 0))
|
||||
|
||||
elif (not not_playing) and (self.start_image is not None):
|
||||
surf = renpy.display.render.render(self.start_image, width, height, st, at)
|
||||
w, h = surf.get_size()
|
||||
rv = renpy.display.render.Render(w, h)
|
||||
rv.blit(surf, (0, 0))
|
||||
|
||||
else:
|
||||
rv = renpy.display.render.Render(0, 0)
|
||||
|
||||
w, h = self.size
|
||||
|
||||
if not playing:
|
||||
rv = None
|
||||
else:
|
||||
rv = render_movie(self.channel, w, h, group=self.group)
|
||||
|
||||
if rv is None:
|
||||
rv = renpy.display.render.Render(w, h)
|
||||
if self.size is not None:
|
||||
rv = resize_movie(rv, self.size[0], self.size[1])
|
||||
|
||||
# Usually we get redrawn when the frame is ready - but we want
|
||||
# the movie to disappear if it's ended, or if it hasn't started
|
||||
|
||||
@@ -777,6 +777,8 @@ class Context(renpy.object.Object):
|
||||
rv.last_abnormal = self.last_abnormal
|
||||
rv.abnormal_stack = list(self.abnormal_stack)
|
||||
|
||||
rv.interacting = False
|
||||
|
||||
return rv
|
||||
|
||||
def predict_call(self, label, return_site):
|
||||
|
||||
+195
-3
@@ -107,6 +107,7 @@ from renpy.text.extras import check_text_tags
|
||||
|
||||
from renpy.memory import profile_memory, diff_memory, profile_rollback
|
||||
|
||||
from renpy.text.font import variable_font_info
|
||||
from renpy.text.textsupport import TAG as TEXT_TAG, TEXT as TEXT_TEXT, PARAGRAPH as TEXT_PARAGRAPH, DISPLAYABLE as TEXT_DISPLAYABLE
|
||||
|
||||
from renpy.execution import not_infinite_loop, reset_all_contexts
|
||||
@@ -122,6 +123,8 @@ from renpy.lint import try_compile, try_eval
|
||||
from renpy.gl2.gl2shadercache import register_shader
|
||||
from renpy.gl2.live2d import has_live2d
|
||||
|
||||
from renpy.bootstrap import get_alternate_base
|
||||
|
||||
renpy_pure("ParameterizedText")
|
||||
renpy_pure("Keymap")
|
||||
renpy_pure("has_screen")
|
||||
@@ -849,7 +852,7 @@ def web_input(prompt, default='', allow=None, exclude='{}', length=None, mask=Fa
|
||||
if roll_forward is not None:
|
||||
default = roll_forward
|
||||
|
||||
wi = renpy.display.behavior.WebInput(prompt, default, length=length, allow=allow, exclude=exclude, mask=mask)
|
||||
wi = renpy.display.behavior.WebInput(substitute(prompt), default, length=length, allow=allow, exclude=exclude, mask=mask)
|
||||
renpy.ui.add(wi)
|
||||
|
||||
renpy.exports.shown_window()
|
||||
@@ -2756,9 +2759,9 @@ def iconify():
|
||||
|
||||
# New context stuff.
|
||||
call_in_new_context = renpy.game.call_in_new_context
|
||||
curried_call_in_new_context = renpy.curry.curry(renpy.game.call_in_new_context)
|
||||
curried_call_in_new_context = curry(call_in_new_context)
|
||||
invoke_in_new_context = renpy.game.invoke_in_new_context
|
||||
curried_invoke_in_new_context = renpy.curry.curry(renpy.game.invoke_in_new_context)
|
||||
curried_invoke_in_new_context = curry(invoke_in_new_context)
|
||||
call_replay = renpy.game.call_replay
|
||||
|
||||
renpy_pure("curried_call_in_new_context")
|
||||
@@ -4595,3 +4598,192 @@ def confirm(message):
|
||||
Return = renpy.store.Return
|
||||
renpy.store.layout.yesno_screen(message, yes=Return(True), no=Return(False))
|
||||
return renpy.ui.interact()
|
||||
|
||||
|
||||
class FetchError(Exception):
|
||||
"""
|
||||
:undocumented:
|
||||
|
||||
The type of errors raised by :func:`renpy.fetch`.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def fetch_requests(url, method, data, content_type, timeout):
|
||||
"""
|
||||
:undocumented:
|
||||
|
||||
Used by fetch on non-emscripten systems.
|
||||
|
||||
Returns either a bytes object, or a FetchError.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import requests
|
||||
|
||||
# Because we don't have nonlocal yet.
|
||||
resp = [ None ]
|
||||
|
||||
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.raise_for_status()
|
||||
resp[0] = r.content # type: ignore
|
||||
except Exception as e:
|
||||
resp[0] = FetchError(str(e)) # type: ignore
|
||||
|
||||
t = threading.Thread(target=make_request)
|
||||
t.start()
|
||||
|
||||
while resp[0] is None:
|
||||
renpy.exports.pause(0)
|
||||
|
||||
t.join()
|
||||
|
||||
return resp[0]
|
||||
|
||||
|
||||
def fetch_emscripten(url, method, data, content_type, timeout):
|
||||
"""
|
||||
:undocumented:
|
||||
|
||||
Used by fetch on emscripten systems.
|
||||
|
||||
Returns either a bytes object, or a FetchError.
|
||||
"""
|
||||
|
||||
import emscripten
|
||||
import time
|
||||
import os
|
||||
|
||||
fn = "/req-" + str(time.time()) + ".data"
|
||||
|
||||
with open(fn, "wb") as f:
|
||||
if data is not None:
|
||||
f.write(data)
|
||||
|
||||
url = url.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)
|
||||
else:
|
||||
command = """fetchFile("{method}", "{url}", "{fn}", "{fn}", "{content_type}")""".format( method=method, url=url, fn=fn, content_type=content_type)
|
||||
|
||||
fetch_id = emscripten.run_script_int(command)
|
||||
|
||||
status = "PENDING"
|
||||
message = "Pending."
|
||||
|
||||
start = time.time()
|
||||
while start - time.time() < timeout:
|
||||
renpy.exports.pause(0)
|
||||
|
||||
result = emscripten.run_script_string("""fetchFileResult({})""".format(fetch_id))
|
||||
status, _ignored, message = result.partition(" ")
|
||||
|
||||
if status != "PENDING":
|
||||
break
|
||||
|
||||
try:
|
||||
|
||||
if status == "OK":
|
||||
with open(fn, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
else:
|
||||
return FetchError(message)
|
||||
|
||||
finally:
|
||||
os.unlink(fn)
|
||||
|
||||
|
||||
|
||||
def fetch(url, method=None, data=None, json=None, content_type=None, timeout=5, result="bytes"):
|
||||
"""
|
||||
:doc: fetch
|
||||
|
||||
This performs an HTTP (or HTTPS) request to the given URL, and returns
|
||||
the content of that request. If it fails, raises a FetchError exception,
|
||||
with text that describes the failure. (But may not be suitable for
|
||||
presentation to the user.)
|
||||
|
||||
`url`
|
||||
The URL to fetch.
|
||||
|
||||
`method`
|
||||
The method to use. Generally one of "GET", "POST", or "PUT", but other
|
||||
HTTP methdos are possible. If `data` or `json` are not None, defaults to
|
||||
"POST", otherwise defaults to GET.
|
||||
|
||||
`data`
|
||||
If not None, a byte string of data to send with the request.
|
||||
|
||||
`json`
|
||||
If not None, a JSON object to send with the request. This takes precendence
|
||||
over `data`.
|
||||
|
||||
`content_type`
|
||||
The content type of the data. If not given, defaults to "application/json"
|
||||
if `json` is not None, or "application/octet-stream" otherwise. Only
|
||||
used on a POST or PUT request.
|
||||
|
||||
`timeout`
|
||||
The number of seconds to wait for the request to complete.
|
||||
|
||||
`result`
|
||||
How to process the result. If "bytes", returns the raw bytes of the result.
|
||||
If "text", decodes the result using UTF-8 and returns a unicode string. If "json",
|
||||
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.
|
||||
|
||||
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
|
||||
by CORS.
|
||||
"""
|
||||
|
||||
import json as _json
|
||||
|
||||
|
||||
if data is not None and json is not None:
|
||||
raise FetchError("data and json arguments are mutually exclusive.")
|
||||
|
||||
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 method is None:
|
||||
if data is not None or json is not None:
|
||||
method = "POST"
|
||||
else:
|
||||
method = "GET"
|
||||
|
||||
if content_type is None:
|
||||
if json is not None:
|
||||
content_type = "application/json"
|
||||
else:
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
if json is not None:
|
||||
data = _json.dumps(json).encode("utf-8")
|
||||
|
||||
if renpy.emscripten:
|
||||
content = fetch_emscripten(url, method, data, content_type, timeout)
|
||||
else:
|
||||
content = fetch_requests(url, method, data, content_type, timeout)
|
||||
|
||||
if isinstance(content, Exception):
|
||||
raise content # type: ignore
|
||||
|
||||
if result == "bytes":
|
||||
return content
|
||||
elif result == "text":
|
||||
return content.decode("utf-8")
|
||||
elif result == "json":
|
||||
return _json.loads(content)
|
||||
|
||||
@@ -661,6 +661,9 @@ class Live2D(renpy.display.displayable.Displayable):
|
||||
|
||||
def _choose_attributes(self, tag, attributes, optional):
|
||||
|
||||
# Filter out _sustain.
|
||||
attributes = [ i for i in attributes if i != "_sustain" ]
|
||||
|
||||
common = self.common
|
||||
|
||||
# Chose all motions.
|
||||
|
||||
+4
-1
@@ -866,7 +866,7 @@ class Lexer(object):
|
||||
|
||||
def string(self):
|
||||
"""
|
||||
Lexes a string, and returns the string to the user, or None if
|
||||
Lexes a non-triple-quoted string, and returns the string to the user, or None if
|
||||
no string could be found. This also takes care of expanding
|
||||
escapes and collapsing whitespace.
|
||||
|
||||
@@ -928,6 +928,9 @@ class Lexer(object):
|
||||
This is about the same as the double-quoted strings, except that
|
||||
runs of whitespace with multiple newlines are turned into a single
|
||||
newline.
|
||||
|
||||
Except in the case of a raw string where this returns a simple string,
|
||||
this returns a list of strings.
|
||||
"""
|
||||
|
||||
s = self.match(r'r?"""([^\\"]|\\.|"(?!""))*"""')
|
||||
|
||||
+89
-18
@@ -692,8 +692,49 @@ def check_style(name, s):
|
||||
check_style_property_displayable(name, k, v)
|
||||
|
||||
|
||||
def check_parameters(kind, node_name, parameter_info):
|
||||
"""
|
||||
`kind`
|
||||
What we're parsing the parameters of, for the error message.
|
||||
"screen", "label", "function", "ATL transform"...
|
||||
|
||||
`node_name`
|
||||
The name of the (kind) we're defining.
|
||||
|
||||
`parameter_info`
|
||||
The ParameterInfo we're scanning, or None.
|
||||
"""
|
||||
|
||||
if parameter_info is None:
|
||||
return
|
||||
|
||||
names = [p[0] for p in parameter_info.parameters]
|
||||
if parameter_info.extrakw:
|
||||
names.extend(parameter_info.extrakw)
|
||||
if parameter_info.extrapos:
|
||||
names.extend(parameter_info.extrapos)
|
||||
|
||||
rv = [name for name in names if (name in python_builtins) or (name in renpy_builtins)]
|
||||
|
||||
if len(rv) == 1:
|
||||
name = rv[0]
|
||||
report("In {0} {1!r}, the {2!r} parameter replaces a built-in name from either Python or Ren'Py, which may cause problems.".format(kind, node_name, name))
|
||||
if not "_" in name:
|
||||
add("This can be fixed by naming it '{}_'".format(name))
|
||||
elif rv:
|
||||
last = rv.pop()
|
||||
prettyprevious = ", ".join(repr(name) for name in rv)
|
||||
report("In {0} {1!r}, the {2} and {3!r} parameters replace built-in names from either Python or Ren'Py, which may cause problems.".format(kind,
|
||||
node_name,
|
||||
prettyprevious,
|
||||
last))
|
||||
|
||||
|
||||
def check_label(node):
|
||||
|
||||
if args.builtins_parameters:
|
||||
check_parameters("label", node.name, node.parameters)
|
||||
|
||||
def add_arg(n):
|
||||
if n is None:
|
||||
return
|
||||
@@ -718,6 +759,9 @@ def check_screen(node):
|
||||
report("The screen %s has not been given a parameter list.", node.screen.name)
|
||||
add("This can be fixed by writing 'screen %s():' instead.", node.screen.name)
|
||||
|
||||
if args.builtins_parameters:
|
||||
check_parameters("screen", node.screen.name, node.screen.parameters)
|
||||
|
||||
|
||||
def check_styles():
|
||||
for full_name, s in renpy.style.styles.items(): # @UndefinedVariable
|
||||
@@ -733,6 +777,11 @@ def check_init(node):
|
||||
report("The init priority ({}) is not in the -999 to 999 range.".format(node.priority))
|
||||
|
||||
|
||||
def check_transform(node):
|
||||
if args.builtins_parameters:
|
||||
check_parameters("ATL transform", node.varname, node.parameters)
|
||||
|
||||
|
||||
def humanize(n):
|
||||
s = str(n)
|
||||
|
||||
@@ -783,6 +832,9 @@ class Count(object):
|
||||
self.words += len(s.split())
|
||||
self.characters += len(s)
|
||||
|
||||
def tuple(self):
|
||||
return (self.blocks, self.words, self.characters)
|
||||
|
||||
|
||||
def common(n):
|
||||
"""
|
||||
@@ -803,22 +855,33 @@ def report_character_stats(charastats):
|
||||
|
||||
rv = [ "Character statistics (for default language):" ]
|
||||
|
||||
count_to_char = collections.defaultdict(list)
|
||||
|
||||
for char, count in charastats.items():
|
||||
count_to_char[count].append(char)
|
||||
|
||||
for count, chars in sorted(count_to_char.items(), reverse=True):
|
||||
chars.sort()
|
||||
|
||||
start = humanize_listing(chars, singular_suffix=" has ", plural_suffix=" have ")
|
||||
|
||||
rv.append(
|
||||
" * " + start + humanize(count) +
|
||||
(" block " if count == 1 else " blocks ") + "of dialogue" +
|
||||
(" each." if len(chars) > 1 else ".")
|
||||
if args.words_char_count:
|
||||
for char in sorted(charastats, key=lambda char: charastats[char].tuple(), reverse=True):
|
||||
count = charastats[char]
|
||||
rv.append(
|
||||
" * " + char
|
||||
+ " has " + humanize(count.blocks) + (" block " if count.blocks == 1 else " blocks ") + "of dialogue, "
|
||||
+ "containing " + humanize(count.words) + " words and "
|
||||
+ humanize(count.characters) + " characters."
|
||||
)
|
||||
|
||||
else:
|
||||
nblocks_to_char = collections.defaultdict(list)
|
||||
|
||||
for char, count in charastats.items():
|
||||
nblocks_to_char[count.blocks].append(char)
|
||||
|
||||
for nblocks, chars in sorted(nblocks_to_char.items(), reverse=True):
|
||||
chars.sort()
|
||||
|
||||
start = humanize_listing(chars, singular_suffix=" has ", plural_suffix=" have ")
|
||||
|
||||
rv.append(
|
||||
" * " + start + humanize(nblocks) +
|
||||
(" block " if nblocks == 1 else " blocks ") + "of dialogue" +
|
||||
(" each." if len(chars) > 1 else ".")
|
||||
)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
@@ -970,7 +1033,11 @@ def lint():
|
||||
ap = renpy.arguments.ArgumentParser(description="Checks the script for errors and prints script statistics.", require_command=False)
|
||||
ap.add_argument("filename", nargs='?', action="store", help="The file to write to.")
|
||||
ap.add_argument("--error-code", action="store_true", help="If given, the error code is 0 if the game has no lint errros, 1 if lint errors are found.")
|
||||
ap.add_argument("--orphan-tl", action="store_true", help="If given, orphan translations are reported.")
|
||||
ap.add_argument("--builtins-parameters", action="store_true", help="If given, renpy or python builtin names in renpy statement parameters are reported.")
|
||||
ap.add_argument("--words-char-count", action="store_true", help="If given, the number of words and characters for each character is reported.")
|
||||
|
||||
global args
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.filename:
|
||||
@@ -1003,7 +1070,7 @@ def lint():
|
||||
# The current count.
|
||||
counts = collections.defaultdict(Count)
|
||||
|
||||
charastats = collections.defaultdict(int)
|
||||
charastats = collections.defaultdict(Count)
|
||||
|
||||
# The current language.
|
||||
language = None
|
||||
@@ -1049,7 +1116,7 @@ def lint():
|
||||
|
||||
counts[language].add(node.what)
|
||||
if language is None:
|
||||
charastats[node.who or 'narrator' ] += 1
|
||||
charastats[node.who or 'narrator'].add(node.what)
|
||||
|
||||
elif isinstance(node, renpy.ast.Menu):
|
||||
check_menu(node)
|
||||
@@ -1073,7 +1140,7 @@ def lint():
|
||||
elif isinstance(node, renpy.ast.Label):
|
||||
check_label(node)
|
||||
|
||||
elif isinstance(node, renpy.ast.Translate):
|
||||
elif isinstance(node, renpy.ast.Translate) and args.orphan_tl:
|
||||
language = node.language
|
||||
if language is None:
|
||||
none_language_ids.add(node.identifier)
|
||||
@@ -1098,12 +1165,16 @@ def lint():
|
||||
elif isinstance(node, renpy.ast.Init):
|
||||
check_init(node)
|
||||
|
||||
elif isinstance(node, renpy.ast.Transform):
|
||||
check_transform(node)
|
||||
|
||||
report_node = None
|
||||
|
||||
check_styles()
|
||||
check_filename_encodings()
|
||||
check_unreachables(all_stmts)
|
||||
check_orphan_translations(none_language_ids, translated_ids)
|
||||
if args.orphan_tl:
|
||||
check_orphan_translations(none_language_ids, translated_ids)
|
||||
|
||||
for f in renpy.config.lint_hooks:
|
||||
f()
|
||||
|
||||
+21
-24
@@ -26,6 +26,7 @@ from typing import Optional
|
||||
|
||||
import renpy
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import types
|
||||
import threading
|
||||
@@ -67,34 +68,20 @@ def get_path(fn):
|
||||
|
||||
# Asset Loading
|
||||
|
||||
apks = [ ]
|
||||
game_apks = [ ]
|
||||
|
||||
if renpy.android:
|
||||
import android.apk # type: ignore
|
||||
|
||||
expansion = os.environ.get("ANDROID_EXPANSION", None)
|
||||
if expansion is not None:
|
||||
print("Using expansion file", expansion)
|
||||
if renpy.config.renpy_base == renpy.config.basedir:
|
||||
# Read the game data from the APK.
|
||||
|
||||
apks = [
|
||||
android.apk.APK(apk=expansion, prefix='assets/x-game/'),
|
||||
android.apk.APK(apk=expansion, prefix='assets/x-renpy/x-common/'),
|
||||
]
|
||||
apks.append(android.apk.APK(prefix='assets/x-game/'))
|
||||
game_apks.append(apks[0])
|
||||
|
||||
game_apks = [ apks[0] ]
|
||||
apks.append(android.apk.APK(prefix='assets/x-renpy/x-common/'))
|
||||
|
||||
else:
|
||||
print("Not using expansion file.")
|
||||
|
||||
apks = [
|
||||
android.apk.APK(prefix='assets/x-game/'),
|
||||
android.apk.APK(prefix='assets/x-renpy/x-common/'),
|
||||
]
|
||||
|
||||
game_apks = [ apks[0] ]
|
||||
|
||||
else:
|
||||
apks = [ ]
|
||||
game_apks = [ ]
|
||||
|
||||
# Files on disk should be checked before archives. Otherwise, among
|
||||
# other things, using a new version of bytecode.rpyb will break.
|
||||
@@ -1003,7 +990,11 @@ class RenpyImporter(object):
|
||||
mod.__name__ = pyname
|
||||
mod.__file__ = renpy.config.gamedir + "/" + filename
|
||||
mod.__loader__ = self
|
||||
mod.__package__ = pystr(fullname.rpartition(".")[0])
|
||||
|
||||
if filename.endswith("__init__.py"):
|
||||
mod.__package__ = pystr(fullname)
|
||||
else:
|
||||
mod.__package__ = pystr(fullname.rpartition(".")[0])
|
||||
|
||||
if mod.__file__.endswith("__init__.py"):
|
||||
mod.__path__ = [ mod.__file__[:-len("__init__.py")] ]
|
||||
@@ -1044,8 +1035,14 @@ class RenpyImporter(object):
|
||||
return self.load_module(fullname, "get_code")
|
||||
|
||||
def get_data(self, filename):
|
||||
if filename.startswith(renpy.config.gamedir + "/"):
|
||||
filename = filename[len(renpy.config.gamedir) + 1:]
|
||||
|
||||
filename = os.path.normpath(filename).replace('\\', '/')
|
||||
|
||||
_check_prefix = "{0}/".format(
|
||||
os.path.normpath(renpy.config.gamedir).replace('\\', '/')
|
||||
)
|
||||
if filename.startswith(_check_prefix):
|
||||
filename = filename[len(_check_prefix):]
|
||||
|
||||
return load(filename).read()
|
||||
|
||||
|
||||
@@ -239,6 +239,7 @@ class StdioRedirector(object):
|
||||
def __init__(self):
|
||||
self.buffer = ''
|
||||
self.log = open("log", developer=False, append=False, flush=True)
|
||||
self.encoding = "utf-8"
|
||||
|
||||
def write(self, s):
|
||||
|
||||
|
||||
+3
-2
@@ -252,8 +252,9 @@ def choose_variants():
|
||||
renpy.config.variants.insert(0, 'web') # type: ignore
|
||||
|
||||
# mobile
|
||||
userAgent = emscripten.run_script_string(r'''navigator.userAgent''')
|
||||
mobile = re.search('Mobile|Android|iPad|iPhone', userAgent)
|
||||
mobile = emscripten.run_script_int(
|
||||
r'''/Mobile|Android|iPad|iPhone/.test(navigator.userAgent)
|
||||
|| (navigator.userAgent.indexOf("Mac") != -1 && navigator.maxTouchPoints > 1)''')
|
||||
if mobile:
|
||||
renpy.config.variants.insert(0, 'mobile') # type: ignore
|
||||
# Reserve android/ios for when the OS API is exposed
|
||||
|
||||
@@ -137,7 +137,7 @@ Preference("self_voicing_volume_drop", 0.5)
|
||||
Preference("emphasize_audio", False)
|
||||
|
||||
# Is the gamepad enabled?
|
||||
Preference("pad_enabled", True)
|
||||
Preference("pad_enabled", True, (bool, str))
|
||||
|
||||
# The side of the screen used for rollback. ("left", "right", or "disable")
|
||||
Preference("mobile_rollback_side", "disable")
|
||||
|
||||
@@ -206,6 +206,9 @@ def create_store(name):
|
||||
Creates the store with `name`.
|
||||
"""
|
||||
|
||||
if name == "store.store":
|
||||
raise NameError('Namespaces may not begin with "store".')
|
||||
|
||||
parent, _, var = name.rpartition('.')
|
||||
|
||||
if parent:
|
||||
|
||||
+9
-7
@@ -236,6 +236,7 @@ class Rollback(renpy.object.Object):
|
||||
|
||||
identifier = None
|
||||
not_greedy = False
|
||||
checkpointing_suspended = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -269,6 +270,9 @@ class Rollback(renpy.object.Object):
|
||||
# rollbacks that occur in greedy mode.
|
||||
self.not_greedy = False
|
||||
|
||||
# The value of checkpointing_suspended when this checkpoint was created.
|
||||
self.checkpointing_suspended = renpy.game.log.checkpointing_suspended
|
||||
|
||||
# A unique identifier for this rollback object.
|
||||
|
||||
global serial
|
||||
@@ -418,6 +422,7 @@ class Rollback(renpy.object.Object):
|
||||
"""
|
||||
|
||||
renpy.game.contexts = renpy.game.contexts[:-1] + [ self.context ]
|
||||
renpy.game.log.checkpointing_suspended = self.checkpointing_suspended
|
||||
|
||||
|
||||
class RollbackLog(renpy.object.Object):
|
||||
@@ -709,13 +714,11 @@ class RollbackLog(renpy.object.Object):
|
||||
|
||||
if self.checkpointing_suspended:
|
||||
hard = False
|
||||
self.current.not_greedy = True
|
||||
|
||||
if hard:
|
||||
self.retain_after_load_flag = False
|
||||
|
||||
if self.current.checkpoint:
|
||||
return
|
||||
|
||||
if not renpy.game.context().rollback:
|
||||
return
|
||||
|
||||
@@ -876,9 +879,6 @@ class RollbackLog(renpy.object.Object):
|
||||
if checkpoints and (self.rollback_limit <= 0) and (not force):
|
||||
return
|
||||
|
||||
self.suspend_checkpointing(False)
|
||||
# will always rollback to before suspension
|
||||
|
||||
self.purge_unreachable(self.get_roots())
|
||||
|
||||
revlog = [ ]
|
||||
@@ -1104,7 +1104,9 @@ class RollbackLog(renpy.object.Object):
|
||||
|
||||
# Now, rollback to an acceptable point.
|
||||
|
||||
greedy = renpy.session.pop("_greedy_rollback", True)
|
||||
greedy = getattr(renpy.store, "_greedy_rollback", True)
|
||||
greedy = renpy.session.pop("_greedy_rollback", greedy)
|
||||
|
||||
self.rollback(0, force=True, label=label, greedy=greedy, on_load=True)
|
||||
|
||||
# Because of the rollback, we never make it this far.
|
||||
|
||||
@@ -2410,6 +2410,8 @@ class SLCustomUse(SLNode):
|
||||
# If we have any children, pass them to (possible) transclude
|
||||
if self.block.children:
|
||||
ctx.transclude = self.block
|
||||
else:
|
||||
ctx.transclude = None
|
||||
|
||||
try:
|
||||
ast.execute(ctx)
|
||||
|
||||
+32
-4
@@ -52,7 +52,8 @@ STYLE_PREFIXES = [
|
||||
parser = None
|
||||
|
||||
# The names of all statements known to SL.
|
||||
statement_names = set()
|
||||
# a map of "parser_name": Parser
|
||||
statements = dict()
|
||||
|
||||
# A list of statements that are valid anywhere a child can be placed.
|
||||
all_child_statements = [ ]
|
||||
@@ -153,7 +154,7 @@ class Parser(object):
|
||||
self.keyword = { }
|
||||
self.children = { }
|
||||
|
||||
statement_names.add(name)
|
||||
statements[name] = self
|
||||
|
||||
# True if this parser takes "as".
|
||||
self.variable = False
|
||||
@@ -277,7 +278,7 @@ class Parser(object):
|
||||
if name not in self.keyword:
|
||||
if name == "continue" or name == "break":
|
||||
l.error("The %s statement may only appear inside a for statement, or an if statement inside a for statement." % name)
|
||||
elif name in statement_names:
|
||||
elif name in statements:
|
||||
l.error('The %s statement is not a valid child of the %s statement.' % (name, self.name))
|
||||
else:
|
||||
l.error('%r is not a keyword argument or valid child of the %s statement.' % (name, self.name))
|
||||
@@ -447,6 +448,29 @@ class Parser(object):
|
||||
PrefixStyle(prefix, prop.name)
|
||||
|
||||
return self
|
||||
|
||||
def copy_properties(self, name):
|
||||
global parser
|
||||
parser = self
|
||||
|
||||
parser_to_copy = statements.get(name, None)
|
||||
if parser_to_copy is None:
|
||||
raise Exception("{!r} is not a known screen statement".format(name))
|
||||
|
||||
for p in parser_to_copy.positional:
|
||||
Positional(p.name)
|
||||
|
||||
for v in set(parser_to_copy.keyword.values()):
|
||||
if isinstance(v, Keyword):
|
||||
Keyword(v.name)
|
||||
|
||||
elif isinstance(v, Style):
|
||||
Style(v.name)
|
||||
|
||||
elif isinstance(v, PrefixStyle):
|
||||
PrefixStyle(v.prefix, v.name)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def add(thing):
|
||||
@@ -554,6 +578,10 @@ def register_sl_displayable(*args, **kwargs):
|
||||
|
||||
These correspond to groups of :doc:`style_properties`. Group can
|
||||
also be "ui", in which case it adds the :ref:`common ui properties <common-properties>`.
|
||||
|
||||
.. method:: copy_properties(name)
|
||||
|
||||
Adds all styles and positional/keyword arguments that can be passed to the `name` screen statement.
|
||||
"""
|
||||
|
||||
kwargs.setdefault("unique", False)
|
||||
@@ -1115,4 +1143,4 @@ def parse_screen(l, loc):
|
||||
Parses the screen statement.
|
||||
"""
|
||||
|
||||
return screen_parser.parse(loc, l, None)
|
||||
return screen_parser.parse(loc, l, None)
|
||||
@@ -108,6 +108,8 @@ text_property_names = [
|
||||
"hinting",
|
||||
"adjust_spacing",
|
||||
"mipmap",
|
||||
"axis",
|
||||
"instance",
|
||||
]
|
||||
|
||||
text_properties = [ Style(i) for i in text_property_names ]
|
||||
|
||||
+11
-2
@@ -269,10 +269,19 @@ 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"])
|
||||
|
||||
if scope is not None:
|
||||
kwargs = MultipleDict(scope, renpy.store.__dict__) # @UndefinedVariable
|
||||
dicts.insert(0, scope)
|
||||
|
||||
if dicts:
|
||||
kwargs = MultipleDict(*dicts)
|
||||
else:
|
||||
kwargs = renpy.store.__dict__ # @UndefinedVariable
|
||||
kwargs = dicts[0]
|
||||
|
||||
try:
|
||||
s = formatter.vformat(s, (), kwargs) # type: ignore
|
||||
|
||||
+251
-227
File diff suppressed because one or more lines are too long
@@ -37,7 +37,9 @@ text_tags = dict(
|
||||
alpha=True,
|
||||
alt=True,
|
||||
art=True,
|
||||
axis=True,
|
||||
done=False,
|
||||
instance=True,
|
||||
image=False,
|
||||
p=False,
|
||||
w=False,
|
||||
@@ -100,6 +102,9 @@ def check_text_tags(s):
|
||||
continue
|
||||
|
||||
# Strip off arguments for tags.
|
||||
text = text.partition('=')[0]
|
||||
text = text.partition(':')[0]
|
||||
|
||||
if text.find('=') != -1:
|
||||
text = text[:text.find('=')]
|
||||
|
||||
|
||||
+51
-4
@@ -696,7 +696,7 @@ font_cache = { }
|
||||
last_scale = 1.0
|
||||
|
||||
|
||||
def get_font(fn, size, bold, italics, outline, antialias, vertical, hinting, scale, shaper):
|
||||
def get_font(fn, size, bold, italics, outline, antialias, vertical, hinting, scale, shaper, instance, axis):
|
||||
|
||||
if hbfont is None:
|
||||
shaper = "freetype"
|
||||
@@ -729,17 +729,23 @@ def get_font(fn, size, bold, italics, outline, antialias, vertical, hinting, sca
|
||||
return rv
|
||||
|
||||
# Check for a cached TTF.
|
||||
key = (fn, size, bold, italics, outline, antialias, vertical, hinting, scale, shaper)
|
||||
key = (fn, size, bold, italics, outline, antialias, vertical, hinting, scale, shaper, instance, None if axis is None else tuple(sorted(axis.items())))
|
||||
|
||||
rv = font_cache.get(key, None)
|
||||
if rv is not None:
|
||||
return rv
|
||||
|
||||
if hinting is True:
|
||||
hinting = renpy.config.font_hinting.get(fn, True)
|
||||
|
||||
if hinting is True:
|
||||
hinting = renpy.config.font_hinting.get(None, "auto")
|
||||
|
||||
# Load a TTF.
|
||||
face = load_face(fn, shaper)
|
||||
|
||||
if shaper == "harfbuzz":
|
||||
rv = hbfont.HBFont(face, int(size * scale), bold, italics, outline, antialias, vertical, hinting) # @UndefinedVariable
|
||||
rv = hbfont.HBFont(face, int(size * scale), bold, italics, outline, antialias, vertical, hinting, instance, axis) # @UndefinedVariable
|
||||
else:
|
||||
rv = ftfont.FTFont(face, int(size * scale), bold, italics, outline, antialias, vertical, hinting) # @UndefinedVariable
|
||||
|
||||
@@ -762,7 +768,48 @@ def load_fonts():
|
||||
i.load()
|
||||
|
||||
for i in renpy.config.preload_fonts:
|
||||
load_face(i)
|
||||
load_face(i, "harfbuzz")
|
||||
|
||||
|
||||
def variable_font_info(font):
|
||||
"""
|
||||
:doc: variable_fonts
|
||||
|
||||
Returns information about a variable font, or None if the font is not
|
||||
variable.
|
||||
|
||||
`font`
|
||||
The filename containing the font.
|
||||
|
||||
The return value is an object with the following fields:
|
||||
|
||||
`instance`
|
||||
A dictionary where the keys are the names of the named instances
|
||||
of the font. (For example, 'light', 'regular', 'bold', and 'heavy'.)
|
||||
The values can be ignored.
|
||||
|
||||
`axis`
|
||||
A dictionary that maps the names of the axes of the font to objects
|
||||
with the following fields:
|
||||
|
||||
`minimum`
|
||||
The minimum value of the axis.
|
||||
`default`
|
||||
The default value of the axis.
|
||||
`maximum`
|
||||
The maximum value of the axis.
|
||||
|
||||
The object returned by this function and the data inside it should not
|
||||
be changed.
|
||||
|
||||
This function may only be called after the Ren'Py display has initialized,
|
||||
and is intended to be called from the console, where it will print in
|
||||
a human-readable form.
|
||||
"""
|
||||
|
||||
face = load_face(font, "harfbuzz")
|
||||
|
||||
return face.variations
|
||||
|
||||
|
||||
class FontGroup(object):
|
||||
|
||||
+17
-12
@@ -316,14 +316,19 @@ cdef class FTFont:
|
||||
FT_Stroker_Set(self.stroker, outline * 64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0)
|
||||
self.expand = outline * 2
|
||||
|
||||
self.has_setup = False
|
||||
|
||||
if hinting == "bytecode":
|
||||
self.hinting = FT_LOAD_NO_AUTOHINT
|
||||
elif hinting == "none" or hinting is None:
|
||||
self.hinting = FT_LOAD_NO_HINTING
|
||||
if hinting == "none" or hinting is None:
|
||||
self.hinting = FT_LOAD_NO_HINTING | FT_LOAD_TARGET_NORMAL
|
||||
elif hinting == "bytecode":
|
||||
self.hinting = FT_LOAD_NO_AUTOHINT | FT_LOAD_TARGET_NORMAL
|
||||
elif hinting == "auto-light":
|
||||
self.hinting = FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_LIGHT
|
||||
elif hinting == "auto":
|
||||
self.hinting = FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_NORMAL
|
||||
else:
|
||||
self.hinting = FT_LOAD_FORCE_AUTOHINT
|
||||
self.hinting = FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_NORMAL
|
||||
|
||||
|
||||
|
||||
cdef setup(self):
|
||||
"""
|
||||
@@ -873,9 +878,9 @@ cdef class FTFont:
|
||||
for px from underline_x <= px < underline_end:
|
||||
line = pixels + py * pitch + px * 4
|
||||
|
||||
line[0] = Sr
|
||||
line[1] = Sg
|
||||
line[2] = Sb
|
||||
line[0] = Sr * Sa // 255
|
||||
line[1] = Sg * Sa // 255
|
||||
line[2] = Sb * Sa // 255
|
||||
line[3] = Sa
|
||||
|
||||
# Strikethrough.
|
||||
@@ -889,7 +894,7 @@ cdef class FTFont:
|
||||
for px from underline_x <= px < underline_end:
|
||||
line = pixels + py * pitch + px * 4
|
||||
|
||||
line[0] = Sr
|
||||
line[1] = Sg
|
||||
line[2] = Sb
|
||||
line[0] = Sr * Sa // 255
|
||||
line[1] = Sg * Sa // 255
|
||||
line[2] = Sb * Sa // 255
|
||||
line[3] = Sa
|
||||
|
||||
+208
-26
@@ -99,6 +99,11 @@ cdef extern from "hb.h":
|
||||
|
||||
void hb_buffer_set_cluster_level(hb_buffer_t *buffer, hb_buffer_cluster_level_t cluster_level)
|
||||
|
||||
# hb-face
|
||||
struct hb_face_t
|
||||
|
||||
void hb_face_destroy(hb_face_t *)
|
||||
|
||||
# hb-font
|
||||
struct hb_font_t
|
||||
|
||||
@@ -119,11 +124,22 @@ cdef extern from "hb.h":
|
||||
|
||||
|
||||
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)
|
||||
hb_bool_t hb_ft_font_changed(hb_font_t *font)
|
||||
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
|
||||
ctypedef const hb_language_impl_t *hb_language_t
|
||||
|
||||
unsigned int hb_ot_name_get_utf8 (hb_face_t *face,
|
||||
hb_ot_name_id_t name_id,
|
||||
hb_language_t language,
|
||||
unsigned int *text_size,
|
||||
char *text)
|
||||
|
||||
# The freetype library object we use.
|
||||
cdef FT_Library library
|
||||
@@ -235,6 +251,47 @@ cdef void close_func(FT_Stream stream):
|
||||
|
||||
return
|
||||
|
||||
class Axis:
|
||||
"""
|
||||
Represents an axis in a variable font.
|
||||
"""
|
||||
|
||||
def __init__(self, index, minimum, default, maximum):
|
||||
self.index = index
|
||||
self.minimum = minimum
|
||||
self.default = default
|
||||
self.maximum = maximum
|
||||
|
||||
def __repr__(self):
|
||||
return "<Axis index={self.index} minimum={self.minimum} default={self.default} maximum={self.maximum}>".format(self=self)
|
||||
|
||||
|
||||
class Variations:
|
||||
"""
|
||||
Represents the variations of a font.
|
||||
"""
|
||||
|
||||
# Ensure this isn't shortened by the console.
|
||||
_console_always_long = True
|
||||
|
||||
def __init__(self):
|
||||
# A map fron a named instance name to its index.
|
||||
self.instance = { }
|
||||
|
||||
# A map from an axis name to its Axis object.
|
||||
self.axis = { }
|
||||
|
||||
def __repr__(self):
|
||||
rv = [ ]
|
||||
|
||||
for k in self.instance:
|
||||
rv.append(" Named Instance: " + repr(k))
|
||||
|
||||
for k, v in self.axis.items():
|
||||
rv.append(" Axis: " + repr(k) + " (minimum={}, default={}, maximum={})".format(v.minimum, v.default, v.maximum))
|
||||
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
cdef class HBFace:
|
||||
"""
|
||||
@@ -256,10 +313,29 @@ cdef class HBFace:
|
||||
|
||||
public object fn
|
||||
|
||||
# The basic info about variations.
|
||||
FT_MM_Var *mm_var
|
||||
|
||||
# Information about the variations, in Python.
|
||||
public object variations
|
||||
|
||||
# Used to keep from switching instance and axis when not
|
||||
# required.
|
||||
public object current_instance
|
||||
public object current_axis
|
||||
|
||||
def __dealloc__(self):
|
||||
if self.mm_var:
|
||||
FT_Done_MM_Var(library, self.mm_var)
|
||||
|
||||
def __init__(self, f, index, fn):
|
||||
|
||||
cdef int error
|
||||
cdef unsigned long size
|
||||
cdef hb_face_t *hb_face
|
||||
|
||||
cdef char text[256]
|
||||
cdef unsigned int text_length
|
||||
|
||||
# The filename.
|
||||
self.fn = fn
|
||||
@@ -294,6 +370,36 @@ cdef class HBFace:
|
||||
# The size the face is at.
|
||||
self.size = -1
|
||||
|
||||
# Variations.
|
||||
self.mm_var = NULL
|
||||
self.variations = None
|
||||
|
||||
rv = FT_Get_MM_Var(self.face, &(self.mm_var))
|
||||
if rv == 0:
|
||||
|
||||
self.variations = Variations()
|
||||
|
||||
hb_face = hb_ft_face_create(self.face, NULL)
|
||||
|
||||
for 0 <= i < self.mm_var.num_axis:
|
||||
if i >= 16:
|
||||
continue
|
||||
|
||||
self.variations.axis[self.mm_var.axis[i].name.decode("utf-8").lower()] = Axis(
|
||||
i,
|
||||
self.mm_var.axis[i].minimum / 65536.0,
|
||||
self.mm_var.axis[i].default / 65536.0,
|
||||
self.mm_var.axis[i].maximum / 65536.0,
|
||||
)
|
||||
|
||||
for 0 < i < self.mm_var.num_namedstyles:
|
||||
text_length = 256
|
||||
if hb_ot_name_get_utf8(hb_face, self.mm_var.namedstyle[i].strid, NULL, &text_length, text):
|
||||
self.variations.instance[text[0:text_length].decode("utf-8").lower()] = i
|
||||
|
||||
hb_face_destroy(hb_face)
|
||||
|
||||
|
||||
cdef class HBFont:
|
||||
|
||||
cdef:
|
||||
@@ -338,6 +444,13 @@ cdef class HBFont:
|
||||
# The font harfbuzz uses.
|
||||
hb_font_t *hb_font
|
||||
|
||||
# For a variable font, the instance to use.
|
||||
object instance
|
||||
|
||||
# For a variable font, the values for all non-default axes.
|
||||
object axis
|
||||
|
||||
|
||||
def __cinit__(self):
|
||||
for i from 0 <= i < 256:
|
||||
self.cache[i].index = -1
|
||||
@@ -350,19 +463,33 @@ cdef class HBFont:
|
||||
if self.stroker != NULL:
|
||||
FT_Stroker_Done(self.stroker)
|
||||
|
||||
if self.hb_font:
|
||||
hb_font_destroy(self.hb_font)
|
||||
|
||||
def __init__(self, face, float size, float bold, bint italic, int outline, bint antialias, bint vertical, hinting):
|
||||
def __init__(self, face, float size, float bold, bint italic, int outline, bint antialias, bint vertical, hinting, instance, axis):
|
||||
|
||||
self.face_object = face
|
||||
self.face = self.face_object.face
|
||||
|
||||
if size < 1:
|
||||
size = 1
|
||||
|
||||
if instance is None and face.variations:
|
||||
if bold >= 1.0:
|
||||
if "bold" in face.variations.instance:
|
||||
bold = 0.0
|
||||
instance = "bold"
|
||||
else:
|
||||
if "regular" in face.variations.instance:
|
||||
instance = "regular"
|
||||
|
||||
if bold:
|
||||
antialias = True
|
||||
|
||||
size = size * renpy.config.ftfont_scale.get(face.fn, 1.0) * renpy.game.preferences.font_size
|
||||
self.instance = instance
|
||||
self.axis = axis
|
||||
|
||||
self.face_object = face
|
||||
self.face = self.face_object.face
|
||||
size = size * renpy.config.ftfont_scale.get(face.fn, 1.0) * renpy.game.preferences.font_size
|
||||
|
||||
self.size = size
|
||||
self.bold = bold
|
||||
@@ -382,24 +509,69 @@ cdef class HBFont:
|
||||
|
||||
self.has_setup = False
|
||||
|
||||
if hinting == "bytecode":
|
||||
self.hinting = FT_LOAD_NO_AUTOHINT
|
||||
elif hinting == "none" or hinting is None:
|
||||
self.hinting = FT_LOAD_NO_HINTING
|
||||
if hinting == "none" or hinting is None:
|
||||
self.hinting = FT_LOAD_NO_HINTING | FT_LOAD_TARGET_NORMAL
|
||||
elif hinting == "bytecode":
|
||||
self.hinting = FT_LOAD_NO_AUTOHINT | FT_LOAD_TARGET_NORMAL
|
||||
elif hinting == "auto-light":
|
||||
self.hinting = FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_LIGHT
|
||||
elif hinting == "auto":
|
||||
self.hinting = FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_NORMAL
|
||||
else:
|
||||
self.hinting = FT_LOAD_FORCE_AUTOHINT
|
||||
|
||||
self.hinting = FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_NORMAL
|
||||
|
||||
FT_Set_Char_Size(self.face_object.face, 0, <int> (self.size * 64), 0, 0)
|
||||
|
||||
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)
|
||||
cdef setup_variations(self):
|
||||
cdef FT_Fixed coords[16]
|
||||
|
||||
hb_ft_font_set_load_flags(self.hb_font, self.hinting | FT_LOAD_COLOR)
|
||||
fo = self.face_object
|
||||
axis = self.axis
|
||||
instance = self.instance
|
||||
variations = fo.variations
|
||||
|
||||
if axis == fo.current_axis and instance == fo.current_instance:
|
||||
return
|
||||
|
||||
fo.current_axis = axis
|
||||
fo.current_instance = instance
|
||||
|
||||
# If we have a known instance, use it.
|
||||
|
||||
if instance and instance.lower() in variations.instance:
|
||||
index = variations.instance[instance.lower()]
|
||||
for 0 <= i < min(fo.mm_var.num_axis, 16):
|
||||
coords[i] = fo.mm_var.namedstyle[index].coords[i]
|
||||
|
||||
else:
|
||||
|
||||
# Otherwise, use per-axis defaults.
|
||||
|
||||
for k, v in variations.axis.items():
|
||||
coords[v.index] = int(v.default * 65536 + 1)
|
||||
|
||||
if axis:
|
||||
|
||||
# If we have per-axis information, use that.
|
||||
|
||||
for k, value in axis.items():
|
||||
|
||||
k = k.lower()
|
||||
if k in variations.axis:
|
||||
ax = variations.axis[k]
|
||||
|
||||
if ax.index >= 16:
|
||||
continue
|
||||
|
||||
if value < ax.minimum:
|
||||
value = ax.minimum
|
||||
elif value > ax.maximum:
|
||||
value = ax.maximum
|
||||
|
||||
coords[ax.index] = int(value * 65536)
|
||||
|
||||
FT_Set_Var_Design_Coordinates(self.face, min(fo.mm_var.num_axis, 16), coords)
|
||||
|
||||
cdef setup(self):
|
||||
"""
|
||||
@@ -413,6 +585,9 @@ cdef class HBFont:
|
||||
|
||||
face = self.face
|
||||
|
||||
if self.face_object.variations:
|
||||
self.setup_variations()
|
||||
|
||||
if self.face_object.size != self.size:
|
||||
self.face_object.size = self.size
|
||||
|
||||
@@ -420,9 +595,6 @@ cdef class HBFont:
|
||||
if error:
|
||||
raise FreetypeError(error)
|
||||
|
||||
hb_ft_font_changed(self.hb_font)
|
||||
|
||||
|
||||
if not self.has_setup:
|
||||
|
||||
self.has_setup = True
|
||||
@@ -434,7 +606,6 @@ cdef class HBFont:
|
||||
self.ascent = FT_CEIL(int(face.size.metrics.ascender * vextent_scale))
|
||||
self.descent = FT_FLOOR(int(face.size.metrics.descender * vextent_scale))
|
||||
|
||||
|
||||
if self.descent > 0:
|
||||
self.descent = -self.descent
|
||||
|
||||
@@ -456,6 +627,17 @@ cdef class HBFont:
|
||||
|
||||
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):
|
||||
@@ -875,9 +1057,9 @@ cdef class HBFont:
|
||||
for px from underline_x <= px < underline_end:
|
||||
line = pixels + py * pitch + px * 4
|
||||
|
||||
line[0] = Sr
|
||||
line[1] = Sg
|
||||
line[2] = Sb
|
||||
line[0] = Sr * Sa // 255
|
||||
line[1] = Sg * Sa // 255
|
||||
line[2] = Sb * Sa // 255
|
||||
line[3] = Sa
|
||||
|
||||
# Strikethrough.
|
||||
@@ -891,7 +1073,7 @@ cdef class HBFont:
|
||||
for px from underline_x <= px < underline_end:
|
||||
line = pixels + py * pitch + px * 4
|
||||
|
||||
line[0] = Sr
|
||||
line[1] = Sg
|
||||
line[2] = Sb
|
||||
line[0] = Sr * Sa // 255
|
||||
line[1] = Sg * Sa // 255
|
||||
line[2] = Sb * Sa // 255
|
||||
line[3] = Sa
|
||||
|
||||
+28
-3
@@ -201,6 +201,8 @@ class TextSegment(object):
|
||||
self.ignore = source.ignore
|
||||
self.default_font = source.default_font
|
||||
self.shaper = source.shaper
|
||||
self.instance = source.instance
|
||||
self.axis = source.axis
|
||||
|
||||
else:
|
||||
self.hyperlink = 0
|
||||
@@ -250,6 +252,9 @@ class TextSegment(object):
|
||||
|
||||
self.shaper = style.shaper
|
||||
|
||||
self.axis = style.axis
|
||||
self.instance = style.instance
|
||||
|
||||
# From here down is the public glyph API.
|
||||
|
||||
def glyphs(self, s, layout):
|
||||
@@ -260,7 +265,7 @@ class TextSegment(object):
|
||||
if self.ignore:
|
||||
return [ ]
|
||||
|
||||
fo = font.get_font(self.font, self.size, self.bold, self.italic, 0, self.antialias, self.vertical, self.hinting, layout.oversample, self.shaper)
|
||||
fo = font.get_font(self.font, self.size, self.bold, self.italic, 0, self.antialias, self.vertical, self.hinting, layout.oversample, self.shaper, self.instance, self.axis)
|
||||
rv = fo.glyphs(s)
|
||||
|
||||
# Apply kerning to the glyphs.
|
||||
@@ -292,7 +297,7 @@ class TextSegment(object):
|
||||
color = self.color
|
||||
black_color = self.black_color
|
||||
|
||||
fo = font.get_font(self.font, self.size, self.bold, self.italic, di.outline, self.antialias, self.vertical, self.hinting, layout.oversample, self.shaper)
|
||||
fo = font.get_font(self.font, self.size, self.bold, self.italic, di.outline, self.antialias, self.vertical, self.hinting, layout.oversample, self.shaper, self.instance, self.axis)
|
||||
fo.draw(di.surface, xo, yo, color, glyphs, self.underline, self.strikethrough, black_color)
|
||||
|
||||
def assign_times(self, gt, glyphs):
|
||||
@@ -357,7 +362,7 @@ class TextSegment(object):
|
||||
origin point.
|
||||
"""
|
||||
|
||||
fo = font.get_font(self.font, self.size, self.bold, self.italic, 0, self.antialias, self.vertical, self.hinting, layout.oversample, self.shaper)
|
||||
fo = font.get_font(self.font, self.size, self.bold, self.italic, 0, self.antialias, self.vertical, self.hinting, layout.oversample, self.shaper, self.instance, self.axis)
|
||||
return fo.bounds(glyphs, bounds)
|
||||
|
||||
|
||||
@@ -1362,6 +1367,26 @@ class Layout(object):
|
||||
elif tag == "noalt":
|
||||
ts = push()
|
||||
|
||||
elif tag == "instance":
|
||||
ts = push()
|
||||
ts.instance = value.lower()
|
||||
ts.axis = None
|
||||
|
||||
elif tag.startswith("axis:"):
|
||||
ts = push()
|
||||
if ts.axis:
|
||||
ts.axis = dict(ts.axis)
|
||||
else:
|
||||
ts.axis = { }
|
||||
|
||||
try:
|
||||
value = float(value)
|
||||
except (TypeError, ValueError):
|
||||
raise
|
||||
|
||||
axis = tag.partition(":")[2].lower()
|
||||
ts.axis[axis] = float(value)
|
||||
|
||||
elif tag[0] == "#":
|
||||
pass
|
||||
|
||||
|
||||
+25
-23
@@ -29,8 +29,6 @@
|
||||
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 # *
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import sys
|
||||
|
||||
import renpy
|
||||
@@ -42,9 +40,7 @@ from renpy.display.behavior import is_selected, is_sensitive
|
||||
|
||||
class Action(renpy.object.Object):
|
||||
"""
|
||||
This can be passed to the clicked method of a button or hotspot. It is
|
||||
called when the action is selected. The other methods determine if the
|
||||
action should be displayed insensitive or disabled.
|
||||
Subclassable by creators, documented in Sphinx.
|
||||
"""
|
||||
|
||||
# Alt text.
|
||||
@@ -66,12 +62,12 @@ class Action(renpy.object.Object):
|
||||
return
|
||||
|
||||
def __call__(self):
|
||||
raise Exception("Not implemented")
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BarValue(renpy.object.Object):
|
||||
"""
|
||||
This can be passed to the value method of bar and hotbar.
|
||||
Subclassable by creators, documented in Sphinx.
|
||||
"""
|
||||
|
||||
# Alt text.
|
||||
@@ -86,7 +82,7 @@ class BarValue(renpy.object.Object):
|
||||
return
|
||||
|
||||
def get_adjustment(self): # type: (BarValue) -> renpy.display.behavior.Adjustment
|
||||
raise Exception("Not implemented")
|
||||
raise NotImplementedError
|
||||
|
||||
def get_style(self):
|
||||
return "bar", "vbar"
|
||||
@@ -104,6 +100,12 @@ class Addable(object):
|
||||
# A style_prefix associates with this addable.
|
||||
style_prefix = None
|
||||
|
||||
def add(self, d, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self, d):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_layer(self):
|
||||
return Exception("Operation can only be performed on a layer.")
|
||||
|
||||
@@ -224,7 +226,7 @@ class ChildOrFixed(Addable):
|
||||
|
||||
|
||||
# A stack of things we can add to.
|
||||
stack = [ ]
|
||||
stack = [ ] # type: list[Addable]
|
||||
|
||||
# A stack of open ui.ats.
|
||||
at_stack = [ ]
|
||||
@@ -251,7 +253,7 @@ def reset():
|
||||
renpy.game.post_init.append(reset)
|
||||
|
||||
|
||||
def interact(type='misc', roll_forward=None, **kwargs): # @ReservedAssignment
|
||||
def interact(type='misc', roll_forward=None, **kwargs):
|
||||
"""
|
||||
:doc: ui
|
||||
:args: (*, roll_forward=None, mouse='default')
|
||||
@@ -421,7 +423,7 @@ def combine_style(style_prefix, style_suffix):
|
||||
else:
|
||||
new_style = style_prefix + "_" + style_suffix
|
||||
|
||||
return renpy.style.get_style(new_style) # @UndefinedVariable
|
||||
return renpy.style.get_style(new_style)
|
||||
|
||||
|
||||
def prefixed_style(style_suffix):
|
||||
@@ -477,9 +479,9 @@ class Wrapper(renpy.object.Object):
|
||||
|
||||
# Pull out the special kwargs, widget_id, at, and style_prefix.
|
||||
|
||||
widget_id = kwargs.pop("id", None) # @ReservedAssignment
|
||||
widget_id = kwargs.pop("id", None)
|
||||
|
||||
at_list = kwargs.pop("at", [ ])
|
||||
at_list = kwargs.pop("at", [ ]) # type: list
|
||||
if not isinstance(at_list, (list, tuple)):
|
||||
at_list = [ at_list ]
|
||||
|
||||
@@ -858,7 +860,7 @@ def menu(menuitems,
|
||||
close()
|
||||
|
||||
|
||||
input = Wrapper(renpy.display.behavior.Input, exclude='{}', style="input", replaces=True) # @ReservedAssignment
|
||||
input = Wrapper(renpy.display.behavior.Input, exclude='{}', style="input", replaces=True)
|
||||
|
||||
|
||||
def imagemap_compat(ground,
|
||||
@@ -982,7 +984,7 @@ def _textbutton(label, clicked=None, style=None, text_style=None, substitute=Tru
|
||||
style = prefixed_style("button")
|
||||
|
||||
if text_style is None:
|
||||
text_style = renpy.style.get_text_style(style, prefixed_style('button_text')) # @UndefinedVariable
|
||||
text_style = renpy.style.get_text_style(style, prefixed_style('button_text'))
|
||||
|
||||
rv = renpy.display.behavior.Button(style=style, clicked=clicked, **button_kwargs)
|
||||
text = renpy.text.text.Text(label, style=text_style, substitute=substitute, scope=scope, **text_kwargs)
|
||||
@@ -1003,7 +1005,7 @@ def _label(label, style=None, text_style=None, substitute=True, scope=None, **kw
|
||||
style = prefixed_style('label')
|
||||
|
||||
if text_style is None:
|
||||
text_style = renpy.style.get_text_style(style, prefixed_style('label_text')) # @UndefinedVariable
|
||||
text_style = renpy.style.get_text_style(style, prefixed_style('label_text'))
|
||||
|
||||
rv = renpy.display.layout.Window(None, style=style, **label_kwargs)
|
||||
text = renpy.text.text.Text(label, style=text_style, substitute=substitute, scope=scope, **text_kwargs)
|
||||
@@ -1021,13 +1023,13 @@ adjustment = renpy.display.behavior.Adjustment
|
||||
def _bar(*args, **properties):
|
||||
|
||||
if len(args) == 4:
|
||||
width, height, range, value = args # @ReservedAssignment
|
||||
width, height, range, value = args
|
||||
if len(args) == 2:
|
||||
range, value = args # @ReservedAssignment
|
||||
range, value = args
|
||||
width = None
|
||||
height = None
|
||||
else:
|
||||
range = 1 # @ReservedAssignment
|
||||
range = 1
|
||||
value = 0
|
||||
width = None
|
||||
height = None
|
||||
@@ -1039,7 +1041,7 @@ def _bar(*args, **properties):
|
||||
height = properties.pop("height")
|
||||
|
||||
if "range" in properties:
|
||||
range = properties.pop("range") # @ReservedAssignment
|
||||
range = properties.pop("range")
|
||||
|
||||
if "value" in properties:
|
||||
value = properties.pop("value")
|
||||
@@ -1067,7 +1069,7 @@ scrollbar = Wrapper(_bar, style='scrollbar', replaces=True)
|
||||
vscrollbar = Wrapper(_bar, style='vscrollbar', replaces=True)
|
||||
|
||||
|
||||
def _autobar_interpolate(range, start, end, time, st, at, **properties): # @ReservedAssignment
|
||||
def _autobar_interpolate(range, start, end, time, st, at, **properties):
|
||||
|
||||
if st > time:
|
||||
t = 1.0
|
||||
@@ -1083,7 +1085,7 @@ def _autobar_interpolate(range, start, end, time, st, at, **properties): # @Rese
|
||||
autobar_interpolate = renpy.curry.curry(_autobar_interpolate)
|
||||
|
||||
|
||||
def _autobar(range, start, end, time, **properties): # @ReservedAssignment
|
||||
def _autobar(range, start, end, time, **properties):
|
||||
return renpy.display.layout.DynamicDisplayable(autobar_interpolate(range, start, end, time, **properties))
|
||||
|
||||
|
||||
@@ -1347,7 +1349,7 @@ def hotspot(*args, **kwargs):
|
||||
null()
|
||||
|
||||
|
||||
def _hotbar(spot, adjustment=None, range=None, value=None, **properties): # @ReservedAssignment
|
||||
def _hotbar(spot, adjustment=None, range=None, value=None, **properties):
|
||||
|
||||
if (adjustment is None) and (range is None) and (value is None):
|
||||
raise Exception("hotbar requires either an adjustment or a range and value.")
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright 2004-2023 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.
|
||||
|
||||
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 # *
|
||||
@@ -0,0 +1,362 @@
|
||||
# Copyright 2004-2023 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.
|
||||
|
||||
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 hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import zlib
|
||||
|
||||
def hash_data(data):
|
||||
"""
|
||||
Given `data` (bytes), returns a hexadecimal hash of the data.
|
||||
"""
|
||||
|
||||
h = hashlib.sha256()
|
||||
h.update(data)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def hash_list(data):
|
||||
"""
|
||||
Hashes a list of strings.
|
||||
"""
|
||||
|
||||
return hash_data("\n".join(data).encode("utf-8"))
|
||||
|
||||
|
||||
def dump(d):
|
||||
"""
|
||||
Dumps a dictionary to a JSON string.
|
||||
"""
|
||||
|
||||
print(json.dumps(d, indent=2))
|
||||
|
||||
|
||||
# Constants used for the compressed field of segments.
|
||||
COMPRESS_NONE = 0
|
||||
COMPRESS_ZLIB = 1
|
||||
|
||||
class Segment(object):
|
||||
"""
|
||||
This represents a segment of a file conaining the data with the given
|
||||
hash.
|
||||
"""
|
||||
|
||||
def __init__(self, offset, size, hash, compressed):
|
||||
|
||||
# The offset within the file.
|
||||
self.offset = offset
|
||||
|
||||
# The size of the data.
|
||||
self.size = size
|
||||
|
||||
# The hash of the data
|
||||
self.hash = hash
|
||||
|
||||
# Determines if the data is compressed. This should be one of the
|
||||
# COMPRESS_* constants.
|
||||
self.compressed = compressed
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Segment):
|
||||
return False
|
||||
|
||||
return self.offset == other.offset and self.size == other.size and self.hash == other.hash and self.compressed == other.compressed
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"offset" : self.offset,
|
||||
"size" : self.size,
|
||||
"hash" : self.hash,
|
||||
"compressed" : self.compressed }
|
||||
|
||||
@staticmethod
|
||||
def from_json(d):
|
||||
rv = Segment(d["offset"], d["size"], d["hash"], d["compressed"])
|
||||
return rv
|
||||
|
||||
|
||||
class Directory(object):
|
||||
"""
|
||||
Represents a directory.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name.replace("\\", "/")
|
||||
|
||||
def to_json(self):
|
||||
return { "name" : self.name }
|
||||
|
||||
@staticmethod
|
||||
def from_json(d):
|
||||
rv = Directory(d["name"])
|
||||
return rv
|
||||
|
||||
class File(object):
|
||||
"""
|
||||
Represents a file.
|
||||
"""
|
||||
|
||||
def __init__(self, name, data_filename=None, segments=None, mtime=0, xbit=False):
|
||||
|
||||
# The name of the file. This is the name that can be stored.
|
||||
self.name = name.replace("\\", "/")
|
||||
|
||||
# If the actual content of the file is stored somewhere else, this is
|
||||
# the name of the file it is stored in.
|
||||
self.data_filename = data_filename.replace("\\", "/") if data_filename else None
|
||||
|
||||
# The segments of the file. This is a list of Segment objects.
|
||||
self.segments = segments or [ ]
|
||||
|
||||
# The modification time of the file.
|
||||
self.mtime = mtime
|
||||
|
||||
# If true, the file is executable.
|
||||
self.xbit = xbit
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"name" : self.name,
|
||||
"segments" : [ i.to_json() for i in self.segments ],
|
||||
"xbit" : self.xbit,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_json(d):
|
||||
rv = File(d["name"], segments=[ Segment.from_json(i) for i in d["segments"] ], xbit=d["xbit"])
|
||||
return rv
|
||||
|
||||
def scan_segments(self, f, offset, size):
|
||||
"""
|
||||
Split a file into segments that are less than 2MB in size.
|
||||
|
||||
`f`
|
||||
The open file.
|
||||
|
||||
`offset`
|
||||
The offset of the file within the file.
|
||||
|
||||
`size`
|
||||
The size of the data to divide into segments.
|
||||
"""
|
||||
|
||||
f.seek(offset)
|
||||
|
||||
while size > 0:
|
||||
segment_size = min(size, 2 * 1024 * 1024)
|
||||
|
||||
data = f.read(segment_size)
|
||||
|
||||
seg = Segment(offset, len(data), hash_data(data), COMPRESS_NONE)
|
||||
self.segments.append(seg)
|
||||
|
||||
offset += len(data)
|
||||
size -= len(data)
|
||||
|
||||
|
||||
def scan_rpa(self, f, total_size):
|
||||
"""
|
||||
Scans an RPA archive, segmenting it into the underlying files.
|
||||
"""
|
||||
|
||||
# Read the header.
|
||||
l = f.read(40)
|
||||
offset = int(l[8:24], 16)
|
||||
key = int(l[25:33], 16)
|
||||
f.seek(offset)
|
||||
index = pickle.loads(zlib.decompress(f.read()))
|
||||
|
||||
|
||||
# This is a list of offset, size tuples for each of the files
|
||||
# in the archive. These will be sorted into the order the segments
|
||||
# appear in the archive.
|
||||
segments = [ ]
|
||||
|
||||
for v in index.values():
|
||||
for i in v:
|
||||
segments.append((i[0] ^ key, i[1] ^ key))
|
||||
|
||||
segments.sort()
|
||||
|
||||
# Iterate through, adding a segment for each block in the .rpa.
|
||||
pos = 0
|
||||
|
||||
for offset, size in segments:
|
||||
self.scan_segments(f, pos, offset - pos)
|
||||
self.scan_segments(f, offset, size)
|
||||
pos = offset + size
|
||||
|
||||
self.scan_segments(f, pos, total_size - pos)
|
||||
|
||||
|
||||
def scan(self):
|
||||
"""
|
||||
Separate the file into segments. This may be done in a content-aware
|
||||
way, if required.
|
||||
"""
|
||||
|
||||
fn = self.data_filename or self.name
|
||||
|
||||
self.mtime = os.path.getmtime(fn)
|
||||
|
||||
self.segments = [ ]
|
||||
|
||||
with open(fn, "rb") as f:
|
||||
|
||||
start = f.read(1024)
|
||||
|
||||
# Determine the size of the file.
|
||||
f.seek(0, 2)
|
||||
size = f.tell()
|
||||
f.seek(0)
|
||||
|
||||
if self.name.endswith(".rpa") and start[:8] == b'RPA-3.0 ':
|
||||
try:
|
||||
self.scan_rpa(f, size)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.scan_segments(f, 0, size)
|
||||
|
||||
def add_data_filename(self, path):
|
||||
"""
|
||||
Sets the data_filename of this file to the name of the file, relative
|
||||
to `path`.
|
||||
"""
|
||||
|
||||
self.data_filename = os.path.join(path, self.name)
|
||||
|
||||
|
||||
class FileList(object):
|
||||
"""
|
||||
Represents a list of files and directories.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.directories = [ ]
|
||||
self.files = [ ]
|
||||
self.blocks = [ ]
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"directories" : [ i.to_json() for i in self.directories ],
|
||||
"files" : [ i.to_json() for i in self.files ],
|
||||
"blocks" : [ i.to_json() for i in self.blocks ],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_json(d):
|
||||
rv = FileList()
|
||||
rv.directories = [ Directory.from_json(i) for i in d["directories"] ]
|
||||
rv.files = [ File.from_json(i) for i in d["files"] ]
|
||||
rv.blocks = [ File.from_json(i) for i in d["blocks"] ]
|
||||
return rv
|
||||
|
||||
def to_current_json(self):
|
||||
"""
|
||||
Returns a JSON representation of the file list that can be used
|
||||
in the current.json files.
|
||||
"""
|
||||
|
||||
return {
|
||||
"directories" : [ i.name for i in self.directories ],
|
||||
"files" : [ i.name for i in self.files ],
|
||||
"xbit" : [ i.name for i in self.files if i.xbit ],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_current_json(d):
|
||||
xbit = set(d["xbit"])
|
||||
|
||||
rv = FileList()
|
||||
|
||||
for i in d["directories"]:
|
||||
rv.directories.append(Directory(i))
|
||||
|
||||
for i in d["files"]:
|
||||
rv.files.append(File(i, xbit=i in xbit))
|
||||
|
||||
return rv
|
||||
|
||||
def add_directory(self, name):
|
||||
"""
|
||||
Called from the launcher to add a directory to this file list.
|
||||
"""
|
||||
self.directories.append(Directory(name))
|
||||
|
||||
def add_file(self, name, path, xbit):
|
||||
"""
|
||||
Called from the launcher to add a file to this file list.
|
||||
"""
|
||||
|
||||
self.files.append(File(name, data_filename=path, xbit=xbit))
|
||||
|
||||
|
||||
def scan(self, root, data_filename=True):
|
||||
"""
|
||||
Scan a directory, recursively, and add the files and directories
|
||||
found to this file test. This is intended for testing. This does
|
||||
not call .scan on the files.
|
||||
"""
|
||||
|
||||
for dn, dirs, files in os.walk(root):
|
||||
|
||||
for d in dirs:
|
||||
d = os.path.relpath(os.path.join(dn, d), root)
|
||||
self.directories.append(Directory(d))
|
||||
|
||||
for fn in files:
|
||||
fn = os.path.join(dn, fn)
|
||||
relfn = os.path.relpath(fn, root)
|
||||
|
||||
if data_filename:
|
||||
xbit = os.access(fn, os.X_OK)
|
||||
else:
|
||||
xbit = False
|
||||
|
||||
f = File(relfn, data_filename=fn if data_filename else None, xbit=xbit)
|
||||
self.files.append(f)
|
||||
|
||||
self.directories.sort(key=lambda x : x.name)
|
||||
self.files.sort(key=lambda x : x.name)
|
||||
|
||||
def encode(self):
|
||||
"""
|
||||
Encode the file list into a file.
|
||||
"""
|
||||
|
||||
data = json.dumps(self.to_json()).encode("utf-8")
|
||||
return zlib.compress(data, 3)
|
||||
|
||||
@staticmethod
|
||||
def decode(data):
|
||||
"""
|
||||
Decode the file list from a file.
|
||||
"""
|
||||
|
||||
data = zlib.decompress(data)
|
||||
return FileList.from_json(json.loads(data.decode("utf-8")))
|
||||
@@ -0,0 +1,229 @@
|
||||
# Copyright 2004-2023 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.
|
||||
|
||||
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 requests
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
def byte_ranges(ranges):
|
||||
"""
|
||||
Given a list of (offset, size) pairs, returns a list of byte ranges
|
||||
that will cover those ranges.
|
||||
"""
|
||||
|
||||
ranges = list(ranges)
|
||||
ranges.sort()
|
||||
|
||||
rv = [ ]
|
||||
|
||||
|
||||
for offset, size in ranges:
|
||||
start = offset
|
||||
end = offset + size - 1
|
||||
|
||||
if rv and rv[-1][1] == start - 1:
|
||||
start = rv.pop()[0]
|
||||
|
||||
rv.append((start, end))
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def write_range(f, headers, content):
|
||||
m = re.search(r"bytes (\d+)-(\d+)/(\d+)", headers["Content-Range"])
|
||||
start = int(m.group(1))
|
||||
end = int(m.group(2))
|
||||
total = int(m.group(3))
|
||||
|
||||
f.seek(start)
|
||||
f.write(content)
|
||||
f.truncate(total)
|
||||
|
||||
return [ (start, end) ]
|
||||
|
||||
|
||||
def write_multipart(f, headers, content):
|
||||
m = re.search(r"boundary=(.*)", headers["Content-Type"])
|
||||
separator = m.group(1).encode("utf-8")
|
||||
|
||||
boundary = b"\r\n--" + separator + b"\r\n"
|
||||
end_boundary = b"\r\n--" + separator + b"--\r\n"
|
||||
|
||||
content = content.split(end_boundary, 1)[0]
|
||||
|
||||
rv = [ ]
|
||||
|
||||
for part in content.split(boundary):
|
||||
|
||||
if not part:
|
||||
continue
|
||||
|
||||
part_header_text, part_content = part.split(b"\r\n\r\n", 1)
|
||||
part_header_text = part_header_text.decode("utf-8")
|
||||
|
||||
part_headers = { }
|
||||
|
||||
for i in part_header_text.split("\r\n"):
|
||||
k, v = i.split(": ", 1)
|
||||
part_headers[k] = v
|
||||
|
||||
rv.extend(write_range(f, part_headers, part_content))
|
||||
|
||||
return rv
|
||||
|
||||
def download_ranges(url, ranges, destination, progress_callback=None):
|
||||
"""
|
||||
`url`
|
||||
The URL to download from.
|
||||
|
||||
`ranges`
|
||||
A list of (offset, size) pairs, where together the offset and size
|
||||
represent a range that needs to be downloaded.
|
||||
|
||||
`destination`
|
||||
The file to write to.
|
||||
|
||||
`progress_callback`
|
||||
A function that will be called with the number of bytes downloaded
|
||||
and the total number of bytes to download. (This is not perfect, as
|
||||
headers will add overhead.)
|
||||
"""
|
||||
|
||||
ranges = byte_ranges(ranges)
|
||||
total_size = sum(end - start + 1 for start, end in ranges)
|
||||
downloaded = 0
|
||||
|
||||
if os.path.exists(destination):
|
||||
mode = "r+b"
|
||||
else:
|
||||
mode = "wb"
|
||||
|
||||
with open(destination, mode) as destination_file:
|
||||
|
||||
while ranges:
|
||||
|
||||
old_ranges = list(ranges)
|
||||
|
||||
headers = { 'Range' : 'bytes=' + ', '.join('%d-%d' % (start, end) for start, end in ranges[:10]) }
|
||||
|
||||
r = requests.get(url, headers=headers, stream=True)
|
||||
r.raise_for_status()
|
||||
|
||||
blocks = [ ]
|
||||
|
||||
while True:
|
||||
b = r.raw.read(128 * 1024)
|
||||
|
||||
if not b:
|
||||
break
|
||||
|
||||
blocks.append(b)
|
||||
downloaded += len(b)
|
||||
if progress_callback is not None:
|
||||
progress_callback(min(downloaded, total_size), total_size)
|
||||
|
||||
content = b"".join(blocks)
|
||||
|
||||
if r.status_code == 206:
|
||||
if r.headers.get("Content-Type", "").startswith("multipart/byteranges"):
|
||||
got_ranges = write_multipart(destination_file, r.headers, content)
|
||||
else:
|
||||
got_ranges = write_range(destination_file, r.headers, content)
|
||||
|
||||
for i in got_ranges:
|
||||
ranges.remove(i)
|
||||
|
||||
if old_ranges == ranges:
|
||||
# No progrsss being made, fail things.
|
||||
return False
|
||||
|
||||
else:
|
||||
destination_file.seek(0)
|
||||
destination_file.truncate()
|
||||
destination_file.write(content)
|
||||
break
|
||||
|
||||
if not ranges:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
def download(url, ranges, destination, progress_callback=None):
|
||||
"""
|
||||
Downloads the file. First tries to use ranges, and if that fails
|
||||
downloads the entire file.
|
||||
|
||||
`url`
|
||||
The URL to download from.
|
||||
|
||||
`ranges`
|
||||
A list of (offset, size) pairs, where together the offset and size
|
||||
represent a range that needs to be downloaded.
|
||||
|
||||
`destination`
|
||||
The file to write to.
|
||||
|
||||
`progress_callback`
|
||||
A function that will be called with the number of bytes downloaded
|
||||
and the total number of bytes to download. (This is not perfect, as
|
||||
headers will add overhead.)
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
if download_ranges(url, ranges, destination, progress_callback=progress_callback):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# This isn't the good path. Try to muscle along by downloading the entire
|
||||
# file.
|
||||
|
||||
total_size = sum(i[1] for i in ranges)
|
||||
downloaded = 0
|
||||
|
||||
r = requests.get(url, stream=True)
|
||||
r.raise_for_status()
|
||||
|
||||
blocks = [ ]
|
||||
|
||||
while True:
|
||||
b = r.raw.read(128 * 1024)
|
||||
|
||||
if not b:
|
||||
break
|
||||
|
||||
blocks.append(b)
|
||||
downloaded += len(b)
|
||||
if progress_callback is not None:
|
||||
progress_callback(min(downloaded, total_size), total_size)
|
||||
|
||||
content = b"".join(blocks)
|
||||
|
||||
with open(destination, "wb") as f:
|
||||
f.write(content)
|
||||
@@ -0,0 +1,186 @@
|
||||
# Copyright 2004-2023 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.
|
||||
|
||||
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 argparse
|
||||
import json
|
||||
import os
|
||||
import zlib
|
||||
|
||||
from . import common
|
||||
|
||||
class BlockGenerator(object):
|
||||
"""
|
||||
This generates the block file containing the segments for an update.
|
||||
"""
|
||||
|
||||
def __init__(self, name, filelist, targetdir, progress=None, max_rpu_size=50 * 1024 * 1024):
|
||||
|
||||
# A name that will be prefixed to the update.
|
||||
self.name = name
|
||||
|
||||
# The filelist being created.
|
||||
self.filelist = filelist
|
||||
|
||||
# The current segment list.
|
||||
self.segments = [ ]
|
||||
|
||||
# The target directory in which the segments will be stored.
|
||||
self.targetdir = targetdir
|
||||
|
||||
# The hashes that have been seen alread,.
|
||||
self.seen_hashes = set()
|
||||
|
||||
# The maximum size of an rpu file.
|
||||
self.max_rpu_size = max_rpu_size
|
||||
|
||||
# If new.rpu is open, a file object for it.
|
||||
self.new_rpu = None
|
||||
|
||||
# The progress reporter.
|
||||
self.progress = progress
|
||||
|
||||
# Clean out files in the target directory starting with .name.
|
||||
for i in os.listdir(targetdir):
|
||||
if i.startswith(self.name + "-") and i.endswith(".rpu"):
|
||||
os.unlink(os.path.join(targetdir, i))
|
||||
|
||||
# The filelist being created.
|
||||
self.generate()
|
||||
|
||||
# Write the filelist to a file.
|
||||
with open(self.path(self.name + ".files.rpu"), "wb") as f:
|
||||
f.write(self.filelist.encode())
|
||||
|
||||
def path(self, name):
|
||||
"""
|
||||
Returns the path to a file in the target directory.
|
||||
"""
|
||||
|
||||
return os.path.join(self.targetdir, name)
|
||||
|
||||
def open_new_rpu(self):
|
||||
"""
|
||||
Opens the new.rpu file if it's closed.
|
||||
"""
|
||||
|
||||
if self.new_rpu is None:
|
||||
self.new_rpu = open(self.path(self.name + "-new.rpu"), "wb")
|
||||
self.new_rpu.write(b"RPU-BLOCK-1.0\r\n")
|
||||
|
||||
def close_new_rpu(self):
|
||||
"""
|
||||
Closes new.rpu, renaming it to its final filename, and adds an
|
||||
entry to the filelist.
|
||||
"""
|
||||
|
||||
if self.new_rpu is None:
|
||||
return
|
||||
|
||||
self.new_rpu.close()
|
||||
self.new_rpu = None
|
||||
|
||||
filename = self.name + "-" + common.hash_list([ i.hash for i in self.segments ]) + ".rpu"
|
||||
|
||||
os.rename(self.path(self.name + "-new.rpu"), self.path(filename))
|
||||
|
||||
self.filelist.blocks.append(common.File(filename, segments=self.segments))
|
||||
self.segments = [ ]
|
||||
|
||||
def generate_segment(self, f, seg):
|
||||
"""
|
||||
Generates a segment into new.rpu.
|
||||
"""
|
||||
|
||||
if seg.hash in self.seen_hashes:
|
||||
return
|
||||
|
||||
self.seen_hashes.add(seg.hash)
|
||||
|
||||
f.seek(seg.offset)
|
||||
data = f.read(seg.size)
|
||||
|
||||
if self.new_rpu and self.new_rpu.tell() + len(data) > self.max_rpu_size:
|
||||
self.close_new_rpu()
|
||||
|
||||
self.open_new_rpu()
|
||||
|
||||
compressed = common.COMPRESS_NONE
|
||||
|
||||
cdata = zlib.compress(data, 3)
|
||||
|
||||
if len(cdata) < len(data) * .95:
|
||||
data = cdata
|
||||
compressed = common.COMPRESS_ZLIB
|
||||
|
||||
offset = self.new_rpu.tell()
|
||||
size = len(data)
|
||||
|
||||
self.new_rpu.write(data)
|
||||
self.segments.append(common.Segment(offset, size, seg.hash, compressed))
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Create a list of files by reverse mtime. The idea is that the newer
|
||||
# files will appear before the older ones, and so we won't have to pull
|
||||
# from older files to create a new one.
|
||||
files = list(self.filelist.files)
|
||||
files.sort(key=lambda x: (x.mtime, x.name))
|
||||
files.reverse()
|
||||
|
||||
for i, file in enumerate(files):
|
||||
|
||||
if self.progress:
|
||||
self.progress(i + 1, len(files))
|
||||
|
||||
file.scan()
|
||||
|
||||
with open(file.data_filename or file.name, "rb") as f:
|
||||
for seg in file.segments:
|
||||
self.generate_segment(f, seg)
|
||||
|
||||
self.close_new_rpu()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("sourcedir")
|
||||
ap.add_argument("targetdir")
|
||||
|
||||
args = ap.parse_args()
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
targetdir = Path(args.targetdir)
|
||||
targetdir.mkdir(exist_ok=True)
|
||||
|
||||
for i in targetdir.glob("*.rpu"):
|
||||
i.unlink()
|
||||
|
||||
fl = common.FileList()
|
||||
fl.scan(args.sourcedir)
|
||||
|
||||
BlockGenerator("game", fl, args.targetdir)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,636 @@
|
||||
# Copyright 2004-2023 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.
|
||||
|
||||
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 argparse
|
||||
import os
|
||||
import time
|
||||
import zlib
|
||||
|
||||
import requests
|
||||
|
||||
from . import download
|
||||
from . import common
|
||||
|
||||
# Constants from 00updater.py.
|
||||
PREPARING = "PREPARING"
|
||||
DOWNLOADING = "DOWNLOADING"
|
||||
UNPACKING = "UNPACKING"
|
||||
FINISHING = "FINISHING"
|
||||
|
||||
|
||||
class UpdateError(Exception):
|
||||
"""
|
||||
Raised when an error happens.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class Plan(object):
|
||||
"""
|
||||
This represents a plan for updating a single segment.
|
||||
"""
|
||||
|
||||
def __init__(self, block, old_filename, old_offset, old_size, compressed, new_filename, new_offset, new_size, hash):
|
||||
# Is old_filename a blockfile that might need to be downloaded?
|
||||
self.block = block
|
||||
|
||||
# The source filename, size, and offset.
|
||||
self.old_filename = old_filename
|
||||
self.old_offset = old_offset
|
||||
self.old_size = old_size
|
||||
|
||||
# If true, the source information is compressed.
|
||||
self.compressed = compressed
|
||||
|
||||
# The target filename, size, and offset.
|
||||
self.new_filename = new_filename
|
||||
self.new_offset = new_offset
|
||||
self.new_size = new_size
|
||||
|
||||
# The hash.
|
||||
self.hash = hash
|
||||
|
||||
class Update(object):
|
||||
|
||||
def __init__(self, url, newlists, targetdir, oldlists, progress_callback=None, logfile=None, aggressive_removal=False):
|
||||
"""
|
||||
`url`
|
||||
The url that's used as a base to download pack files from.
|
||||
|
||||
`newlists`
|
||||
A list of one or more new file lists.
|
||||
|
||||
`targetdir`
|
||||
The directory to update.
|
||||
|
||||
`oldlists`
|
||||
A list of one or more old file lists.
|
||||
|
||||
`progress_callback`
|
||||
A function that's called to report progress. It takes two arguments,
|
||||
a message and a float between 0.0 and 1.0.
|
||||
|
||||
`logfile`
|
||||
A file to log to. If None, a file is created in the target directory.
|
||||
|
||||
`aggressive_removal`
|
||||
If true, files that are not in the new file list are removed as soon
|
||||
as they are no longer needed.
|
||||
"""
|
||||
|
||||
self.url = url
|
||||
|
||||
self.targetdir = targetdir
|
||||
self.oldlists = oldlists
|
||||
self.newlists = newlists
|
||||
|
||||
self.old_directories = [ i for j in self.oldlists for i in j.directories ]
|
||||
self.new_directories = [ i for j in self.newlists for i in j.directories ]
|
||||
self.new_files = [ i for j in self.newlists for i in j.files ]
|
||||
self.old_files = [ i for j in self.oldlists for i in j.files ]
|
||||
self.block_files = [ i for j in self.newlists for i in j.blocks ]
|
||||
|
||||
# The total number of bytes that will be used on disk when the downloads
|
||||
# are complete.
|
||||
self.disk_total = 0
|
||||
|
||||
# The total number of bytes to download, and the number of bytes downloaded.
|
||||
self.download_total = 0
|
||||
self.download_done = 0
|
||||
|
||||
# The total number of bytes to write, and the number of bytes written.
|
||||
self.write_total = 0
|
||||
self.write_done = 0
|
||||
|
||||
# A list of plan objects.
|
||||
self.plan = [ ]
|
||||
|
||||
# A cache for the destination filename and file pointer.
|
||||
self.destination_filename = None
|
||||
self.destination_fp = None
|
||||
|
||||
# Should removal of old files be done ASAP, or at the end?
|
||||
self.aggressive_removal = aggressive_removal
|
||||
|
||||
# The progress callback.
|
||||
self.progress_callback = progress_callback
|
||||
|
||||
# The set of files to remove.
|
||||
self.removals = set() # type: set[str]
|
||||
|
||||
# The various directories.
|
||||
self.updatedir = os.path.join(self.targetdir, "update")
|
||||
self.blockdir = os.path.join(self.updatedir, "block")
|
||||
self.deleteddir = os.path.join(self.updatedir, "deleted")
|
||||
|
||||
os.makedirs(self.updatedir, exist_ok=True)
|
||||
os.makedirs(self.blockdir, exist_ok=True)
|
||||
os.makedirs(self.deleteddir, exist_ok=True)
|
||||
|
||||
self.logfile = logfile
|
||||
|
||||
print("-" * 80, file=self.logfile)
|
||||
print("Starting update at %s." % time.ctime(), file=self.logfile)
|
||||
|
||||
self.progress(PREPARING, 0.0)
|
||||
|
||||
self.write_padding()
|
||||
self.find_incomplete_files()
|
||||
self.scan_old_files()
|
||||
self.prepare_new_files()
|
||||
self.find_removals()
|
||||
self.remove_identical_files()
|
||||
self.create_plan()
|
||||
self.compute_totals()
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Called to actually perform the update.
|
||||
"""
|
||||
|
||||
self.make_directories()
|
||||
self.execute_plan()
|
||||
|
||||
|
||||
if self.destination_fp is not None:
|
||||
self.destination_fp.close()
|
||||
|
||||
self.progress(FINISHING, 0.0)
|
||||
|
||||
self.create_empty_new_files()
|
||||
self.rename_new_files()
|
||||
self.remove_old_files()
|
||||
self.set_xbit()
|
||||
|
||||
def progress(self, message, done):
|
||||
"""
|
||||
Called to report progress.
|
||||
|
||||
`message`
|
||||
A human readable message.
|
||||
|
||||
`done`
|
||||
The amount of progress that is done, between 0.0 and 1.0.
|
||||
"""
|
||||
|
||||
if self.progress_callback is not None:
|
||||
self.progress_callback(message, done)
|
||||
else:
|
||||
print("Progress: %s: %.4f" % (message, 100.0 * done))
|
||||
|
||||
def log(self, message, *args):
|
||||
if self.logfile is not None:
|
||||
print(message % args, file=self.logfile)
|
||||
|
||||
def delete(self, filename):
|
||||
"""
|
||||
Try very hard to delete `filename`. If it can't be deleted, move it
|
||||
to the deleted directory, to be cleaned up the next time the game
|
||||
starts.
|
||||
"""
|
||||
|
||||
if not os.path.exists(filename):
|
||||
return
|
||||
|
||||
try:
|
||||
os.unlink(filename)
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
serial = 0
|
||||
|
||||
while True:
|
||||
serial += 1
|
||||
new = os.path.join(self.deleteddir, "%s.delete.%d.rpu" % (basename, serial))
|
||||
|
||||
if not os.path.exists(new):
|
||||
os.rename(filename, new)
|
||||
break
|
||||
|
||||
def rename(self, old, new):
|
||||
try:
|
||||
os.rename(old, new)
|
||||
except:
|
||||
self.delete(new)
|
||||
os.rename(old, new)
|
||||
|
||||
def make_directories(self):
|
||||
"""
|
||||
Creates the directories in self.new_directories.
|
||||
"""
|
||||
|
||||
directories = list(self.new_directories)
|
||||
directories.sort(key=lambda i : i.name)
|
||||
|
||||
for d in directories:
|
||||
if not os.path.exists(os.path.join(self.targetdir, d.name)):
|
||||
os.makedirs(os.path.join(self.targetdir, d.name))
|
||||
|
||||
def find_incomplete_files(self):
|
||||
"""
|
||||
Scan a directory, recursively, and add the files and directories
|
||||
found to this file test. This is intended for testing. This does
|
||||
not call .scan on the files.
|
||||
"""
|
||||
|
||||
root = self.targetdir
|
||||
|
||||
for dn, dirs, files in os.walk(root):
|
||||
|
||||
for fn in files:
|
||||
fn = os.path.join(dn, fn)
|
||||
|
||||
if not fn.endswith(".new.rpu"):
|
||||
continue
|
||||
|
||||
oldfn = fn[:-len(".new.rpu")] + ".old.rpu"
|
||||
self.rename(fn, oldfn)
|
||||
|
||||
relfn = os.path.relpath(oldfn, root)
|
||||
f = common.File(relfn, data_filename=oldfn)
|
||||
self.old_files.append(f)
|
||||
|
||||
def write_padding(self):
|
||||
"""
|
||||
Writes a file containing the padding for RPAs, so it's
|
||||
not necessary to download a block file just for that.
|
||||
"""
|
||||
|
||||
padding = b"Made with Ren'Py."
|
||||
|
||||
fn = os.path.join(self.targetdir, "_padding.old.rpa")
|
||||
with open(fn, "wb") as f:
|
||||
f.write(padding)
|
||||
|
||||
f = common.File("_padding.old.rpa", data_filename=fn)
|
||||
self.old_files.append(f)
|
||||
|
||||
def scan_old_files(self):
|
||||
"""
|
||||
Scans the old files, generating a list of segments.
|
||||
"""
|
||||
|
||||
total = 0
|
||||
done = 0
|
||||
|
||||
existing = [ ]
|
||||
|
||||
for i in self.old_files:
|
||||
i.add_data_filename(self.targetdir)
|
||||
|
||||
if not os.path.exists(i.data_filename):
|
||||
continue
|
||||
|
||||
existing.append(i)
|
||||
total += os.path.getsize(i.data_filename)
|
||||
|
||||
self.old_files = existing
|
||||
|
||||
total = max(1, total)
|
||||
|
||||
for i in self.old_files:
|
||||
i.scan()
|
||||
done += os.path.getsize(i.data_filename)
|
||||
|
||||
self.progress(PREPARING, done / total)
|
||||
|
||||
def prepare_new_files(self):
|
||||
"""
|
||||
Prepares the new files.
|
||||
"""
|
||||
|
||||
self.disk_total = 0
|
||||
|
||||
for i in self.new_files:
|
||||
i.add_data_filename(self.targetdir)
|
||||
|
||||
for s in i.segments:
|
||||
self.disk_total += s.size
|
||||
|
||||
def remove_identical_files(self):
|
||||
"""
|
||||
Removes from self.source_files any file that exists and is identical
|
||||
to the file in self.target_files.
|
||||
"""
|
||||
|
||||
old_by_name = { i.name : i for i in self.old_files }
|
||||
|
||||
new_files = [ ]
|
||||
|
||||
self.log("Removing identical files:")
|
||||
|
||||
for f in self.new_files:
|
||||
if f.name not in old_by_name:
|
||||
new_files.append(f)
|
||||
self.log(" new %s", f.name)
|
||||
continue
|
||||
|
||||
if f.segments != old_by_name[f.name].segments:
|
||||
new_files.append(f)
|
||||
self.log(" changed %s", f.name)
|
||||
continue
|
||||
|
||||
self.log(" same %s", f.name)
|
||||
|
||||
self.log("%d files are unchanged.", len(self.new_files) - len(new_files))
|
||||
self.log("%d files are new/changed.", len(new_files))
|
||||
|
||||
self.new_files = new_files
|
||||
|
||||
def create_plan(self):
|
||||
"""
|
||||
Creates the plan for updating everything, by finding each missing
|
||||
segment and creating a plan object.
|
||||
"""
|
||||
|
||||
# A map from segment hash to (block, file, segment) tuples.
|
||||
segment_locations = { }
|
||||
|
||||
for f in sorted(self.block_files, key=lambda i : i.mtime):
|
||||
for s in f.segments:
|
||||
segment_locations[s.hash] = (True, f, s)
|
||||
|
||||
for f in sorted(self.old_files, key=lambda i : i.mtime):
|
||||
for s in f.segments:
|
||||
segment_locations[s.hash] = (False, f, s)
|
||||
|
||||
plan = [ ]
|
||||
|
||||
for target_file in self.new_files:
|
||||
|
||||
for target_segment in target_file.segments:
|
||||
block, source_file, source_segment = segment_locations.get(target_segment.hash, (False, None, None))
|
||||
|
||||
if source_file is None:
|
||||
self.log("Segment %s was not found.", target_segment.hash)
|
||||
raise Exception("Segment %s was not found in index.")
|
||||
|
||||
plan.append(Plan(
|
||||
block,
|
||||
source_file.data_filename or source_file.name,
|
||||
source_segment.offset,
|
||||
source_segment.size,
|
||||
source_segment.compressed,
|
||||
target_file.data_filename,
|
||||
target_segment.offset,
|
||||
target_segment.size,
|
||||
target_segment.hash,
|
||||
))
|
||||
|
||||
plan.sort(key=lambda p : (p.block, p.old_filename, p.old_offset, p.old_size))
|
||||
|
||||
self.log("Created a plan with %d entries.", len(plan))
|
||||
|
||||
self.plan = plan
|
||||
|
||||
def compute_totals(self):
|
||||
"""
|
||||
Computes the total number of bytes to download and write.
|
||||
"""
|
||||
|
||||
self.download_total = 0
|
||||
self.write_total = 0
|
||||
|
||||
download_set = set()
|
||||
|
||||
for p in self.plan:
|
||||
self.write_total += p.new_size
|
||||
|
||||
if p.block:
|
||||
|
||||
key = (p.old_filename, p.old_offset, p.old_size)
|
||||
|
||||
if key not in download_set:
|
||||
download_set.add(key)
|
||||
self.download_total += p.old_size
|
||||
|
||||
|
||||
# Make sure we can't divide by zero.
|
||||
self.download_total = max(self.download_total, 1)
|
||||
self.write_total = max(self.write_total, 1)
|
||||
|
||||
def find_removals(self):
|
||||
"""
|
||||
Find the set of files that exist in old but not in new.
|
||||
"""
|
||||
|
||||
old = { i.data_filename for i in self.old_files }
|
||||
new = { i.data_filename for i in self.new_files }
|
||||
|
||||
self.removals = old - new
|
||||
|
||||
def write_destination(self, filename, offset, data):
|
||||
"""
|
||||
Writes data to the destination file at the given offset.
|
||||
"""
|
||||
|
||||
filename = filename + ".new.rpu"
|
||||
|
||||
if self.destination_filename != filename:
|
||||
if self.destination_fp is not None:
|
||||
self.destination_fp.close()
|
||||
|
||||
self.destination_filename = filename
|
||||
if os.path.exists(filename):
|
||||
self.destination_fp = open(os.path.join(self.targetdir, filename), "r+b")
|
||||
else:
|
||||
self.destination_fp = open(os.path.join(self.targetdir, filename), "wb")
|
||||
|
||||
self.destination_fp.seek(offset)
|
||||
self.destination_fp.write(data)
|
||||
|
||||
self.write_done += len(data)
|
||||
self.download_patch_progress()
|
||||
|
||||
def download_patch_progress(self):
|
||||
|
||||
done = 0.5 * self.download_done / self.download_total
|
||||
done += 0.5 * self.write_done / self.write_total
|
||||
|
||||
done = min(done, 1.0)
|
||||
|
||||
self.progress(DOWNLOADING, done)
|
||||
|
||||
def download_block_file(self, filename, plan):
|
||||
"""
|
||||
Downloads the portions of the block file that are needed.
|
||||
"""
|
||||
|
||||
ranges = { (i.old_offset, i.old_size) for i in plan }
|
||||
ranges = list(ranges)
|
||||
|
||||
url = self.url + "/" + filename
|
||||
filename = os.path.join(self.targetdir, "update", filename)
|
||||
|
||||
old_download_done = self.download_done
|
||||
|
||||
def download_progress(done, total):
|
||||
self.download_done = old_download_done + done
|
||||
self.download_patch_progress()
|
||||
|
||||
download.download(url, ranges, filename, download_progress)
|
||||
|
||||
return filename
|
||||
|
||||
def execute_file_plan(self, plan):
|
||||
"""
|
||||
This executes the plan for one source file.
|
||||
"""
|
||||
|
||||
old_filename = plan[0].old_filename
|
||||
block = plan[0].block
|
||||
|
||||
if block:
|
||||
old_filename = self.download_block_file(old_filename, plan)
|
||||
|
||||
# From here on, source_filename must point to a complete file on disk.
|
||||
|
||||
with open(old_filename, "rb") as f:
|
||||
|
||||
hash = None
|
||||
data = b''
|
||||
|
||||
for p in plan:
|
||||
|
||||
self.log("%s (%d, %d)\n -> %s (%d, %d) %s", p.old_filename, p.old_offset, p.old_size, p.new_filename, p.new_offset, p.new_size, "compressed" if p.compressed else "")
|
||||
|
||||
if hash != p.hash:
|
||||
f.seek(p.old_offset)
|
||||
data = f.read(p.old_size)
|
||||
|
||||
if p.compressed == common.COMPRESS_ZLIB:
|
||||
data = zlib.decompress(data)
|
||||
|
||||
hash = common.hash_data(data)
|
||||
|
||||
if hash != p.hash:
|
||||
self.log("Hash mismatch on %s offset %d size %d.", p.old_filename, p.old_offset, p.old_size)
|
||||
raise UpdateError("Hash mismatch on %s offset %d size %d." % (p.old_filename, p.old_offset, p.old_size))
|
||||
|
||||
self.write_destination(p.new_filename, p.new_offset, data)
|
||||
|
||||
if block:
|
||||
self.log("Blockfile delete %s.", old_filename)
|
||||
self.delete(old_filename)
|
||||
else:
|
||||
if self.aggressive_removal and old_filename in self.removals:
|
||||
self.log("Aggressively delete %s.", old_filename)
|
||||
self.delete(old_filename)
|
||||
|
||||
def execute_plan(self):
|
||||
"""
|
||||
This executes the full plan, one file at a time.
|
||||
"""
|
||||
|
||||
queue = [ ]
|
||||
old_key = (False, None)
|
||||
|
||||
for p in self.plan:
|
||||
key = (p.block, p.old_filename)
|
||||
|
||||
if key != old_key:
|
||||
if queue:
|
||||
self.execute_file_plan(queue)
|
||||
|
||||
queue = [ ]
|
||||
|
||||
old_key = key
|
||||
|
||||
queue.append(p)
|
||||
|
||||
if queue:
|
||||
self.execute_file_plan(queue)
|
||||
|
||||
def create_empty_new_files(self):
|
||||
"""
|
||||
Creates new files that do not have any data.
|
||||
"""
|
||||
|
||||
for i in self.new_files:
|
||||
if not i.segments:
|
||||
self.log("Create empty file %s.", i.data_filename)
|
||||
with open(i.data_filename + ".new.rpu", "wb") as f:
|
||||
pass
|
||||
|
||||
def rename_new_files(self):
|
||||
"""
|
||||
Renames the new files to final names.
|
||||
"""
|
||||
|
||||
for f in self.new_files:
|
||||
filename = os.path.join(self.targetdir, f.name)
|
||||
if os.path.exists(filename + ".new.rpu"):
|
||||
self.rename(filename + ".new.rpu", filename)
|
||||
|
||||
def remove_old_files(self):
|
||||
"""
|
||||
Removes the old files and directories.
|
||||
"""
|
||||
|
||||
for i in self.removals:
|
||||
self.log("Final delete %s.", i)
|
||||
self.delete(i)
|
||||
|
||||
directories = set(i.name for i in self.old_directories) - set(i.name for i in self.new_directories)
|
||||
|
||||
for i in reversed(sorted(directories)):
|
||||
self.log("Remove directory %s.", i)
|
||||
try:
|
||||
os.rmdir(os.path.join(self.targetdir, i))
|
||||
except:
|
||||
pass
|
||||
|
||||
def set_xbit(self):
|
||||
"""
|
||||
Sets the executable bit on files that require it.
|
||||
"""
|
||||
|
||||
for i in self.new_files:
|
||||
if i.xbit:
|
||||
try:
|
||||
os.chmod(i.data_filename, 0o755)
|
||||
except:
|
||||
raise UpdateError("Could not set the executable bit on %s." % i.data_filename)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("url")
|
||||
ap.add_argument("targetdir")
|
||||
|
||||
args = ap.parse_args()
|
||||
|
||||
targetlist = common.FileList()
|
||||
targetlist.scan(args.targetdir, data_filename=False)
|
||||
|
||||
resp = requests.get(args.url + "/game.files.rpu")
|
||||
sourcelist = common.FileList.decode(resp.content)
|
||||
|
||||
Update(args.url, [ sourcelist ], args.targetdir, [ targetlist ])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+2
-2
@@ -51,8 +51,8 @@ class Version(object):
|
||||
Version("main", 3, "8.2.0", "TBD")
|
||||
Version("main", 2, "7.7.0", "TBD")
|
||||
|
||||
Version("fix", 3, "8.1.3", "Where No One Has Gone Before")
|
||||
Version("fix", 2, "7.6.3", "To Boldy Go")
|
||||
Version("fix", 3, "8.1.4", "Where No One Has Gone Before")
|
||||
Version("fix", 2, "7.6.4", "To Boldy Go")
|
||||
|
||||
def make_dict(branch, suffix="00000000", official=False, nightly=False):
|
||||
"""
|
||||
|
||||
@@ -14,8 +14,8 @@ sleep 2
|
||||
./build.sh
|
||||
)
|
||||
|
||||
nice rpy build --python 3
|
||||
nice rpy build --python 2
|
||||
rpy build --python 3
|
||||
rpy build --python 2
|
||||
|
||||
nice lib/py3-linux-x86_64/python distribute.py
|
||||
nice lib/py2-linux-x86_64/python distribute.py
|
||||
lib/py3-linux-x86_64/python distribute.py
|
||||
lib/py2-linux-x86_64/python distribute.py
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
python early:
|
||||
config.generating_documentation = True
|
||||
|
||||
init 1000000 python:
|
||||
import doc
|
||||
import shaderdoc
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 481px) {
|
||||
div[role="search"] {
|
||||
[role="search"] {
|
||||
top: 12.944px;
|
||||
margin-bottom: 12.944px;
|
||||
width: 274.125px;
|
||||
|
||||
+43
-37
@@ -1,5 +1,3 @@
|
||||
.. _building-distributions:
|
||||
|
||||
Building Distributions
|
||||
======================
|
||||
|
||||
@@ -27,22 +25,22 @@ Macintosh
|
||||
as a single file. The updater does not work with this package.
|
||||
|
||||
Windows
|
||||
A zip file targeting Windows x86_64.
|
||||
A zip file targeting Windows x86_64.
|
||||
|
||||
Windows, Mac, and Linux for Markets
|
||||
A distribution that contains the information required to run on
|
||||
software markets like itch.io and Steam. This isn't meant to be
|
||||
run directly (and probably won't work on the Mac), but should be
|
||||
fed to the app store upload process.
|
||||
A distribution that contains the information required to run on
|
||||
software markets like itch.io and Steam. This isn't meant to be
|
||||
run directly (and probably won't work on the Mac), but should be
|
||||
fed to the app store upload process.
|
||||
|
||||
.. warning::
|
||||
|
||||
The zip and tar.bz2 files that Ren'Py produces contain permissions
|
||||
information that must be present for Ren'Py to run on Linux and
|
||||
Macintosh.
|
||||
The zip and tar.bz2 files that Ren'Py produces contain permissions
|
||||
information that must be present for Ren'Py to run on Linux and
|
||||
Macintosh.
|
||||
|
||||
Unpacking and re-packing a zip file on Windows and then running it
|
||||
on Linux or Macintosh is not supported.
|
||||
Unpacking and re-packing a zip file on Windows and then running it
|
||||
on Linux or Macintosh is not supported.
|
||||
|
||||
Basic Configuration
|
||||
-------------------
|
||||
@@ -63,31 +61,31 @@ use.
|
||||
|
||||
.. var:: build.directory_name = "..."
|
||||
|
||||
This is used to create the names of directories in the archive
|
||||
files. For example, if this is set to "mygame-1.0", the Linux
|
||||
version of the project will unpack to "mygame-1.0-linux".
|
||||
This is used to create the names of directories in the archive
|
||||
files. For example, if this is set to "mygame-1.0", the Linux
|
||||
version of the project will unpack to "mygame-1.0-linux".
|
||||
|
||||
This is also used to determine the name of the directory in
|
||||
which the package files are placed. For example, if you set
|
||||
build.directory_name to mygame-1.0, the archive files will
|
||||
be placed in mygame-1.0-dists in the directory above the base
|
||||
directory.
|
||||
This is also used to determine the name of the directory in
|
||||
which the package files are placed. For example, if you set
|
||||
build.directory_name to mygame-1.0, the archive files will
|
||||
be placed in mygame-1.0-dists in the directory above the base
|
||||
directory.
|
||||
|
||||
This variable should not contain special characters like spaces,
|
||||
colons, and semicolons. If not set, it defaults to :var:`build.name`
|
||||
a dash, and :var:`config.version`.
|
||||
This variable should not contain special characters like spaces,
|
||||
colons, and semicolons. If not set, it defaults to :var:`build.name`
|
||||
a dash, and :var:`config.version`.
|
||||
|
||||
.. var:: build.executable_name = "..."
|
||||
|
||||
This variable controls the name of the executables that the user
|
||||
clicks on to start the game.
|
||||
This variable controls the name of the executables that the user
|
||||
clicks on to start the game.
|
||||
|
||||
This variable should not contain special characters like spaces,
|
||||
colons, and semicolons. If not set, it defaults to :var:`build.name`.
|
||||
This variable should not contain special characters like spaces,
|
||||
colons, and semicolons. If not set, it defaults to :var:`build.name`.
|
||||
|
||||
For example, if this is set to "mygame", the user will be able
|
||||
to run mygame.exe on Windows, mygame.app on Macintosh, and
|
||||
mygame.sh on Linux.
|
||||
For example, if this is set to "mygame", the user will be able
|
||||
to run mygame.exe on Windows, mygame.app on Macintosh, and
|
||||
mygame.sh on Linux.
|
||||
|
||||
.. _special-files:
|
||||
|
||||
@@ -173,15 +171,15 @@ must match at least one character.
|
||||
|
||||
For example::
|
||||
|
||||
# Include README.txt
|
||||
build.classify("README.txt", "all")
|
||||
# Include README.txt
|
||||
build.classify("README.txt", "all")
|
||||
|
||||
# But exclude all other txt files.
|
||||
build.classify("**.txt", None)
|
||||
# But exclude all other txt files.
|
||||
build.classify("**.txt", None)
|
||||
|
||||
# Add png and jpg files in the game directory into an archive.
|
||||
build.classify("game/**.png", "archive")
|
||||
build.classify("game/**.jpg", "archive")
|
||||
# Add png and jpg files in the game directory into an archive.
|
||||
build.classify("game/**.png", "archive")
|
||||
build.classify("game/**.jpg", "archive")
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
@@ -420,3 +418,11 @@ The following variables provide further control of the build process:
|
||||
|
||||
This is a dictionary mapping strings to strings, that can be used to
|
||||
add or override keys in the mac's Info.plist file.
|
||||
|
||||
.. var:: build.update_formats = [ "rpu" ]
|
||||
|
||||
|
||||
This is a list of formats that the updater will build. The default,
|
||||
"rpu" is supported from Ren'Py 7.7 and 8.2 on. If you need to support
|
||||
updating using the earlier zsync-based updates, add "zsync' to the
|
||||
list.
|
||||
|
||||
+178
-34
@@ -10,43 +10,91 @@ Changelog (Ren'Py 7.x-)
|
||||
8.2.0 / 7.7.0
|
||||
=============
|
||||
|
||||
Emoji and Other Text Improvements
|
||||
---------------------------------
|
||||
|
||||
Ren'Py's text handing has gotten an overhaul in this release, with the
|
||||
intent of supporting Emoji and scripts that require complext text shaping,
|
||||
like Indic languages. This required many changes.
|
||||
Harfbuzz Integration
|
||||
--------------------
|
||||
|
||||
The first was a change in Ren'Py's defaults to enable bytecode hinting of
|
||||
fonts. This may cause some of the fonts to change shape slightly, but
|
||||
is required to allow script-like fonts to combine properly.
|
||||
Ren'Py now uses the Harfbuzz library to perform text shaping. On all
|
||||
versions of Ren'Py, Harfbuzz is used to supply additional information
|
||||
to the freetype authinter.
|
||||
|
||||
The second change is only enabled in Ren'Py 8, and that's the use of the
|
||||
Harfbuzz library to shape text. Harfbuzz is used reorder and select different
|
||||
glyphs based on the context they're in and the language of the text provided.
|
||||
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.
|
||||
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.)
|
||||
|
||||
Next, Ren'Py has gained the ability to render fonts that use the COLR section
|
||||
to provide color glyphs. These are general fonts used for Emoji. Ren'Py ships
|
||||
with a font that contains the Twiemoji image, which covers a majority of the
|
||||
Emoji in use.
|
||||
The new :propref:`shaper` style property controls the shaper used to text,
|
||||
for compatibility with older versions of Ren'Py.
|
||||
|
||||
Ren'Py now automatically switches to the Emoji font when it encounters Emoji
|
||||
characters in text. The supported characters are those in the `Emoji 15 <https://unicode.org/Public/emoji/15.0/emoji-test.txt>`_
|
||||
set. Harfbuzz support is required to render joining sequences of Emoji,
|
||||
|
||||
Emoji-Related Text Improvements
|
||||
-------------------------------
|
||||
|
||||
Next, Ren'Py has gained the ability to render fonts that use the COLRv0 standard
|
||||
to provide color glyphs. Ren'Py ships with a font that contains the Twiemoji images,
|
||||
which covers a majority of the Emoji in use (but not all of them).
|
||||
|
||||
Ren'Py will automatically switch to the Emoji font when it encounters Emoji
|
||||
characters in text. The supported characters are those in the
|
||||
`Emoji 15.1 <https://unicode.org/Public/emoji/15.1/emoji-test.txt>`_ standard.
|
||||
|
||||
Ren'Py 8 with Harfbuzz shaping is required to render joining sequences of Emoji,
|
||||
including things like gender and skin-tone modifiers, so you'll need Ren'Py 8
|
||||
to have those work. This switching occurrs when a font tag is not being
|
||||
used.
|
||||
|
||||
The new :propref:`emoji_font`, :propref:`prefer_emoji` and :propref:`shaper`
|
||||
style properties help control these new text rendering features.
|
||||
The new :propref:`emoji_font`, :propref:`prefer_emoji` style properties control
|
||||
Emoji font selection.
|
||||
|
||||
Fundamentally, you can include Emoji into your game by typing it into your
|
||||
script as character dialogue. For example::
|
||||
|
||||
e "I'm feeling 😃 today."
|
||||
|
||||
Text in Indic languages should also be supported, but you'll need to find a
|
||||
font that supports the language you're using.
|
||||
Variable Fonts
|
||||
--------------
|
||||
|
||||
Ren'Py now supports OpenType variable fonts. These are fonts that use
|
||||
one or more axes of variablity to change how the font is rendered. For
|
||||
example, a font may have the "weight" axis, which controls how bold the
|
||||
font is, and the "width" axis, which controls how wide the font is.
|
||||
|
||||
Variable font support required Ren'Py 8 and the harfbuzz shaper to work.
|
||||
|
||||
To support variable fonts, Ren'Py has added the :propref:`instance` and
|
||||
:propref:`axis` style properties, and the :tt:`instance` and :tt:`axis`
|
||||
text tags, as well as the :func:`renpy.variable_font_info` function.
|
||||
|
||||
See the :ref:`variable fonts documentation <variable-fonts>` for more information.
|
||||
|
||||
Font Hinting
|
||||
------------
|
||||
|
||||
There is a new :propref:`hinting` mode "auto-light", that performs autohinting
|
||||
in the vertical but not horizontal direction.
|
||||
|
||||
The new :var:`config.font_hinting` variable makes it possible to enable
|
||||
font hinting on a per-font basis, while the style system can be used to
|
||||
control hinting per-use. For example::
|
||||
|
||||
define config.font_hinting["MyFont.tff"] = "bytecode"
|
||||
|
||||
enables bytecode hinting for MyFont.ttf.
|
||||
|
||||
Interpolation Improvements
|
||||
--------------------------
|
||||
|
||||
When a variable is interpolated into a string, and the interpolation namespace
|
||||
exists, that namespace will be searched for the values to interpolate. For
|
||||
example, ::
|
||||
|
||||
define t = "Not shown."
|
||||
define interpolate.t = "Shown."
|
||||
|
||||
label start:
|
||||
e "[t]" # Will show "Shown."
|
||||
|
||||
|
||||
Speech Bubble Improvements
|
||||
--------------------------
|
||||
@@ -62,9 +110,67 @@ that filter the list of bubble property names based on the image tag
|
||||
that's speaking. This makes it possible to have bubbles that are
|
||||
specific to some but not all characters.
|
||||
|
||||
Developer Tools
|
||||
---------------
|
||||
|
||||
There is a new "Skip splashscreen" option in Options section
|
||||
of the launcher preferences. When checked, this will cause
|
||||
games launched to skip the splashscreen label when starting.
|
||||
|
||||
A new 'Show Filename and Line' option is avilable from the
|
||||
shift+D developer menu. When enabled, this will cause the
|
||||
filename and line number of the current statement to be
|
||||
displayed. Clicking on the filename and line will open
|
||||
the file in the default text editor, at the given line,
|
||||
if possible.
|
||||
|
||||
Actions and Screen Values
|
||||
-------------------------
|
||||
|
||||
The :ref:`data-actions` are now presented and explained in a more condensed
|
||||
manner. These actions have been reimplemented using a data manager that
|
||||
describes what to do with the data (Set-, Toggle-, Cycle-, Increment-) and a
|
||||
data accessor that describes the kind of data to change
|
||||
(-Variable, -ScreenVariable, -LocalVariable, -Field, -Dict).
|
||||
|
||||
There are two new managers:
|
||||
|
||||
* The Cycle- actions (CycleVariable, CycleLocalVariable, CycleField...) take a
|
||||
list of values and each time the action is run (i.e each time the button is
|
||||
clicked), the target value is set to be the next element in the list.
|
||||
* The Increment- actions (IncrementVariable, IncrementDict, IncrementField...)
|
||||
add a certain value (by default, 1) to the target value. These can also be
|
||||
used to decrement the field.
|
||||
|
||||
The :class:`LocalVariableValue` bar value and :class:`LocalVariableInputValue` input
|
||||
values have been added, for completeness.
|
||||
|
||||
:ref:`input-values` now take an optional `disable_on_enter` parameter which
|
||||
makes them disable themselves when the enter key is pressed.
|
||||
|
||||
HTTPS/HTTP Fetch
|
||||
----------------
|
||||
|
||||
Ren'Py now has better support for :doc:`fetch`, using the new renpy.fetch
|
||||
function. While the Requests library still remains supported on Desktop and Mobile,
|
||||
(it's used internally by Ren'Py), the new fetch function:
|
||||
|
||||
* Support GET, POST, and PUT requests for HTTPS and HTTP URLs.
|
||||
* Supports fetching from the web platform, subject to the rules of the web platform.
|
||||
* Will not block the game while downloading.
|
||||
* Can take data as either bytes or objects that be encoded to JSON.
|
||||
* Can return data as bytes, as string, or objects decoded from JSON.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
The new :func:`EditFile` action attempts to open a file and
|
||||
line in a text editor.
|
||||
|
||||
The virtual dpi of an SVG file can be set with the new `dpi`
|
||||
parameter to :func:`Image`.
|
||||
|
||||
The new :func:`CopyToClipboard` action copies text to the clipboard.
|
||||
|
||||
The new :func:`renpy.confirm` function provides a way of using
|
||||
@@ -96,6 +202,9 @@ The ``jump expression`` statement can now take a local label name of the form
|
||||
".local_name". Previously, only "global_name" or "global_name.local_name" were
|
||||
allowed.
|
||||
|
||||
:ref:`creator-defined-sl` can now copy all properties from other screen
|
||||
language statements.
|
||||
|
||||
|
||||
Other Changes
|
||||
-------------
|
||||
@@ -133,6 +242,44 @@ be deferred until the lock files are removed when the git operation
|
||||
completes.
|
||||
|
||||
|
||||
.. _renpy-8.1.3:
|
||||
.. _renpy-7.6.3:
|
||||
|
||||
8.1.3 / 7.6.3
|
||||
=============
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
Ren'Py now considers :var:`config.gl2` to be true on macOS. This is because
|
||||
there are several fixes for window resizing on newer versions of macOS
|
||||
in the gl2 renderer that are not present in the gl renderer.
|
||||
|
||||
MMX acceleration for video playback has been re-enabled on Windows and
|
||||
Linux.
|
||||
|
||||
The way the Steam Deck keyboard is shown has changed. They keyboard
|
||||
is now shown, once, when a text input is displayed. By default, the
|
||||
keyboard is shown at the top of the screen, and the keyboard will
|
||||
only be shown once. If it's hidden (for example, the Steam button
|
||||
is pressed), the player needs to hit Steam+X to show it. This works
|
||||
around issues with the Steam Deck.
|
||||
|
||||
The 32-bit windows Live2D library will be installed into Ren'Py 7.
|
||||
You may need to reinstall Live2D to get this library.
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
An issue that prevented keys from being bound to text (for example,
|
||||
keysyms like "r" rather than "K_r") has been fixed.
|
||||
|
||||
There have been several documentation fixes.
|
||||
|
||||
An issue with rollback not working at the start of the game has been
|
||||
fixed.
|
||||
|
||||
|
||||
.. _renpy-8.1.2:
|
||||
.. _renpy-7.6.2:
|
||||
|
||||
@@ -146,7 +293,7 @@ There have been many documentation improvements.
|
||||
|
||||
When using :func:`renpy.classify`, a directory will now match patterns
|
||||
that do not end with /. (For example, "renpy.app" will match the renpy.app
|
||||
directory.
|
||||
directory).
|
||||
|
||||
ATL has been changed to use a deep compare to determine if a transform should
|
||||
be continued or restarted. This means a transform will restart if global
|
||||
@@ -154,13 +301,13 @@ variables it uses are changed.
|
||||
|
||||
The styles of a viewport's children will not change when it gains drag
|
||||
focus. This was rarely used, and the style change could cause drags to
|
||||
be slow or missed.
|
||||
be slow or to miss.
|
||||
|
||||
Load will now roll the game back to the statement after the last statement
|
||||
that interacted to the user. (Previously, it would roll back to the start
|
||||
of the current statement.) This makes rollback on load match other rollbacks.
|
||||
|
||||
The :func:`_autosave` variable now takes precedence over forced autosaves,
|
||||
The :var:`_autosave` variable now takes precedence over forced autosaves,
|
||||
including those on quit and at choice menus.
|
||||
|
||||
PYTHON* variables are filtered from the environment when launching a
|
||||
@@ -178,13 +325,11 @@ pixel in size in both dimensions.
|
||||
|
||||
:func:`renpy.pause` can now roll forward to calls and jumps from screens.
|
||||
|
||||
On the web browser, the Window preference now disables fullscreen mode.
|
||||
On the web browser, the ``"display" : "window"`` preference now disables
|
||||
fullscreen mode.
|
||||
|
||||
It is now possible to bind mouse buttons to skipping.
|
||||
|
||||
MMX acceleration for video playback has been re-enabled on Windows and
|
||||
Linux.
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
@@ -194,7 +339,7 @@ The Ren'Py 8 launcher can now launch games on Windows systems where the
|
||||
path to Ren'Py is not representable in the system encoding.
|
||||
|
||||
The functionality to import Python from the game/ directory has been
|
||||
improved to better comply with Python's PEP 302.
|
||||
improved to better comply with Python's :pep:`302`.
|
||||
|
||||
:func:`GamepadExist` now works as documented. As a byproduct of this fix,
|
||||
the gamepad screen will be displayed in Help when in developer mode.
|
||||
@@ -206,8 +351,8 @@ available.
|
||||
Viewport inertia continues even if the interaction restarts during the
|
||||
animation.
|
||||
|
||||
The if_changed clause to ``play`` (and :func:`renpy.music.play`) now
|
||||
considers and preserves looping.
|
||||
The if_changed clause to :ref:`play <play-statement>` (and
|
||||
:func:`renpy.music.play`) now considers and preserves looping.
|
||||
|
||||
VS Code launch has been fixed on Linux.
|
||||
|
||||
@@ -217,7 +362,6 @@ Movie functions now ensure the relevant channels exist before playing. This
|
||||
can fix issue caused by loading a Movie from a save file.
|
||||
|
||||
|
||||
|
||||
.. _renpy-8.1.1:
|
||||
.. _renpy-7.6.1:
|
||||
|
||||
@@ -752,7 +896,7 @@ clicking on the attribute name.
|
||||
|
||||
The :func:`Text` displayable now takes a new `tokenized` argument. When
|
||||
true, the Text displayable expects to take a list of tokens taken from
|
||||
a :ref:`custom text tag <custom-text-tags>`.
|
||||
a :doc:`custom text tag <custom_text_tags>`.
|
||||
|
||||
Two new layers are now part of Ren'Py. The "top" layer is displayed above
|
||||
all other layers, and does not participate in transitions. This makes
|
||||
|
||||
@@ -565,6 +565,15 @@ Occasionally Used
|
||||
The user can progress forward through the rollback buffer by
|
||||
clicking.
|
||||
|
||||
.. var:: config.font_hinting = { None : "auto" }
|
||||
|
||||
This is a dictionary from a string containing the font filename to a string
|
||||
giving one of the font hinting modes in :propref:`hinting`. When
|
||||
:propref:`hinting` is True, the value is looked up in this dictionary,
|
||||
and the resulting mode is used.
|
||||
|
||||
If no key is found, None is looked up, and the resulting mode is used.
|
||||
|
||||
.. var:: config.font_name_map = { }
|
||||
|
||||
This is a map from (font name) to (font filepath/fontgroup). Font names
|
||||
@@ -1507,8 +1516,9 @@ Rarely or Internally Used
|
||||
upper-left corner of the layer, with height and width giving the
|
||||
layer size.
|
||||
|
||||
If a layer is not mentioned in config.layer_clipping, then it is
|
||||
assumed to take up the full screen.
|
||||
If a layer is not mentioned in config.layer_clipping, then it will
|
||||
take up the full size of its container. Typically this will be the
|
||||
screen, unless being shown inside a :class:`Layer` displayable.
|
||||
|
||||
.. var:: config.layeredimage_offer_screen = True
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.. _custom-text-tags:
|
||||
|
||||
================
|
||||
================
|
||||
Custom Text Tags
|
||||
================
|
||||
|
||||
@@ -9,8 +7,7 @@ can manipulate the text and text tags defined within, including adding
|
||||
and removing text and other text tags.
|
||||
|
||||
Custom text tags are created by assigning a text tag function to an
|
||||
entry in the config.custom_text_tags dictionary or the
|
||||
config.self_closing_custom_tags dictionary.
|
||||
entry in one of the following dictionaries.
|
||||
|
||||
.. var:: config.custom_text_tags = { }
|
||||
|
||||
@@ -23,6 +20,14 @@ config.self_closing_custom_tags dictionary.
|
||||
Maps text tag names to a self-closing text tag functions, when the text tag
|
||||
does not wrap other text.
|
||||
|
||||
.. warning::
|
||||
|
||||
The functions may be called at any moment as part of the prediction
|
||||
process. For that reason, their behavior should not depend on the state
|
||||
of the game (global variables, typically) and they should not induce
|
||||
side-effects (calling :doc:`statement_equivalents` for example, or
|
||||
changing global variables).
|
||||
|
||||
A text tag function takes three arguments: The tag itself, the argument
|
||||
for the tag, and a list of content tuples. For example, for the text::
|
||||
|
||||
@@ -65,12 +70,11 @@ the third argument.
|
||||
Lists of tokens can be passed to :func:`Text` when its `tokenized`
|
||||
argument is True.
|
||||
|
||||
Caveats
|
||||
-------
|
||||
.. warning::
|
||||
|
||||
The dialogue text tags {p}, {w}, {nw}, and {fast} are processed before
|
||||
custom text tags, and should either be not included inside a custom
|
||||
text tag, or passed through unchanged.
|
||||
The dialogue text tags {p}, {w}, {nw}, and {fast} are processed before
|
||||
custom text tags, and should either be not included inside a custom
|
||||
text tag, or passed through unchanged.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
@@ -274,22 +274,20 @@ These displayables are used to create certain visual effects.
|
||||
.. include:: inc/disp_effects
|
||||
|
||||
Image Manipulators
|
||||
-------------------
|
||||
------------------
|
||||
|
||||
An :doc:`image manipulator <im>` is a displayable that takes an image or image
|
||||
manipulator, and either loads it or performs an operation on it.
|
||||
Image manipulators can only take images or other
|
||||
image manipulators as input.
|
||||
:doc:`Image manipulators <im>` are an historic kind of displayables that
|
||||
apply transformations or operations exclusively to other images or image
|
||||
manipulators - to the exclusion of the other kinds of displayables.
|
||||
|
||||
An image manipulator can be used any place a displayable can, but not
|
||||
vice-versa. An :func:`Image` is a kind of image manipulator, so an
|
||||
Image can be used whenever an image manipulator is required.
|
||||
|
||||
The use of image manipulators is
|
||||
historic. A number of image manipulators that had been documented in the
|
||||
past should no longer be used, as they suffer from inherent problems.
|
||||
In any case except for :func:`im.Data`, the :func:`Transform` displayable provides
|
||||
similar functionality in a more general manner, while fixing the problems.
|
||||
Their use is historic. A number of image manipulators that had been documented
|
||||
in a distant past should no longer be used, as they suffer from inherent
|
||||
problems, and in general (except for :func:`im.Data`), the :func:`Transform`
|
||||
displayable provides similar functionality while fixing the problems.
|
||||
|
||||
For the list of image manipulators, see the :doc:`image manipulator <im>`
|
||||
documentation.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
HTTPS/HTTP Fetch
|
||||
================
|
||||
|
||||
Ren'Py supports fetching information over HTTP and HTTPS using the
|
||||
renpy.fetch function. This function:
|
||||
|
||||
* Supports GET, POST, and PUT requests.
|
||||
* Support POSTing or PUTing data or json to the server.
|
||||
* Can return the result as bytes, a string, or a json object.
|
||||
|
||||
The fetch function is meant to be used in the main thread of Ren'Py, after
|
||||
the game has started. It can be used on desktop, mobile, or the web platform.
|
||||
When used on the web platform, when not fetching from the same server that
|
||||
originally served the game, the server must support `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_.
|
||||
|
||||
As a very simple example, this gets news from a server::
|
||||
|
||||
$ news = renpy.fetch("https://example.com/news.txt", result="text")
|
||||
"Here's the news: [news]"
|
||||
|
||||
And this posts JSON to a server, and gets JSON back::
|
||||
|
||||
$ result = renpy.fetch("https://example.com/api", json={"name": "Ren'Py"}, result="json")
|
||||
|
||||
As with any application that communicates over the network, security needs
|
||||
to be considered when using renpy.fetch, especially when displaying media
|
||||
returned this way. (Ren'Py is generally not hardened against malicious images,
|
||||
movies, and audio files.)
|
||||
|
||||
.. include:: inc/fetch
|
||||
|
||||
Requests
|
||||
--------
|
||||
|
||||
On desktop and mobile, Ren'Py includes the `Requests <https://requests.readthedocs.io/>`_
|
||||
library. This is more powerful, but doesn't integrate as well as renpy.fetch.
|
||||
(For example, Ren'Py will call :func:`renpy.pause` to wait for data, while
|
||||
requests will block, which could lead to problems like audio stalling.)
|
||||
+14
-12
@@ -1,7 +1,5 @@
|
||||
:orphan:
|
||||
|
||||
.. _image-manipulator:
|
||||
|
||||
Image Manipulators
|
||||
==================
|
||||
|
||||
@@ -14,17 +12,26 @@ An image manipulator can be used any place a displayable can, but not
|
||||
vice-versa. An :func:`Image` is a kind of image manipulator, so an
|
||||
Image can be used whenever an image manipulator is required.
|
||||
|
||||
With the few exceptions listed below, the use of image manipulators is
|
||||
historic. A number of image manipulators that had been documented in the
|
||||
past should no longer be used, as they suffer from inherent problems.
|
||||
In any case except for `im.Data`, the :func:`Transform` displayable provides
|
||||
similar functionality in a more general manner, while fixing the problems.
|
||||
.. warning::
|
||||
|
||||
The use of image manipulators is
|
||||
historic. A number of image manipulators that had been documented in the
|
||||
past should no longer be used, as they suffer from inherent problems,
|
||||
and in general (except for :func:`im.Data`), the :func:`Transform`
|
||||
displayable provides similar functionality while fixing the problems.
|
||||
|
||||
.. include:: inc/im_im
|
||||
|
||||
im.MatrixColor
|
||||
--------------
|
||||
|
||||
.. warning::
|
||||
|
||||
The im.MatrixColor image manipulator has been replaced by Transforms
|
||||
and ATL transforms that specify the matrixcolor property. Each `im.matrix`
|
||||
generator has been given a new `Matrix` equivalent, which can be found
|
||||
in the :doc:`matrixcolor documentation <matrixcolor>`.
|
||||
|
||||
The im.MatrixColor image manipulator is an image manipulator that uses
|
||||
a matrix to control how the colors of an image are transformed. The
|
||||
matrix used can be an im.matrix object, which encodes a 5x5 matrix in
|
||||
@@ -41,11 +48,6 @@ intermediate image is not needed, multiplying matrices is far
|
||||
more efficient, in both time and image cache space, than using
|
||||
two im.MatrixColors.
|
||||
|
||||
The im.MatrixColor image manipulator has been replaced by Transforms
|
||||
and ATL transforms that specify the matrixcolor property. Each `im.matrix`
|
||||
generator has been given a new `Matrix` equivalent, which can be found
|
||||
in the :doc:`matrixcolor documentation <matrixcolor>`.
|
||||
|
||||
.. warning::
|
||||
|
||||
The new Matrix objects multiply in the opposite order as the im.Matrixcolor
|
||||
|
||||
@@ -33,16 +33,15 @@ report any issues. When reporting issues, please determine the hardware
|
||||
8.2.0 / 7.7.0
|
||||
--------------
|
||||
|
||||
**Text Changes** The way Ren'Py handles text has changed in several ways. A big one is
|
||||
the switch to using harfbuzz for shaping (in Ren'Py 8), and bytecode
|
||||
hinting, which may change the letterforms. The positioning of vertical
|
||||
text has also been changed by harfbuzz rendering.
|
||||
**Text Changes** Ren'Py uses harfbuzz for shaping, which may produce
|
||||
different glyphs than would have been produced differently, and may change
|
||||
the spacing of text. The positioning of vertical text has also been
|
||||
changed by harfbuzz rendering.
|
||||
|
||||
To revert these changes, include in your game::
|
||||
To revert this changes, include in your game::
|
||||
|
||||
style default:
|
||||
shaper "freetype"
|
||||
hinting True
|
||||
|
||||
Ren'Py will automatically use an Emoji font when required. To disable this,
|
||||
add::
|
||||
@@ -135,7 +134,7 @@ this produced an error message like::
|
||||
|
||||
SHA1: ...
|
||||
|
||||
Whine this can be cause by other problems (like simply using entirely incorrect
|
||||
While this can be cause by other problems (like simply using entirely incorrect
|
||||
keys), one potential fix is:
|
||||
|
||||
1. In your project's base directory, rename ``bundle.keystore`` to ``bundle.keystore.bak``.
|
||||
|
||||
@@ -15,11 +15,11 @@ To find out more about Ren'Py, please visit the Ren'Py home page:
|
||||
|
||||
简体中文的文档请参考这里:
|
||||
|
||||
https://renpy.cn/doc/
|
||||
https://doc.renpy.cn/zh-CN/
|
||||
|
||||
繁體中文的文件請參考這裡:
|
||||
|
||||
https://renpy.cn/doc-tw/
|
||||
https://doc.renpy.cn/zh-TW/
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
@@ -126,7 +126,7 @@ To find out more about Ren'Py, please visit the Ren'Py home page:
|
||||
model
|
||||
other
|
||||
ren_py
|
||||
|
||||
fetch
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
.. _labels-control-flow:
|
||||
|
||||
Labels & Control Flow
|
||||
=====================
|
||||
|
||||
|
||||
+129
-126
@@ -3,81 +3,68 @@ Language Basics
|
||||
===============
|
||||
|
||||
Before we can describe the Ren'Py language, we must first describe the
|
||||
structure of a Ren'Py script. This includes how a files are broken into
|
||||
structure of a Ren'Py script. This includes how files are broken into
|
||||
blocks made up of lines, and how those lines are broken into the
|
||||
elements that make up statements.
|
||||
|
||||
Files
|
||||
=====
|
||||
|
||||
The script of a Ren'Py game is made up of all the files found under
|
||||
the game directory ending with the .rpy extension. Ren'Py will
|
||||
consider each of these files (in the Unicode order of their paths),
|
||||
and will use the contents of the files as the script.
|
||||
The script of a Ren'Py game is made up of all the files found under the
|
||||
:file:`game/` directory ending with the :file:`.rpy` extension. Ren'Py will
|
||||
consider each of these files (in the Unicode order of their paths), and will
|
||||
use the contents of the files as the script.
|
||||
|
||||
Generally, there's no difference between a script broken into multiple
|
||||
files, and a script that consists of one big file. Control can be
|
||||
transferred between files by jumping to or calling a label in another
|
||||
file. This makes the division of a script up into files a matter of
|
||||
personal style – some game-makers prefer to have small files (like one
|
||||
per event, or one per day), while others prefer to have one big
|
||||
script.
|
||||
Generally, there's no difference between a script written in one big file and a
|
||||
script broken into multiple files. Control can be transferred within the script
|
||||
(including between files) by jumping to or calling a :doc:`label <label>` in
|
||||
another file. This makes the division of a script up into files a matter of
|
||||
personal style : some game creators prefer to have small files (like one
|
||||
per event, or one per day), while others prefer to have one big script.
|
||||
|
||||
To speed up loading time, Ren'Py will compile the .rpy files into
|
||||
.rpyc files when it starts up. When a .rpy file is changed, the .rpyc
|
||||
file will be updated when Ren'Py starts up. However, if a .rpyc file
|
||||
exists without a corresponding .rpy file, the .rpyc file will be
|
||||
used. This can lead to problems if a .rpy file is deleted without
|
||||
deleting the .rpyc file.
|
||||
To speed up loading time, Ren'Py will compile the :file:`.rpy` files into
|
||||
:file:`.rpyc` files when it starts up. When a :file:`.rpy` file is changed, the
|
||||
:file:`.rpyc` file will be updated when Ren'Py starts up. However, if a
|
||||
:file:`.rpyc` file exists without a corresponding :file:`.rpy` file, the
|
||||
:file:`.rpyc` file will be used. This can lead to problems if a :file:`.rpy`
|
||||
file is deleted, or renamed, or moved, without deleting the :file:`.rpyc`
|
||||
file : the script it contains will still get executed.
|
||||
|
||||
Filenames must being with a letter or number, and may not begin with
|
||||
Filenames must begin with a letter or number, but may not begin with
|
||||
"00", as Ren'Py uses such files for its own purposes.
|
||||
|
||||
Base Directory
|
||||
--------------
|
||||
|
||||
The base directory is the directory that contains all files that are
|
||||
distributed with the game. (It may also contain some files that are not
|
||||
distributed with the game.) Things like README files should be placed in the
|
||||
base directory, from where they will be distributed.
|
||||
distributed with the game (even though not all the files in the base directory
|
||||
are usually distributed). See also : :doc:`build`. Things like README files
|
||||
should be placed in the base directory.
|
||||
|
||||
The base directory is created underneath the Ren'Py directory, and has the name
|
||||
of your game. For example, if your Ren'Py directory is named renpy-6.11.2, and
|
||||
your game is named "HelloWorld", your base directory will be
|
||||
renpy-6.11.2/HelloWorld.
|
||||
The base directory is created within the "Projects Directory", which can be set
|
||||
in the Launcher, when you create a new game. For example, if your Projects
|
||||
Directory is named :file:`renpygames`, and your game is named "HelloWorld", your
|
||||
base directory will be :file:`renpygames/HelloWorld`.
|
||||
|
||||
Game Directory
|
||||
--------------
|
||||
|
||||
The game directory is almost always a directory named "game" underneath the
|
||||
base directory. For example, if your base directory is renpy-6.11.2/HelloWorld,
|
||||
your game directory will be renpy-6.11.2/HelloWorld/game.
|
||||
|
||||
However, Ren'Py searches directories in the following order:
|
||||
|
||||
* The name of the executable, without the suffix. For example,
|
||||
if the executable is named moonlight.exe, it will look for
|
||||
a directory named moonlight under the base directory.
|
||||
* The name of the executable, without the suffix, and with
|
||||
a prefix ending with _ removed. For example, if the executable
|
||||
is moonlight_en.exe, Ren'Py will look for a directory named en.
|
||||
* The directories "game", "data", and "launcher", in that order.
|
||||
|
||||
The launcher will only properly recognize the "game" and "data" directories,
|
||||
however.
|
||||
The game directory is a directory named "game" inside the base directory. For
|
||||
example, if your base directory is :file:`renpygames/HelloWorld`, your game
|
||||
directory will be :file:`renpygames/HelloWorld/game`.
|
||||
|
||||
The game directory contains all the files used by the game. It, including all
|
||||
subdirectories, is scanned for .rpy and .rpyc files, and those are combined to
|
||||
form the game script. It is scanned for .rpa archive files, and those are
|
||||
automatically used by the game. Finally, when the game gives a path to a file
|
||||
to load, it is loaded relative to the game directory. (But note that
|
||||
config.searchpath can change this.)
|
||||
subdirectories, is scanned for :file:`.rpy` and :file:`.rpyc` files, and those
|
||||
are combined to form the game script. It is scanned for :file:`.rpa` archive
|
||||
files, and those are automatically used by the game. Finally, when Ren'Py takes
|
||||
or considers a path to a file, the path is (with very few exceptions) relative
|
||||
to the game directory (but note that :var:`config.searchpath` can change this).
|
||||
|
||||
Comments
|
||||
========
|
||||
|
||||
A Ren'Py script file may contain comments. A comment begins with a
|
||||
hash mark ('#'), and ends at the end of the line containing the
|
||||
hash mark (``#``), and ends at the end of the line containing the
|
||||
comment. As an exception, a comment may not be part of a string.
|
||||
|
||||
::
|
||||
@@ -98,19 +85,19 @@ A script file is broken up into :dfn:`logical lines`. A logical line
|
||||
always begins at the start of a line in the file. A logical line ends
|
||||
at the end of a line, unless:
|
||||
|
||||
* The last character on the line is a backslash ('\\').
|
||||
* The last character on the line is a backslash (``\``).
|
||||
|
||||
* The line contains an open parenthesis character ('(', '{', or '['),
|
||||
that hasn't been matched by the cooresponding close parenthesis
|
||||
character (')', '}', or ']', respectively).
|
||||
* The line contains an open parenthesis character (``(``, ``{``, or ``[``),
|
||||
that hasn't been matched by the cooresponding closing parenthesis
|
||||
character (\ ``)``, ``}``, or ``]``, respectively).
|
||||
|
||||
* The end of the line occurs during a string.
|
||||
* The end of the line occurs during a string - *any* string, even with single
|
||||
quotes, as opposed to Python rules.
|
||||
|
||||
Once a logical line ends, the next logical line begins at the start of
|
||||
the next line.
|
||||
|
||||
Most statements in the Ren'Py language consist of a single logical
|
||||
line, while some statements consist of multiple lines.
|
||||
Most statements in the Ren'Py language consist of a single logical line.
|
||||
|
||||
::
|
||||
|
||||
@@ -122,7 +109,7 @@ line, while some statements consist of multiple lines.
|
||||
$ a = [ "Because of parenthesis, this line also",
|
||||
"spans more than one line." ]
|
||||
|
||||
Empty logical lines are ignored.
|
||||
Empty lines are ignored and do not count as logical lines.
|
||||
|
||||
|
||||
Indentation and Blocks
|
||||
@@ -143,25 +130,25 @@ dividing a file into blocks are:
|
||||
|
||||
* All logical lines inside a block must have the same indentation.
|
||||
|
||||
* A block ends when a logical line is encountered with less
|
||||
* A block ends when a non-empty logical line is encountered with less
|
||||
indentation than the lines in the block.
|
||||
|
||||
Indentation is very important to Ren'Py, and cause syntax or logical
|
||||
errors when it's incorrect. At the same time, the use of indentation
|
||||
to convey block structure provides us a way of indicating that
|
||||
structure without overwhelming the script text.
|
||||
Indentation is very important in Ren'Py, as it is in Python, and it can cause
|
||||
syntax or logical errors when it's incorrect. At the same time, the use of
|
||||
indentation to express the block structure is far simpler than other languages
|
||||
using other delimiters.
|
||||
|
||||
::
|
||||
|
||||
"This statement, and the if statement that follows, is part of a block."
|
||||
"This statement, and the if statement that follows, are part of a block."
|
||||
|
||||
if True:
|
||||
if True:
|
||||
|
||||
"But this statement is part of a new block."
|
||||
"But this statement is part of a new block."
|
||||
|
||||
"This is also part of that new block."
|
||||
"This is also part of that new block."
|
||||
|
||||
"This is part of the first block, again."
|
||||
"This is part of the first block, again."
|
||||
|
||||
|
||||
.. _elements-of-statements:
|
||||
@@ -173,7 +160,7 @@ Ren'Py statements are made of a few basic parts.
|
||||
|
||||
:dfn:`Keyword`
|
||||
A keyword is a word that must literally appear in the script of the game.
|
||||
Keywords are used to introduce statements and properties.
|
||||
Keywords are typically used to introduce statements and properties.
|
||||
|
||||
:dfn:`Name`
|
||||
A name begins with a letter or underscore, which is followed by
|
||||
@@ -201,21 +188,40 @@ Ren'Py statements are made of a few basic parts.
|
||||
``beach``, ``night``, and ``happy``.
|
||||
|
||||
:dfn:`String`
|
||||
A string begins with a quote character (one of ", ', or \`),
|
||||
contains some sequence of characters, and ends with the same quote
|
||||
character.
|
||||
A string begins with a quote character (one of ", ', or \`), contains some
|
||||
sequence of characters, and ends with the same quote character.
|
||||
|
||||
The backslash character (\\) is used to escape quotes, special
|
||||
characters such as % (written as \\%), [ (written as \\[), and
|
||||
{ (written as \\{). It's also used to include newlines, using the \\n
|
||||
sequence.
|
||||
|
||||
Inside a Ren'Py string, consecutive whitespace is compressed into
|
||||
a single whitespace character, unless a space is preceded by a
|
||||
backslash. ::
|
||||
Inside a Ren'Py string, consecutive sequences of whitespace and line
|
||||
breaks are compressed into a single whitespace character, unless a space is
|
||||
preceded by a backslash. ::
|
||||
|
||||
'Strings can\'t contain their delimiter, unless you escape it.'
|
||||
|
||||
"There will be a space between the two following
|
||||
words."
|
||||
|
||||
"There will be a line break between\nthese."
|
||||
|
||||
"And there will be three spaces between\ \ \ these."
|
||||
|
||||
The ``r`` prefix is supported, and follow more or less the same rules as in
|
||||
Python. Other prefixes, like ``u``, ``b`` or ``f``, are not supported.
|
||||
Triple-quoted strings are generally not accepted in places where a normal
|
||||
string is expected, and when they are, they usually yield a different
|
||||
result - see :ref:`monologue-mode` for an example.
|
||||
|
||||
.. note::
|
||||
|
||||
This applies to strings found *directly* in Ren'Py script, such as in
|
||||
:ref:`say-statement` or :doc:`menus`. Strings found inside
|
||||
:ref:`python statements <python-statement>`, or in expressions (see
|
||||
below), follow ordinary Python rules.
|
||||
|
||||
:dfn:`Simple Expression`
|
||||
A simple expression is a Python expression, used to include Python
|
||||
in some parts of the Ren'Py script. A simple expression begins
|
||||
@@ -235,30 +241,26 @@ Ren'Py statements are made of a few basic parts.
|
||||
are all simple expressions. But ``3 + 4`` is not, as the
|
||||
expression ends at the end of a string.
|
||||
|
||||
:dfn:`At List`
|
||||
An at list is a list of simple expressions, separated by commas.
|
||||
|
||||
:dfn:`Python Expression`
|
||||
A Python expression is an arbitrary Python expression, that may
|
||||
not include a colon. These are used to express the conditions in
|
||||
the if and while statements.
|
||||
A Python expression is an arbitrary Python expression, that may not include
|
||||
a colon. These are used to express the conditions in the
|
||||
:ref:`if <if-statement>` and :ref:`while <while-statement>` statements.
|
||||
|
||||
|
||||
Common Statement Syntax
|
||||
=======================
|
||||
|
||||
Most Ren'Py statements share a common syntax. With the exception of
|
||||
the say statement, they begin with a keyword that introduces the
|
||||
statement. This keyword is followed by a parameter, if the statement
|
||||
takes one.
|
||||
Most Ren'Py statements share a common syntax. With the exception of the
|
||||
:ref:`say-statement`, they begin with a keyword that introduces the statement.
|
||||
This keyword is followed by a parameter, if the statement takes one.
|
||||
|
||||
The parameter is then followed by one or more properties. Properties
|
||||
may be supplied in any order, provided each property is only supplied
|
||||
once. A property starts off with a keyword. For most properties, the
|
||||
property name is followed by one of the syntax elements given above.
|
||||
|
||||
If the statement takes a block, the line ends with a colon
|
||||
(:). Otherwise, the line just ends.
|
||||
If the statement takes a block, the line ends with a colon (:). Otherwise, the
|
||||
line just ends.
|
||||
|
||||
|
||||
.. _python-basics:
|
||||
@@ -268,11 +270,10 @@ Python Expression Syntax
|
||||
|
||||
.. note::
|
||||
|
||||
It may not be necessary to read this section thoroughly right
|
||||
now. Instead, skip ahead, and if you find yourself unable to figure
|
||||
out an example, or want to figure out how things actually work, you
|
||||
can go back and review this.
|
||||
|
||||
It may not be necessary to read this section thoroughly right
|
||||
now. Instead, skip ahead, and if you find yourself unable to figure
|
||||
out an example, or want to figure out how things actually work, you
|
||||
can go back and review this.
|
||||
|
||||
Many portions of Ren'Py take Python expressions. For example, defining
|
||||
a new Character involves a call to the :func:`Character` function. While
|
||||
@@ -293,7 +294,7 @@ Here's a synopsis of Python expressions.
|
||||
Python strings begin with " or ', and end with the same
|
||||
character. \\ is used to escape the end character, and to
|
||||
introduce special characters like newlines (\\n). Unlike Ren'Py
|
||||
strings, Python strings can't span lines.
|
||||
strings, Python strings can't span several lines, or be delimited with \`.
|
||||
|
||||
:dfn:`True, False, None`
|
||||
There are three special values. ``True`` is a true value, ``False`` is
|
||||
@@ -320,32 +321,29 @@ Here's a synopsis of Python expressions.
|
||||
may vary. A list begins with a ``[``, contains a comma-separated
|
||||
list of expressions, and ends with ``]``. For example::
|
||||
|
||||
[ ]
|
||||
[ 1 ]
|
||||
[ 1, 2 ]
|
||||
[ 1, 2, 3 ]
|
||||
[]
|
||||
[1]
|
||||
[1, 2]
|
||||
[1, 2, 3]
|
||||
|
||||
:dfn:`Variable`
|
||||
Python expressions can use variables, that store values defined
|
||||
using the ``define`` statement or Python statements. A variable begins
|
||||
with a letter or underscore, and then has zero or more letters,
|
||||
numbers, or underscores. For example::
|
||||
Python expressions can use variables, that store values defined using the
|
||||
:ref:`define-statement` or the :ref:`default-statement`. A variable name
|
||||
follows the rules of a :dfn:`name` as explained in
|
||||
:ref:`elements-of-statements`. For example::
|
||||
|
||||
name
|
||||
love_love_points
|
||||
trebuchet2_range
|
||||
|
||||
Variables beginning with _ are reserved for Ren'Py's use, and
|
||||
shouldn't be used by creators.
|
||||
playername
|
||||
love_love_points
|
||||
trebuchet2_range
|
||||
|
||||
:dfn:`Field Access`
|
||||
Python modules and objects have fields, which can be accessed
|
||||
with by following an expression (usually a variable) with a
|
||||
dot and the field name. For example::
|
||||
Python modules and objects have fields, which can be accessed by following
|
||||
an expression (usually a variable) with a dot and the field name.
|
||||
For example::
|
||||
|
||||
config.screen_width
|
||||
|
||||
Consists of a variable (config) followed by a field access
|
||||
consists of a variable (config) followed by a field access
|
||||
(screen_width).
|
||||
|
||||
:dfn:`Call`
|
||||
@@ -354,18 +352,18 @@ Here's a synopsis of Python expressions.
|
||||
left-parenthesis, a comma-separated list of arguments, and a
|
||||
right-parenthesis. The argument list begins with the position
|
||||
arguments, which are Python expressions. These are followed by
|
||||
keyword arguments, which consist of the argument name, and equals
|
||||
sign, and an expression. In the example example::
|
||||
keyword arguments, which consist of the argument name, an equals
|
||||
sign, and an expression. In this example::
|
||||
|
||||
Character("Eileen", type=adv, color="#0f0")
|
||||
|
||||
we call the Character function. It's given one positional
|
||||
we call the :func:`Character` function. It's given one positional
|
||||
argument, the string "Eileen". It's given two keyword argument:
|
||||
``type`` with the value of the ``adv`` variable, and ``color``
|
||||
with a string value of "#0f0".
|
||||
with a string value of ``"#0f0"``.
|
||||
|
||||
Constructors are a type of function which returns a new object,
|
||||
and are called the same way.
|
||||
Other objects than functions can be called, and are widely known as
|
||||
:dfn:`callables`.
|
||||
|
||||
When reading this documentation, you might see a function signature
|
||||
like:
|
||||
@@ -384,16 +382,21 @@ This function:
|
||||
* Has one keyword argument, position, which has a default value
|
||||
of (0, 0).
|
||||
|
||||
Since the functions ends with \*\*properties, it means that it can
|
||||
Since the functions ends with ``**properties``, it means that it can
|
||||
take :doc:`style properties <style_properties>` as additional keyword
|
||||
arguments. Other special entries are \*args, which means that it takes
|
||||
an arbitrary number of positional parameters, and \*\*kwargs, which means
|
||||
that the keyword arguments are described in the documentation.
|
||||
arguments. Other special entries are ``*args``, which means that it takes
|
||||
an arbitrary number of positional parameters, and ``**kwargs``, which means
|
||||
that it takes a wide range of keyword parameters which are usually explained
|
||||
in the function's documentation.
|
||||
|
||||
Python is a lot more powerful than we have space for in this manual.
|
||||
To learn Python in more detail, we recommend starting with the Python
|
||||
tutorial, which is available from
|
||||
`python.org <http://docs.python.org/release/2.7/tutorial/index.html>`_.
|
||||
While we don't think a deep knowledge of Python is necessary to work
|
||||
with Ren'Py, the basics of Python statements and expressions is
|
||||
often helpful.
|
||||
When you see a ``/`` symbol on its own in a function signature, it means that
|
||||
the parameters before it are positional-only, and should not be passed by
|
||||
keyword. When you see a ``*`` symbol on its own, conversely, it means that the
|
||||
parameters *after* it are keyword-only, which means that they should only be
|
||||
passed using the ``name=value`` syntax.
|
||||
|
||||
Python is a lot more powerful than we have space for in this manual. To learn
|
||||
Python in more detail, we recommend starting with the Python tutorial, which is
|
||||
available from `python.org <https://docs.python.org/tutorial/index.html>`__.
|
||||
While a deep knowledge of Python is not necessary to work with Ren'Py, knowing
|
||||
the basics of Python statements and expressions is often helpful.
|
||||
|
||||
@@ -31,10 +31,72 @@ These are actions that manage screens, interaction results, and control flow.
|
||||
|
||||
.. include:: inc/control_action
|
||||
|
||||
.. _data-actions:
|
||||
|
||||
Data Actions
|
||||
------------
|
||||
|
||||
These set or toggle data.
|
||||
A number of these actions, encompassing the most usual cases, follow a simple
|
||||
pattern shown in the following table:
|
||||
|
||||
+----------------+---------------------------+---------------------------------+--------------------------------+------------------------+-----------------------+
|
||||
| Managers | Accessors |
|
||||
+ +---------------------------+---------------------------------+--------------------------------+------------------------+-----------------------+
|
||||
| | Variable | ScreenVariable | LocalVariable | Field | Dict |
|
||||
+================+===========================+=================================+================================+========================+=======================+
|
||||
| Set | :func:`SetVariable` | :func:`SetScreenVariable` | :func:`SetLocalVariable` | :func:`SetField` | :func:`SetDict` |
|
||||
+----------------+---------------------------+---------------------------------+--------------------------------+------------------------+-----------------------+
|
||||
| Toggle | :func:`ToggleVariable` | :func:`ToggleScreenVariable` | :func:`ToggleLocalVariable` | :func:`ToggleField` | :func:`ToggleDict` |
|
||||
+----------------+---------------------------+---------------------------------+--------------------------------+------------------------+-----------------------+
|
||||
| Cycle | :func:`CycleVariable` | :func:`CycleScreenVariable` | :func:`CycleLocalVariable` | :func:`CycleField` | :func:`CycleDict` |
|
||||
+----------------+---------------------------+---------------------------------+--------------------------------+------------------------+-----------------------+
|
||||
| Increment | :func:`IncrementVariable` | :func:`IncrementScreenVariable` | :func:`IncrementLocalVariable` | :func:`IncrementField` | :func:`IncrementDict` |
|
||||
+----------------+---------------------------+---------------------------------+--------------------------------+------------------------+-----------------------+
|
||||
|
||||
The accessors determine the target whose value will change, and the manager determines what the new value
|
||||
will be. Their behavior is relatively simple to grasp:
|
||||
|
||||
- The :abbr:`-Variable (SetVariable, ToggleVariable, CycleVariable, IncrementVariable)`
|
||||
actions change the value of the global variable called `name`, found in the general
|
||||
store. The `name` argument must be a string, and can be a simple name like "strength", or one with dots
|
||||
separating the variable from fields, like "hero.strength" or "persistent.show_cutscenes".
|
||||
- The :abbr:`-ScreenVariable (SetScreenVariable, ToggleScreenVariable, CycleScreenVariable, IncrementScreenVariable)`
|
||||
actions change the value of the variable called `name`, associated with the
|
||||
current top-level screen. In a `use`\ d screen, this action sets the variable in the context of the
|
||||
screen containing all the `use`\ d one(s).
|
||||
- The :abbr:`-LocalVariable (SetLocalVariable, ToggleLocalVariable, CycleLocalVariable, IncrementLocalVariable)`
|
||||
actions change the value of the variable called `name`, taken locally to the
|
||||
screen it's in. This action is only useful in a screen that has been `use`\ d by another screen (for more
|
||||
information, see :ref:`sl-use`). In all other cases, the -ScreenVariable actions should be preferred,
|
||||
as yielding better performance and allowing more of the screen to be cached. The -LocalVariable
|
||||
actions must be created in the context that the variable is set in - it can't be passed in from somewhere
|
||||
else.
|
||||
- The :abbr:`-Field (SetField, ToggleField, CycleField, IncrementField)`
|
||||
actions change the value of the field called `field` of the object `object`.
|
||||
- The :abbr:`-Dict (SetDict, ToggleDict, CycleDict, IncrementDict)`
|
||||
actions change the value of the key `key` in the dictionary `dict` : they change
|
||||
``dict[key]``. This also works with lists.
|
||||
|
||||
* The :abbr:`Set- (SetVariable, SetScreenVariable, SetLocalVariable, SetField, SetDict)`
|
||||
actions simply set the value of the target to the passed `value`. Note that this has nothing
|
||||
to do with ``set``, which is a builtin type in Python. ``target = value``
|
||||
* The :abbr:`Toggle- (ToggleVariable, ToggleScreenVariable, ToggleLocalVariable, ToggleField, ToggleDict)`
|
||||
actions invert the boolean value of their target, between `true_value` (if given and not
|
||||
None) and `false_value` (same). When `true_value` and `false_value` are both None, ``target = not target``
|
||||
* The :abbr:`Cycle- (CycleVariable, CycleScreenVariable, CycleLocalVariable, CycleField, CycleDict)`
|
||||
actions cycle through the provided `values`, which must be a non-empty sequence (a list,
|
||||
tuple or range). If the target's value is not in the sequence at the time the action runs, it is set to
|
||||
the first value in the sequence. The `loop` parameter (defaulting to True) determines what happens when
|
||||
the `values` run out : if True it's started from the beginning, if False it raises an exception. The
|
||||
`reverse` parameter (defaulting to False) reverses the passed `values` sequence.
|
||||
* The :abbr:`Increment- (IncrementVariable, IncrementScreenVariable, IncrementLocalVariable, IncrementField, IncrementDict)`
|
||||
actions add `amount` to their target, which defaults to 1 but may be of any type
|
||||
compatible with the target. ``target = target + amount``
|
||||
|
||||
.. include:: inc/generated_data_action
|
||||
|
||||
The following data actions do not follow the pattern above. Some of them are related to Python's ``set`` type,
|
||||
not to be confused with the Set- actions above.
|
||||
|
||||
.. include:: inc/data_action
|
||||
|
||||
|
||||
@@ -1128,7 +1128,7 @@ One use of nearrect is for dropdown menus::
|
||||
|
||||
has vbox
|
||||
|
||||
# This is the button that is clicked to enable the dropdown,
|
||||
# This is the button that is clicked to enable the dropdown
|
||||
textbutton "Difficulty: [difficulty]":
|
||||
|
||||
# This action captures the focus rectangle, and in doing so,
|
||||
@@ -1139,7 +1139,7 @@ One use of nearrect is for dropdown menus::
|
||||
action Return()
|
||||
|
||||
# All sorts of other screen elements could be here, but the nearrect needs
|
||||
# be at the top level, and the last thing show, apart from its child.
|
||||
# to be at the top level, and the last thing shown, apart from its child.
|
||||
|
||||
# Only if the focus has been captured, display the dropdown.
|
||||
# You could also use showif instead of basic if
|
||||
|
||||
+484
-479
File diff suppressed because it is too large
Load Diff
@@ -445,6 +445,16 @@ Text Style Properties
|
||||
If not None, this should be a style object. The style that's used for
|
||||
alternate ruby text.
|
||||
|
||||
.. style-property:: axis dict or None
|
||||
|
||||
This allows the axes of a :ref:`variable font <variable-fonts>` to be
|
||||
set. If not None, this should be a dictionary mapping axis names to
|
||||
values. For example::
|
||||
|
||||
style default:
|
||||
font "VariableFont.ttf"
|
||||
axis { "weight" : 500, "width" : 95 }
|
||||
|
||||
.. style-property:: black_color color
|
||||
|
||||
When rendering an image-based font, black will be mapped to this
|
||||
@@ -491,11 +501,48 @@ Text Style Properties
|
||||
image-based font, this should be the name used to register the
|
||||
font.
|
||||
|
||||
.. style-property:: size int
|
||||
.. style-property:: hinting str
|
||||
|
||||
The size of the font on the screen. While this is nominally in
|
||||
pixels, font files may have creative interpretations of this
|
||||
value.
|
||||
Controls how the font will be hinted. This should be one of the following
|
||||
strings:
|
||||
|
||||
"auto"
|
||||
The default, forces use of the Freetype auto hinter.
|
||||
"auto-light"
|
||||
Forces the use of the freetype auto hinter in light mode, which only
|
||||
hints vertically.
|
||||
"bytecode"
|
||||
Uses bytecode hinting information found in the font.
|
||||
"none"
|
||||
Does not hint the font.
|
||||
|
||||
This may also be True, in which case the value is looked up in
|
||||
:var:`config.font_hinting`.
|
||||
|
||||
.. style-property:: hyperlink_functions tuple of (function, function, function)
|
||||
|
||||
This is a tuple of three functions relating to hyperlinks in text.
|
||||
|
||||
The first item is the hyperlink style function. When called with a single
|
||||
argument, the argument of the hyperlink, it must return a style object to
|
||||
use for the hyperlink, such as ``style.hyperlink_text``. Note that a
|
||||
style object is not a string.
|
||||
|
||||
The second item is the hyperlink clicked function. This function is called
|
||||
when a hyperlink is chosen by the user. If it returns a value other than
|
||||
None, the interaction returns that value.
|
||||
|
||||
The third item is the hyperlink focus function. This function is called
|
||||
with the argument of the hyperlink when the hyperlink gains focus, and
|
||||
with None when it loses focus. If it returns a value other than None,
|
||||
the interaction returns that value.
|
||||
|
||||
.. style-property:: instance string or None
|
||||
|
||||
When using a :ref:`variable font <variable-fonts>`, this can be
|
||||
a string giving a named instance of the font to use. For example,
|
||||
if the font has a "Bold" instance, this can be set to "Bold" to
|
||||
use that instance.
|
||||
|
||||
.. style-property:: italic boolean
|
||||
|
||||
@@ -671,8 +718,18 @@ Text Style Properties
|
||||
.. style-property:: shaper "harfbuzz" or "freetype".
|
||||
|
||||
The shaper used on text. This should be one of "harfbuzz" or "freetype".
|
||||
The harfbuzz shape is more capable but only works on Ren'Py 8, while
|
||||
"freetype"
|
||||
The harfbuzz shaper is more capable but only works on Ren'Py 8, while
|
||||
"freetype" works in Ren'Py 7 as well.
|
||||
|
||||
The shaper takes a series of characters and turns it into a series of
|
||||
positioned glyphs. This is used for things like ligatures, Indic/Bhramic
|
||||
languages, and combining emjoi.
|
||||
|
||||
.. style-property:: size int
|
||||
|
||||
The size of the font on the screen. While this is nominally in
|
||||
pixels, font files may have creative interpretations of this
|
||||
value.
|
||||
|
||||
.. style-property:: slow_abortable boolean
|
||||
|
||||
@@ -709,41 +766,10 @@ Text Style Properties
|
||||
|
||||
If True, an underline will be added to the text.
|
||||
|
||||
.. style-property:: hyperlink_functions tuple of (function, function, function)
|
||||
|
||||
This is a tuple of three functions relating to hyperlinks in text.
|
||||
|
||||
The first item is the hyperlink style function. When called with a single
|
||||
argument, the argument of the hyperlink, it must return a style object to
|
||||
use for the hyperlink, such as ``style.hyperlink_text``. Note that a
|
||||
style object is not a string.
|
||||
|
||||
The second item is the hyperlink clicked function. This function is called
|
||||
when a hyperlink is chosen by the user. If it returns a value other than
|
||||
None, the interaction returns that value.
|
||||
|
||||
The third item is the hyperlink focus function. This function is called
|
||||
with the argument of the hyperlink when the hyperlink gains focus, and
|
||||
with None when it loses focus. If it returns a value other than None,
|
||||
the interaction returns that value.
|
||||
|
||||
.. style-property:: vertical boolean
|
||||
|
||||
If True, the text will be rendered vertically.
|
||||
|
||||
.. style-property:: hinting str
|
||||
|
||||
Controls how the font will be hinted. This should be one of the following
|
||||
strings:
|
||||
|
||||
"auto"
|
||||
The default, forces use of the Freetype auto hinter.
|
||||
"bytecode"
|
||||
Uses bytecode hinting information found in the font.
|
||||
"none"
|
||||
Does not hint the font.
|
||||
|
||||
|
||||
.. _window-style-properties:
|
||||
|
||||
Window Style Properties
|
||||
|
||||
+66
-4
@@ -84,10 +84,11 @@ displayed. For example, if the player's name is stored in the
|
||||
|
||||
g "Welcome to the Nekomimi Institute, [playername]!"
|
||||
|
||||
Ren'Py will interpolate variables found in the global store. When
|
||||
using a text widget in a screen, Ren'Py will also interpolate screen
|
||||
local variables. (This can be overridden by supplying an explicit
|
||||
scope argument to the Text displayable.)
|
||||
Ren'Py will search for variables in the following order:
|
||||
|
||||
* When used in a screen, screen-local variables.
|
||||
* Variables found in the ``interpolate`` namespace.
|
||||
* Variables found in the global namespace.
|
||||
|
||||
Ren'Py isn't limited to interpolating simple variables. It can also
|
||||
interpolate fields and components of tuples. So it's possible to have::
|
||||
@@ -822,3 +823,64 @@ necessary.
|
||||
|
||||
Whenever text is displayed that overflows the available area, Ren'Py
|
||||
will log an error to the text_overflow.txt file.
|
||||
|
||||
.. _variable-fonts:
|
||||
|
||||
Variable Fonts
|
||||
==============
|
||||
|
||||
Ren'Py has support for OpenType variable fonts. These fonts may support
|
||||
multiple axes, such as weight and width, and based on the value of these
|
||||
axes, the display of the font can change. Variable fonts may also have
|
||||
named instances, which provide different values for the axes. For example,
|
||||
the "bold" named instance will likely provide the bold version of a font,
|
||||
while "regular" will be a more normal version.
|
||||
|
||||
Variable fonts require the use of the harfbuzz text shaper, controlled by
|
||||
the :propref:`shaper` style property. Once this is set, the :propref:`instance`
|
||||
property selects a named instance, while the :propref:`axis` property sets
|
||||
the value of one or more axes.
|
||||
|
||||
These can also be set in the gui system. For example one can use::
|
||||
|
||||
define gui.text_font = "nunito.ttf"
|
||||
define gui.text_instance = "light"
|
||||
define gui.text_axis = {"width" : 125}
|
||||
|
||||
Will start with the light instance, and make it wide.
|
||||
|
||||
If no instance is given, Ren'Py will default to "regular" for non-bold text,
|
||||
and "bold" for bold text.
|
||||
|
||||
There are two text tags that support the use of variable fonts.
|
||||
|
||||
.. text-tag:: instance
|
||||
|
||||
The instance tag changes the instance that is being used. For example::
|
||||
|
||||
"This is {instance=heavy}heavy{/instance} text."
|
||||
|
||||
When the instance tag is used, the axis properties are overriden.
|
||||
|
||||
.. text-tag:: axis
|
||||
|
||||
The axis tag changes the value of one or more axes. For example::
|
||||
|
||||
"This is {axis:width=125}wide{/axis} text."
|
||||
|
||||
The axis tag can be used multiple times, and the values will be combined.
|
||||
|
||||
"This is {axis:width=125}{axis:weight=200}wide and bold{/axis}{/axis} text."
|
||||
|
||||
The value of on the right side of the equals is expected to be a floating
|
||||
point number.
|
||||
|
||||
To get the available instances and axes of a font, use the :func:`renpy.variable_font_info`
|
||||
function. This function is intended to be called from the console. To access it, hit
|
||||
shift+O to open the console, and then type::
|
||||
|
||||
renpy.variable_font_info("nunito.ttf")
|
||||
|
||||
This will display information for the nunito.ttf font.
|
||||
|
||||
.. include:: inc/variable_fonts
|
||||
|
||||
@@ -112,6 +112,14 @@ The Web platform natively uses a default presplash image. To override it, you ca
|
||||
an image named `web-presplash`, `.jpg`, `.png` or `.webp`, and it will replace
|
||||
the default.
|
||||
|
||||
Icon
|
||||
---------
|
||||
|
||||
The Web page icon can be customized by putting an image file with the name `web-icon.png`
|
||||
in the base directory of your project. This image must have a minimum resolution of
|
||||
512x512 and its width and height must be equal.
|
||||
If no custom image is given, the default Ren'Py icon is used.
|
||||
|
||||
Progressive Downloading
|
||||
-----------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user