Compare commits

..

222 Commits

Author SHA1 Message Date
Asriel Senna 7d8644f2f8 Changelog and rework the section 2023-10-30 23:59:30 +01:00
Asriel Senna c7a284c1e4 Reimplement disable_on_enter based on new inputvalue implem 2023-10-30 23:55:21 +01:00
Tom Rothamel 8b79a3663c doc: Changelog the last few merges. 2023-10-30 17:56:30 -04:00
Elckarow fdf38adc83 sl2: copy_properties method for custom statements (#4839)
This takes the properties from another screen language statement.
2023-10-30 17:51:42 -04:00
Denys 921feb4ada Update Ukrainian translation. (#4996) 2023-10-30 17:50:14 -04:00
Gouvernathor f367e996aa Rework Inputvalues (#5080)
Add LocalVariableValue, factorize a bunch of code, use the same doc-generating model as with bar values.
2023-10-30 17:48:53 -04:00
Tom Rothamel c0c182d200 Merge pull request #5081 from renpy/language-basics
Rework the language basics page
2023-10-30 17:47:50 -04:00
Tom Rothamel a5fec5a7a5 rpy: Add a path if range queries are failing entirely. 2023-10-30 02:26:35 -04:00
Gouvernathor 0f668f2810 More tweaks 2023-10-30 00:57:00 +01:00
Asriel Senna 5cba186326 Chitespace, docstrings and unused ref anchors 2023-10-30 00:44:57 +01:00
Asriel Senna 74631fe701 Rework the language basics page 2023-10-30 00:44:26 +01:00
Tom Rothamel 13eeb9d0c0 update: Move prompt confirm to later in the update process.
Specifically, until after the update method has been selected, and
for rpu, after the files have been scanned and the size of the
download is known.

This will (shortly) let us give the user information about
download and disk size.
2023-10-29 14:55:39 -04:00
Tom Rothamel 1066157585 update: Call the old update format "zsync". 2023-10-29 12:25:22 -04:00
Tom Rothamel 7742627373 update: Add support for updating an empty directory.
That is, taking an empty alternate base directory and turning it
into a working game.
2023-10-28 22:34:27 -04:00
Tom Rothamel 95102a6b71 rpu: Add support for determining the download and disk size.
And pausing to prompt the user once that's known.
2023-10-28 22:30:14 -04:00
Tom Rothamel acac446c8b py2: Fix. 2023-10-28 13:35:41 -04:00
Tom Rothamel 9fc8e43f17 Merge branch 'fix' 2023-10-28 01:10:03 -04:00
Tom Rothamel 6d6003f4e0 sl2: Fix issue with transcludes and custom statements.
Fixes #5074.
2023-10-28 00:53:25 -04:00
Tom Rothamel 7ff1b3b5c5 Merge pull request #5057 from renpy/lint-toggles
Add lint toggles
2023-10-27 00:50:42 -04:00
Tom Rothamel 4fd318880e doc: LocalVariableValue 2023-10-27 00:48:56 -04:00
Gouvernathor 6cd57b192a Factorize and improve some Barvalue subclasses (#5064)
* factorize the most common BarValue subclasses, using the same recipe (but in a much simpler way) as in Action mixins #4473, which considerably shortens the code and avoids code duplication
* fix the kind parameter of FieldValue and VariableValue - it was saved but never passed to _get_field and _set_field
* fix some subclasses doc entries that weren't appropriately worded, some which set variables mentioned "fields"
* implement LocalVariableValue, supersede and fix add the LocalVariableValue action #5055, and allow it to have adequately-worded exceptions


Co-authored-by: Mal Graty <mal.graty@googlemail.com>
2023-10-27 00:47:21 -04:00
Tom Rothamel 8c6aab0bc6 Revert "add the LocalVariableValue action (#5055)"
This reverts commit 2451b3f0da.
2023-10-27 00:46:14 -04:00
Elckarow 2451b3f0da add the LocalVariableValue action (#5055) 2023-10-27 00:42:27 -04:00
Gouvernathor daea1375c3 Various tweaks and updates (#5069)
* Simplify currying in exports

* Unused import

* Docstrings, whitespace and exceptions

The docstrings were obsolete (not only buttons and hotspots take actions now, and they are now the core of screen language so nobody ignores what they are)
and the information that they can be subclassed is much more important in my view
Also these exceptions are better

* utflf
2023-10-27 00:41:32 -04:00
Tom Rothamel ada2b860f6 Merge pull request #5070 from qTich/fix/web-input-prompt
fix: translate `prompt` when used `web_input`
2023-10-27 00:41:09 -04:00
Tom Rothamel 6758447cad Merge pull request #5073 from renpy/pass-layeredimage
Re-add the pass keyword to the layeredimage language
2023-10-27 00:40:53 -04:00
Tom Rothamel 035d6e7b30 doc: Fetch. 2023-10-27 00:06:32 -04:00
Tom Rothamel 0675f6ce44 fetch: Fix fetching on the web platform. 2023-10-26 23:25:56 -04:00
Gouvernathor c7317c076b Re-add the pass keyword to the layeredimage language 2023-10-27 03:50:17 +02:00
Tom Rothamel 6bd420b421 fetch: Add the renpy.fetch function.
This is a single function that can fetch data from a web server,
optionally taking binary data or json as input, and returning its
result as bytes, text, or json-decoded data.
2023-10-26 00:59:19 -04:00
Tichq c1f2db72b5 fix: translate prompt when used web_input 2023-10-26 03:14:12 +03:00
Tom Rothamel f87917e399 Changes to android APK loading.
* Remove support for Expansion APKs. These have been obsolete for
  some time.

* Only use the APK for game files if an alternate base directory is
  not in use.
2023-10-25 19:52:33 -04:00
Gouvernathor 6bc50d6454 Make the doc generation of the mixined actions conditional 2023-10-25 19:44:55 +02:00
Tom Rothamel a2bfa02de1 doc: Fix caps on one line. 2023-10-25 09:38:10 -04:00
Gouvernathor a3ad875263 doc: fax the fox 2023-10-25 14:01:20 +02:00
Tom Rothamel 80d3075bf0 id: Force the director into lines mode after a reload.
Fixes #5028.
2023-10-25 01:08:44 -04:00
Tom Rothamel 0b213cc7db doc: Edits to the data accessor changelog. 2023-10-24 23:34:39 -04:00
Tom Rothamel 23fc2f66c9 Merge pull request #5067 from renpy/changelog-mixins
Changelog mixins
2023-10-24 20:40:20 -04:00
Ren'Py Bot 19498d5bce Merge branch 'fix' 2023-10-24 04:30:07 -04:00
Tom Rothamel 7f37382012 Avoid distributing web-icon.png in other formats. 2023-10-23 22:58:51 -04:00
Kassy 7fcad404b6 Web icon fix (#5018)
* Fix web icon name

The code looks for "web-icon.png" but the comment and function names
referred to "pwa_icon.png".

* Document the web icon customization
2023-10-23 22:58:20 -04:00
Tom Rothamel d8ad7d1672 Create checkpoints when hard skipping.
Fixes #5025.
2023-10-23 22:04:38 -04:00
Tom Rothamel e4b550b8c3 Create checkpoints when hard skipping.
Fixes #5025.
2023-10-23 22:04:01 -04:00
Gouvernathor 67bdb9ab03 Changelog mixins 2023-10-23 10:44:58 +02:00
Gouvernathor 6f44533bbe Mixin-Based Data Actions
This change replaces the implementation of the data actions with mixins, one for accessing the data and one for changing it. Doing so simplifies and regularizes the implementation, and makes it possible to add new types of data actions, like the Cycle and Increment actions added here.
2023-10-22 22:47:11 -04:00
Tom Rothamel d084b624b9 Merge branch 'fix' 2023-10-22 22:34:16 -04:00
Tom Rothamel 06d3a90bd7 Include checkpointing_suspended in the rollback objects.
It's possible (and had been for some time) to rollback into a spot
where checkpointing was suspended - but checkpointing would be re-enabled,
meaning that checkpoint_suspended would be forgotten.
2023-10-22 22:27:18 -04:00
Tom Rothamel 5e64403699 Fix issues with greedy rollback by default.
Fixes #5040, which was caused by the interaction of greedy rollbacl
and suspended rollback.

Fixes #5035, which was just never actually looking at the _greedy_rollback
variable.
2023-10-22 22:12:55 -04:00
Tom Rothamel d8954aa732 doc: Change links to Chinese documentation. 2023-10-21 01:52:15 -04:00
Tom Rothamel 669b07afef Add support for an alternate base directory.
This is a directory that is checked for update/version.json. If that
file exists and has versions that match the current engine, it is
used as the base directory instead of what the engine provides.

The main reason for this is to support Android games larger than
2GB, by letting those games download content.
2023-10-21 01:45:26 -04:00
Tom Rothamel e2bc3fb55c rpu: Write a version.json file into the update directory.
This file only exists if the update has finished successfully,
and contains the version information from updates.json.

It's intended to be used ot ensure that an alternate base
directory works with Ren'Py.
2023-10-20 23:24:07 -04:00
Tom Rothamel 088b1572ef rpy: Search for Ren'Py's update.pem in the base directory. 2023-10-20 00:31:30 -04:00
Gouvernathor a30f562d38 Resolve type issues 2023-10-19 21:23:42 +02:00
Ren'Py Bot 4768a012db Merge branch 'fix' 2023-10-19 04:30:07 -04:00
Tom Rothamel c7d8bd1dc9 rpu: Allow an update to be signed by multiple keys.
This provides a way to migrate old versions to a new key.
2023-10-19 01:27:07 -04:00
Tom Rothamel be226363e8 rpu: Include update information in all mutable formats. 2023-10-19 01:11:07 -04:00
Tom Rothamel 267a807146 rpu: Verify ECDSA signatures. 2023-10-19 00:53:17 -04:00
Tom Rothamel 34468cf57d Ignore update.pem. 2023-10-19 00:13:25 -04:00
Tom Rothamel 172f9f926e rpu: Create the updates.ecdsa file. 2023-10-19 00:06:14 -04:00
Tom Rothamel f6afced012 rpu: Include a verifying key with the built project. 2023-10-18 23:33:04 -04:00
Tom Rothamel 00cd7e4ada rpu: Generate update.pem when updates are enabled. 2023-10-18 23:13:52 -04:00
Tom Rothamel 9f4ab7d761 Always oversample SVGs.
Work to support placeholders lead to problems with SVG scaling
when the window scaled. This is resolved by scaling up the
placeholder, if required.

Fixes #5063.
2023-10-18 21:49:46 -04:00
Tom Rothamel 9ac420c259 id: Add lines explaining how to customize the director.
Fixes #5029.
2023-10-17 22:16:38 -04:00
Tom Rothamel 6c548d30ad Tweak the wording of the namespace error. 2023-10-17 00:02:26 -04:00
Tom Rothamel 615c6a728d Merge pull request #5059 from renpy/store.store
Prevent creation of a "store" substore
2023-10-17 00:01:12 -04:00
Gouvernathor bb8c508837 Prevent creation of a "store" substore 2023-10-16 18:45:43 +02:00
Tom Rothamel 30833f1f5f Allow renpy.utter_restart(True) to work on mobile. 2023-10-16 00:48:16 -04:00
Tom Rothamel 57bf18c705 rpu: Support for adding DLC. 2023-10-16 00:48:01 -04:00
Tom Rothamel e30855cb93 rpu: In the launcher, allow the website to pick rpu/zsync.
This makes it possible to deploy rpu, but have a fallback if
problems are reported.
2023-10-15 21:01:36 -04:00
Tom Rothamel ed0f536853 Merge branch 'fix' 2023-10-15 20:54:08 -04:00
Tom Rothamel e4f601310b Fix an issue with projects.txt.
Specifically, /path/to/mygame/ would produce a game with an
empty name, rather than mygame.
2023-10-15 20:54:00 -04:00
Tom Rothamel b92107dd02 rpu: Fix multi-part downloading. 2023-10-15 20:38:02 -04:00
Tom Rothamel 19c5e806b1 rpu: Add progress and logging to the updater. 2023-10-15 19:39:33 -04:00
Tom Rothamel f6d9762c5c rpu: Integrate rpu with the existing updater. 2023-10-15 15:58:01 -04:00
Ren'Py Bot 8b679cca71 Merge branch 'fix' 2023-10-15 04:30:07 -04:00
Tom Rothamel 34fe132e55 rpu: Use an environment variable to enable RPU. 2023-10-14 23:40:12 -04:00
Gouvernathor 0036bc3fe9 Optional plural 2023-10-15 03:52:22 +02:00
Gouvernathor d321d1ff51 Implement character char and word counts
fixes #5001
2023-10-15 03:46:44 +02:00
Gouvernathor a98d15eed4 Toggle the builtins-in-parameters check
disabled by default
2023-10-15 03:22:40 +02:00
Gouvernathor 57dba01448 Merge branch 'lint-parameter-names' into lint-toggles 2023-10-15 03:19:14 +02:00
Gouvernathor 3532b589d5 Implement lint options and make orphan tl check optional 2023-10-15 03:15:31 +02:00
Gouvernathor 51cc42a901 Base GUI entries for lint toggles 2023-10-15 02:33:51 +02:00
Gouvernathor e09b87dffe typos 2023-10-15 02:27:13 +02:00
Gouvernathor c5752398b9 doc: extend new im wording to the displayables page 2023-10-15 02:23:44 +02:00
Ren'Py Bot d5fe9c9a11 Merge branch 'fix' 2023-10-14 04:30:07 -04:00
Tom Rothamel b8ca91a62a bubble: Make available outside the overlay.
Fixes #5052.
2023-10-13 23:47:56 -04:00
Tom Rothamel 5aec41f9e1 bubble: Make available outside the overlay.
Fixes #5052.
2023-10-13 23:47:03 -04:00
Tom Rothamel b1d1db1075 doc: Finish documenting shaper. 2023-10-13 08:35:35 -04:00
Tom Rothamel 2708eda3d3 py2: Make generating rpus work with py2. 2023-10-13 08:35:35 -04:00
Ren'Py Bot ad83e0354e Merge branch 'fix' 2023-10-12 04:30:07 -04:00
Tom Rothamel 1c823a4e7c rpu: Support compressed segments. 2023-10-12 00:23:52 -04:00
Tom Rothamel 11186c86ae rpu: Allow rpus to be created in parallel. 2023-10-11 22:44:08 -04:00
Tom Rothamel 6014bfa007 Use the correct variable to configure the package formats. 2023-10-11 22:44:08 -04:00
Gouvernathor ef0d2db11d Fix translation 2023-10-11 18:23:50 +02:00
Ren'Py Bot 7dd1db50d2 Merge branch 'fix' 2023-10-11 04:30:07 -04:00
Tom Rothamel c8413912d2 Add a "Skip splashscreen" preference to the launcher.
This sets RENPY_SKIP_SPLASHSCREEN, which causes Ren'Py to
skip the splashscreen label.
2023-10-10 22:34:30 -04:00
Tom Rothamel 969b2ccecf SyntaxError: Invalid syntax. 2023-10-10 21:22:01 -04:00
Tom Rothamel 228c72f6c1 rpu: Create FileLists from the current.json file. 2023-10-10 20:40:44 -04:00
Tom Rothamel ad9af51510 rpu: Enable building rpus for the SDK. 2023-10-10 20:40:44 -04:00
Tom Rothamel 45faa71da8 Merge pull request #5045 from mal/layer-clip-docs
Clarify clipping behaviour as it relates to layers and Layer
2023-10-10 20:07:45 -04:00
Tom Rothamel e530c78cc0 Merge pull request #5050 from renpy/im-directives
Add warnings and deprecated sections for image manipulators
2023-10-10 20:07:27 -04:00
Gouvernathor c3a26654f7 Update the transform property 2023-10-11 00:52:20 +02:00
Gouvernathor 14710606ef doc: heavy-handed use of the deprecated directive 2023-10-11 00:41:13 +02:00
Gouvernathor 72cb9e3929 doc: Add warnings to the im page 2023-10-11 00:38:55 +02:00
Ren'Py Bot 9c3b082260 Merge branch 'fix' 2023-10-10 04:30:07 -04:00
Mal Graty 46cfe60086 doc: Signature for Layer displayable 2023-10-09 14:06:35 +01:00
Mal Graty 588d1e1137 doc: Warn of layer_clipping precedence in Layer 2023-10-09 14:04:14 +01:00
Mal Graty 15a12efd61 doc: Clarify behaviour there's no layer_clipping 2023-10-09 14:04:14 +01:00
Gouvernathor b0434ea1d9 doc: Update a link that was a teensy bit outdated 2023-10-09 14:58:47 +02:00
Tom Rothamel 96b44cdcb2 Add Show Filename and Line
A new option in the developer menu that shows the filename and line,
and lets you click to edit.
2023-10-08 21:58:44 -04:00
Tom Rothamel 1fde5f985a Add the EditFile action.
This opens the filename and line in a text editor,
if possible.
2023-10-08 21:54:43 -04:00
Tom Rothamel 34dcdc7f0e rpu: Include rpu update information in update.json. 2023-10-07 15:47:49 -04:00
Tom Rothamel 2238b460a7 rpu: Provide progress when making the update.
This was required because previously, only writing the files
made progress.
2023-10-07 10:37:47 -04:00
Tom Rothamel ff8ba2d10c rpu: Start integration with distribute.
This can now create the rpu files for a game.
2023-10-06 23:58:30 -04:00
Tom Rothamel 5e3495c333 rpu: Soft limit to 50MB.
While a big file will let the .rpy get bigger, this makes sure that
two files won't be downloaded when one will do.
2023-10-06 23:23:02 -04:00
Tom Rothamel 79945d9649 rpu: Prepare to generate updates.
* Add FileList.add_file and FileList.add_directory.
* Prepend the rpus with the name of the list that creates them.
* Delete obsolete rpus.
2023-10-06 22:57:23 -04:00
Tom Rothamel 254fa8c4ee Add missing encoding field to StdioRedirector.
Fixes a crash in compileall.
2023-10-06 22:56:32 -04:00
Tom Rothamel 14b596e15a Change the Sphinx search role.
Fixes #5037.
2023-10-05 21:59:56 -04:00
Tom Rothamel f33aef4711 Use premultiplied alpha for underline and strikethrough.
Fixes #5033.
2023-10-04 23:17:56 -04:00
Tom Rothamel 7d86e8509a rpu: Move to the renpy.update module. 2023-10-04 22:20:35 -04:00
Tom Rothamel dd26bbf859 rpu: Combine filetypes and util into a single common file. 2023-10-04 01:08:59 -04:00
Tom Rothamel cde1f301d2 rpu: Manage the executable bit.
Also, remove mtime from the json files.
2023-10-03 22:06:23 -04:00
Tom Rothamel 2d8e7d3a1a rpu: Create empty new files. 2023-10-02 23:24:08 -04:00
Tom Rothamel 1c2b3ed231 rpu: Delete files when not necessary, or at the end. 2023-10-02 23:14:43 -04:00
Tom Rothamel 021aa22a80 rpu: Add logging and deletion infrastructure. 2023-10-01 22:50:44 -04:00
Tom Rothamel e548ed13b2 rpu: Add support for reporting progress. 2023-10-01 01:21:03 -04:00
Tom Rothamel b1cf7eb64e Merge pull request #5020 from qTich/fix/pad_enabled
fix: `pad_enabled` instance type,, since can contains `all`
2023-09-30 19:42:55 -04:00
Tom Rothamel 86e9454b79 Merge pull request #5024 from Kassy2048/ipad-detect
New detection for iPad
2023-09-30 19:42:31 -04:00
Kassy f7d2434dff New detection for iPad
Since iOS 13, Safari uses the desktop mode by default on iPad which removes
the "iPad" string from the User-Agent header. The new detection looks for
an Apple product with touch enabled (there is no MacOS product with touch
screens AFAIK).
2023-10-01 01:36:00 +02:00
Tichq 07c96e8872 fix: pad_enabled instance type,, since can contains all 2023-09-30 11:50:27 +03:00
Tom Rothamel 0f51948f80 Search the interpolate namespace for variables during interpolation. 2023-09-29 22:04:43 -04:00
Ren'Py Bot 8448808c67 Merge branch 'fix' 2023-09-29 04:30:08 -04:00
Tom Rothamel 8da08dd187 Restore a (now simpler) render_movie.
Since this function is used to show cutscenes.

Fixes #5015.
2023-09-28 21:16:07 -04:00
Tom Rothamel eedc86f20e rpu: Use range downloads to get block files. 2023-09-28 01:25:10 -04:00
Tom Rothamel ccb978f454 rpu: Add a progress callback to the downloader. 2023-09-28 00:16:26 -04:00
Tom Rothamel 33830fd6b4 rpu: Start work on range download library. 2023-09-27 23:45:33 -04:00
Tom Rothamel 615c624a70 Revert "Call functions when interpolating."
This reverts commit 4d8d065c89.
2023-09-27 22:00:14 -04:00
Gouvernathor 66f17ae6dd Avoid using non-f-string interpolation syntax 2023-09-27 23:34:01 +02:00
Ren'Py Bot 7bf7303f85 Merge branch 'fix' 2023-09-27 04:30:08 -04:00
Tom Rothamel 4d8d065c89 Call functions when interpolating.
This will make it possible to have accessors when interpolating.

Fixes #5009.
2023-09-27 01:46:24 -04:00
Tom Rothamel fc111cc0ab Merge pull request #5007 from renpy/custom-text-tags
Custom text tags
2023-09-26 21:32:44 -04:00
Gouvernathor b535e2b63c Disown dynamic ctt, add warning markup, remove useless label 2023-09-27 00:28:39 +02:00
Tom Rothamel fb294567fe rpu: Execute the plan.
This involves copying data from the source files, including block
files that are on disk, into the target files, and then renaming
those files when done.
2023-09-26 02:26:00 -04:00
Tom Rothamel b2893b5ea8 rpu: Create plan.
The plan decides where to get data from, and write it to.
2023-09-25 23:58:33 -04:00
Tom Rothamel 5853d63076 Make sure the .rpa padding is taken fron a local file.
This will prevent a block file from being downloaded just to gain
access to the .rpa padding.
2023-09-25 22:39:23 -04:00
Tom Rothamel bd7cd2a0c6 rpu: Consider .new.rpu files as sources for update data.
So as to recover from a failed update.
2023-09-24 09:34:41 -04:00
Tom Rothamel 9d42e5c667 rpu: Log the initial evaluation of files. 2023-09-24 01:51:20 -04:00
Tom Rothamel ab9ee40e1a rpy: Add the ability to unserialize the file types. 2023-09-24 01:51:20 -04:00
Ren'Py Bot 0aa28b6804 Merge branch 'fix' 2023-09-23 04:30:07 -04:00
Tom Rothamel 0891fff309 rpu: Generate the block files and index.
The block files contain the deduplicated segments, while the
index stores where segments are in the destination files and
the block files.
2023-09-23 01:37:30 -04:00
Tom Rothamel ca37ebacd1 rpu: Add support for scanning a .rpa file. 2023-09-22 23:29:34 -04:00
Tom Rothamel 51aff5d94d rpu: Initial work, scanning directoriues to create files. 2023-09-22 22:22:36 -04:00
Gouvernathor 7ff6acc36c french translations 2023-09-23 01:44:00 +02:00
Denys cda1c18e03 Update Ukrainian translations (#4993) 2023-09-23 00:46:53 +02:00
Denys 7f615dcddc Update error.rpy (#4992) 2023-09-22 16:43:52 +02:00
Ren'Py Bot 646a7535b0 Merge branch 'fix' 2023-09-22 04:30:08 -04:00
Tom Rothamel 4600339cf7 Update the .gitignore. 2023-09-21 22:58:44 -04:00
Tom Rothamel 03fdf2801e Simplify how Movie's size argument works.
Previously it had been its own code path, but now the movie
is rendered normally and then resized. Fixes #4983.
2023-09-21 22:45:29 -04:00
Ren'Py Bot e427b4e910 Merge branch 'fix' 2023-09-21 04:30:08 -04:00
Gouvernathor 0826778a5e Add a warning when creating new projects in renpy 7
not disabled, because it still makes sense for test purposes or to update default files
2023-09-21 04:30:07 -04:00
Gouvernathor 5050ee20ee doc typo 2023-09-21 04:30:07 -04:00
Tom Rothamel b4cf19cb0c Filter out the _sustain tag when choosing live2d attributes.
Fixes #4984.
2023-09-20 23:06:25 -04:00
Tom Rothamel dee35cfc66 Allow the DPI of SVG images to be selected.
With a new DPI parameter to Image. Fixes #4832.
2023-09-19 22:53:58 -04:00
Gouvernathor 325175f3c3 Better detect when the console should expect a new line before executing the block (#4834)
* Make blocks with comments open a newline in the console

* Support decorators in the console

* Support ending lines with \ in the console
2023-09-19 21:59:28 -04:00
Tom Rothamel 787f09929b Merge pull request #4691 from renpy/warn-new-renpy7
Add a warning when creating new projects in renpy 7
2023-09-19 21:51:14 -04:00
Vladya c5f9ddeae8 "Backslash in filename" fix
Fixes "Backslash in filename" error, when using method from pkgutil.get_data and similar.
2023-09-19 21:49:11 -04:00
Tom Rothamel c43c34c943 Merge pull request #4966 from NyashniyVladya/patch-1
RenpyImporter.get_data fix
2023-09-19 21:47:35 -04:00
Gouvernathor ce07691bbc doc typo 2023-09-19 16:12:41 +02:00
brainos233 9e76a31da1 doc: add missing reference end-string (#4978) 2023-09-19 16:12:16 +02:00
Gouvernathor 90701d03c1 doc typo 2023-09-19 16:10:28 +02:00
Ren'Py Bot 0256f2bba7 Merge branch 'fix' 2023-09-19 04:30:56 -04:00
Tom Rothamel a12d9c544e Bump fix branch versions. 2023-09-19 00:12:40 -04:00
Tom Rothamel 8bb5291324 doc: Update sponsor doc. 2023-09-18 22:27:29 -04:00
Tom Rothamel 186b082e3c Finish changelog. 2023-09-18 22:24:59 -04:00
Tom Rothamel 4dd40736a1 live2d: Install 32-bit windows library.
Fixes #4953.
2023-09-18 22:22:08 -04:00
Tom Rothamel 8c3741e6f9 Fix a truly ancient translation bug. 2023-09-18 18:34:41 -04:00
Gouvernathor 6e7f28f574 Update the ShowMenu doc entry (#4967)
better apparent signature, better documenting how args and kwargs work
2023-09-18 18:22:46 -04:00
Gouvernathor fa24e47d29 Update the ShowMenu doc entry (#4967)
better apparent signature, better documenting how args and kwargs work
2023-09-18 18:22:12 -04:00
Ren'Py Bot 12acbfc963 Merge branch 'fix' 2023-09-18 04:30:45 -04:00
Tom Rothamel 8e1945a628 Mark a context as non interacting when it's copied for rollback.
This is an attempt at fixing a stack trace that ended wit:

  File "C:\renpy\renpy\ui.py", line 299, in interact
    rv = renpy.game.interface.interact(roll_forward=roll_forward, **kwargs)
  File "C:\renpy\renpy\display\core.py", line 3556, in interact
    raise Exception("Cannot start an interaction in the middle of an interaction, without creating a new context.")
Exception: Cannot start an interaction in the middle of an interaction, without creating a new context.

After an autoreload.
2023-09-17 22:57:02 -04:00
Ren'Py Bot fdae52f6ad Merge branch 'fix' 2023-09-17 04:30:56 -04:00
Tom Rothamel 4b47d59b3b steam: Make the steam deck keyboard a lot less complicated.
This disables:

* Automatic showing the keyboard if the player dismisses it.
* Shifting the screen to where the keyboard is,.

And it changes the keyboard area to the top of the screen.

The goal of this is to work around recently-introduced issues with
Steam Deck keyboard handling.
2023-09-16 23:45:59 -04:00
Tom Rothamel 9cde14fa6d steam: Make the steam deck keyboard a lot less complicated.
This disables:

* Automatic showing the keyboard if the player dismisses it.
* Shifting the screen to where the keyboard is,.

And it changes the keyboard area to the top of the screen.

The goal of this is to work around recently-introduced issues with
Steam Deck keyboard handling.
2023-09-16 23:41:57 -04:00
Tom Rothamel a98fa5f7fe hb: Changelog. 2023-09-16 20:07:15 -04:00
Tom Rothamel bf0bc1d837 Update to Emoji 15.1. 2023-09-16 18:44:36 -04:00
Tom Rothamel db24c1eb3a hb: Document variable fonts. 2023-09-16 00:59:20 -04:00
Tom Rothamel c8a88bb17e hb: Fix minor issue with variable fonts and text tags. 2023-09-16 00:41:33 -04:00
Tom Rothamel 9c73cff8ce hb: Add renpy.variable_font_info().
A function that returns information about a variable font, so
that the creator knows how Ren'Py understands their font.
2023-09-15 01:11:22 -04:00
Ren'Py Bot 5a4b14009e Merge branch 'fix' 2023-09-14 23:23:49 -04:00
Tom Rothamel a4cb1a123e Avoid calling interface.timeout when the interface isn't ready.
Fixes #4968.
2023-09-14 18:15:42 -04:00
Tom Rothamel 057f69602b Avoid nicing distribute. 2023-09-14 17:57:59 -04:00
Ren'Py Bot 2040da88db Merge branch 'fix' 2023-09-14 04:30:56 -04:00
Tom Rothamel 82de5b7af0 Revert "Enable or disable text input as required."
This reverts commit a65cecfc18.

Bug #4784 is implicated in this, but there's a new fix
in the form of an SDL patch.
2023-09-14 01:10:15 -04:00
Tom Rothamel a688409eb9 hb: Implement the axis: and instance text tags.
These let the user set an axis or an instance of a variable
font.
2023-09-13 09:08:15 -04:00
Tom Rothamel 2ec1cb0cdc hb: Make font variations case insensitive. 2023-09-13 08:20:37 -04:00
Tom Rothamel a73082df6f hb: Add styles to support variable fonts. 2023-09-13 01:21:17 -04:00
Tom Rothamel e33888949a hb: Construct the harfbuzz face after the variable font stuff is set.
This fixes issues where some data appears to remain cached.
2023-09-13 01:15:48 -04:00
Tom Rothamel 6422153ce1 hb: Allow instances and axis values of variable fonts to be specified. 2023-09-12 23:32:46 -04:00
Tom Rothamel 9d1263d7c3 hb: Add the first support for font instances and variations. 2023-09-12 00:38:35 -04:00
Tom Rothamel 608cbab10c Use the correct key. 2023-09-11 21:16:02 -04:00
Tom Rothamel 2b08c40a4a Add config.font_hinting.
This is a single variable that allows all uses of a font to
have the same hinting, which styles can override.
2023-09-10 22:06:56 -04:00
Vladya bd01358481 "Backslash in filename" fix
Fixes "Backslash in filename" error, when using method from pkgutil.get_data and similar.
2023-09-10 23:30:12 +03:00
Tom Rothamel 1e518437dd Revert to using the "auto" hinter.
With renpy/renpy-build@ebe6d6a01a,
the autohinter looks quite good on Latin and non-Latin languages,
so we should continue to prefer it.
2023-09-10 00:54:04 -04:00
Tom Rothamel 2096c691ce Implement the auto-light hinting mode.
This isn't going to be the default, but it is available if creators
want it.
2023-09-10 00:46:17 -04:00
Tom Rothamel 6b4eadf0fd Merge branch 'fix' 2023-09-09 21:52:51 -04:00
Tom Rothamel fc8ac8344d Allow a checkpoint to become a checkpoint.
A checkpoint is created when a failed rollback happens, and probably
also when saving. When that statement finishes, we'd want the
checkpoint to become hard.

Fixes #4963.
2023-09-09 20:47:12 -04:00
Ren'Py Bot f6d8afdad6 Merge branch 'fix' 2023-09-09 04:30:55 -04:00
Tom Rothamel 420564557f Force the gl2 renderer to be used on macOS.
This is because there are major macOS-relevant fixes to the renderer,
such as the fix for #4155, that don't make sense to backport to the
gl renderer.
2023-09-08 22:32:17 -04:00
Ren'Py Bot a8cab927bb Merge branch 'fix' 2023-09-08 04:30:56 -04:00
Tom Rothamel 81ee1d994e Fix "attempted relative import with no known parent package"
By setting __package__ correctly. Fixes #4961.
2023-09-08 01:29:30 -04:00
Tom Rothamel 52c1e64b6f steam: Force the window to be the top of screen.
The previous code could - and would - put the text onto the bottom
half of the screen, which popped the textbox up on the top half,
which looked terrible. By forcing the location, the end result
looks much nicer.

Fixes #4959.
2023-09-07 23:24:33 -04:00
Gouvernathor 3ef755d5d4 Merge branch 'fix' 2023-09-06 21:34:15 +02:00
Gouvernathor e719dd8d47 doc: fixes
fixes #4956 and something that was only fixed on master
2023-09-06 21:33:00 +02:00
Gouvernathor ab3f1242d7 Merge branch 'fix' 2023-09-06 11:21:04 +02:00
Gouvernathor 8df82cbac0 doc: fixes and improvements on the midnight commit 2023-09-06 11:20:34 +02:00
Gouvernathor 13c557b5a2 doc: remove duplication 2023-09-06 10:57:11 +02:00
Ren'Py Bot 44e32ddffb Merge branch 'fix' 2023-09-06 04:30:56 -04:00
Tom Rothamel 60745892c1 Bump versions. 2023-09-06 00:44:45 -04:00
Tom Rothamel d511804be3 Produce a valid-ish version for pygame_sdl2. 2023-09-05 22:06:47 -04:00
Tom Rothamel 487c60da2d doc: Changelog. 2023-09-05 21:13:50 -04:00
Gouvernathor 1a375c525c Add a warning when creating new projects in renpy 7
not disabled, because it still makes sense for test purposes or to update default files
2023-06-04 11:45:22 +02:00
Gouvernathor 553ada3b04 Lint parameters shadowing builtin names 2022-08-18 22:38:43 +02:00
99 changed files with 5915 additions and 2493 deletions
+6
View File
@@ -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
View File
@@ -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']:
+16
View File
@@ -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
+1 -1
View File
@@ -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)
+5 -1
View File
@@ -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
+1 -1
View File
@@ -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):
+3
View File
@@ -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:
+3 -1
View File
@@ -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 -10
View File
@@ -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()
+68 -20
View File
@@ -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"
+13 -4
View File
@@ -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):
+21 -1
View File
@@ -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."
+146 -145
View File
@@ -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"
+23 -23
View File
@@ -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"
+27 -27
View File
@@ -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
+40 -44
View File
@@ -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"
+5 -5
View File
@@ -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
+4 -5
View File
@@ -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
+5 -5
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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,
+46 -1
View File
@@ -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 )
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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"):
+3
View File
@@ -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
View File
@@ -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):
+4 -2
View File
@@ -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.
+32
View File
@@ -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
View File
@@ -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):
+8 -1
View File
@@ -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:
+1 -2
View File
@@ -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")
+9 -5
View File
@@ -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"
+17
View File
@@ -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
View File
@@ -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),
)
+29 -4
View File
@@ -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" ] +
+2 -2
View File
@@ -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
+19 -6
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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)
+51
View File
@@ -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))
+1 -1
View File
@@ -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
+6
View File
@@ -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
+3 -1
View File
@@ -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:
+1 -6
View File
@@ -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
View File
@@ -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.
"""
"""
+4
View File
@@ -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.
+2 -2
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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)
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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")
+3
View File
@@ -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
View File
@@ -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.
+2
View File
@@ -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
View File
@@ -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)
+2
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.")
+23
View File
@@ -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 # *
+362
View File
@@ -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")))
+229
View File
@@ -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)
+186
View File
@@ -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()
+636
View File
@@ -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
View File
@@ -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):
"""
+4 -4
View File
@@ -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
+3 -1
View File
@@ -1,4 +1,6 @@

python early:
config.generating_documentation = True
init 1000000 python:
import doc
import shaderdoc
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+12 -2
View File
@@ -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
+14 -10
View File
@@ -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
--------
+8 -10
View File
@@ -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.
+38
View File
@@ -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
View File
@@ -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
+6 -7
View File
@@ -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``.
+3 -3
View File
@@ -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
-2
View File
@@ -1,5 +1,3 @@
.. _labels-control-flow:
Labels & Control Flow
=====================
+129 -126
View File
@@ -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.
+63 -1
View File
@@ -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
+2 -2
View File
@@ -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
File diff suppressed because it is too large Load Diff
+63 -37
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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
-----------------------