Compare commits

...

153 Commits

Author SHA1 Message Date
Andy_kl 902514c3e1 Add col_offset parameter to py_compile`. 2024-11-03 19:00:44 +04:00
Andy_kl 3ace1a1cbb Rewrite fix_locations.
1. It makes `ast.increment_lineno` in the same pass.
2. It correctly sets `end_lineno/end_col_offset`.
3. It can strip `end_...`s for generated AST.
2024-11-03 02:18:29 +04:00
Andy_kl 8fdafb50ba Do not accept ast in py_compile.
It was used in screen lang v1 and no longer used.
2024-11-03 01:32:41 +04:00
Andy_kl b6b6cfa9ea Render multi-caret parse errors correctly. 2024-11-03 00:10:11 +04:00
Andy_kl d5416f8f86 Work around how Python tokenizer init SyntaxError. 2024-11-03 00:02:26 +04:00
Andy_kl 88af74ab10 Modernize ParserError and uses. 2024-11-02 23:59:50 +04:00
Andy_kl a0a84714ce Add monkeypatch of linecache.updatecache.
This replaces `renpy.lexer.get_line_text` but also
allows other python modules to read renpy files,
namely `traceback` module.
2024-11-02 20:49:08 +04:00
Ren'Py Bot 2155bb02ae Merge branch 'fix' 2024-11-02 04:30:28 -04:00
Tom Rothamel bc6e2a63d0 Only set the tag if it's not explicitly set. 2024-10-31 22:07:10 -04:00
Ren'Py Bot 204ca35321 Merge branch 'fix' 2024-10-31 04:30:28 -04:00
Tom Rothamel 899307d791 Make show expression similar to show.
In the most common string case, it will now use the tag from
the string, rather than the expression.

Per #5877.
2024-10-31 01:16:33 -04:00
Tom Rothamel 0bf4ab51d6 Make show expression use a unique tag.
This prevents tags from conflicting by mistake. Discussed at #5877.
2024-10-30 22:52:37 -04:00
Tom Rothamel 88d3870d5c Merge branch 'fix' 2024-10-30 22:31:14 -04:00
Tom Rothamel db7d998c38 doc: Sponsors. 2024-10-30 22:31:07 -04:00
Ren'Py Bot b1623451e8 Merge branch 'fix' 2024-10-30 04:30:30 -04:00
Tom Rothamel af7314a8a1 Merge pull request #5875 from AbdulGoodLooks/patch-5
Fix typo in changelog.rst
2024-10-29 18:04:56 -04:00
Abdul a8567d0811 Fix typo in changelog.rst 2024-10-29 20:59:27 +00:00
Tom Rothamel 74a6660ae1 Merge branch 'fix' 2024-10-28 01:30:51 -04:00
Tom Rothamel 797e37678a Revert "Fix an issue that could stop audio rollback from working."
This reverts commit 14ced3377a.
2024-10-28 00:30:33 -04:00
Tom Rothamel c4528cf6ad Improve selection of what Python versions to include.
Fixes #5866.
2024-10-27 19:09:07 -04:00
Ren'Py Bot 1417e58963 Merge branch 'fix' 2024-10-27 04:30:30 -04:00
Ren'Py Bot 65440785e5 Merge branch 'fix' 2024-10-25 01:11:50 -04:00
Tom Rothamel c8a2c7036f Merge pull request #5864 from craigds/fix-obvious-undefined-error
Fix an obvious undefined variable error
2024-10-24 20:58:52 -04:00
Craig de Stigter c512985741 Fix an obvious undefined variable error 2024-10-25 11:57:58 +13:00
Ren'Py Bot a2d6911b4e Merge branch 'fix' 2024-10-24 04:30:30 -04:00
Ren'Py Bot d59029d66c Merge branch 'fix' 2024-10-23 04:30:30 -04:00
Ren'Py Bot a9ecae1a59 Merge branch 'fix' 2024-10-22 04:30:30 -04:00
Tom Rothamel 3bf1b78250 Merge pull request #5855 from Ayowel/patch-1
Only ignore .rpe files if they are not explicitly tracked
2024-10-21 22:26:19 -04:00
Ayowel 2eda0dc2e9 Do not needlessly track files in late_base_patterns 2024-10-21 22:10:39 +02:00
Ayowel 7084ee4b46 Only ignore .rpe files if they are not explicitly tracked 2024-10-21 20:44:56 +02:00
Ren'Py Bot 93d760c9fa Merge branch 'fix' 2024-10-21 04:30:30 -04:00
Ren'Py Bot 01f74075b8 Merge branch 'fix' 2024-10-20 04:30:30 -04:00
Ren'Py Bot 51f523b3c3 Merge branch 'fix' 2024-10-19 04:30:30 -04:00
Tom Rothamel faf1e15710 Document that renpy.emscripten is the emscripten module.
Closes #5848.
2024-10-19 00:34:50 -04:00
Ren'Py Bot ef4c0bd1d9 Merge branch 'fix' 2024-10-18 04:30:30 -04:00
Tom Rothamel 339300d2a0 Report non-dynamic images that reference non-existent images.
Fixes #5771.
2024-10-18 01:35:40 -04:00
Tom Rothamel 51f6b0d756 Add referrer support to installer.download. 2024-10-18 01:35:40 -04:00
Tom Rothamel f547ea3ed1 Restart looping movies during rollback.
Fixes #5564.
2024-10-17 22:31:30 -04:00
Ren'Py Bot 8911825d46 Merge branch 'fix' 2024-10-15 04:30:30 -04:00
Ren'Py Bot ddc3f67539 Merge branch 'fix' 2024-10-14 04:30:30 -04:00
Tom Rothamel 99915a3ed3 Merge pull request #5772 from the66F95/replays-seen-events
no seen labels, images, audio in replays
2024-10-13 19:54:07 -04:00
avonder 21774019a7 android: Install-time asset pack support. (#5837)
Enable reading from APK-based asset packs
2024-10-13 19:32:10 -04:00
Ren'Py Bot 33fbe01388 Merge branch 'fix' 2024-10-13 04:30:30 -04:00
Tom Rothamel 82dd5968e3 Merge pull request #5838 from racinmat/readme-update
docs: restrict the cython version and add setuptools
2024-10-12 10:06:15 -04:00
Matěj Račinský 43c6f83aff docs: restrict the cython version and add setuptools 2024-10-12 14:25:29 +02:00
Tom Rothamel e6fc6e3c26 Scale image fonts using nearest neighbor.
When config.nearest_neighbor is True.

Fixes #5830.
2024-10-11 21:53:13 -04:00
the66F95 7fe14c09c9 Update persistentexports.py 2024-10-11 09:12:39 +02:00
the66F95 588824cf3f Update config.py 2024-10-11 09:11:58 +02:00
Tom Rothamel ddf7b35160 Merge pull request #5687 from onitake/patch/ffmpeg-7
Replace deprecated APIs when compiling against newer FFmpeg
2024-10-10 23:15:05 -04:00
Tom Rothamel d85879b1fd Script to find recursive inner functions. 2024-10-08 22:53:24 -04:00
Tom Rothamel adfbb4970b Remove recursive inner functions in transform and lexer. 2024-10-08 09:43:19 -04:00
Ren'Py Bot bbf2cff58f Merge branch 'fix' 2024-10-08 04:30:30 -04:00
Tom Rothamel a1a0b78cb4 Remove recursive inner functions.
Per #5832.
2024-10-08 01:29:56 -04:00
Tom Rothamel 4cd14e18b5 Factor out check_spline_types.
Recursive inner functions create reference loops that need to be
GC-ed, so eliminating such can reduce GC pauses.

Per #5832.
2024-10-07 22:52:08 -04:00
Ren'Py Bot e70011862a Merge branch 'fix' 2024-10-07 04:30:27 -04:00
Ren'Py Bot 916c6cc9e9 Merge branch 'fix' 2024-10-06 04:30:30 -04:00
Ren'Py Bot f1e2d074c6 Merge branch 'fix' 2024-10-05 04:30:30 -04:00
Tom Rothamel 6e83ae9bd1 Display if transform are applied to a layer.
Fixes #5822.
2024-10-05 01:01:43 -04:00
Tom Rothamel d3506085c8 Add an internal api to determine if transforms are applied to a layer. 2024-10-05 00:59:40 -04:00
Ren'Py Bot 02d7a3305a Merge branch 'fix' 2024-10-04 04:30:30 -04:00
Ren'Py Bot cd590d78eb Merge branch 'fix' 2024-10-03 04:30:30 -04:00
Ren'Py Bot 0ed2c16ee9 Merge branch 'fix' 2024-10-02 04:30:30 -04:00
Tom Rothamel fc429eb984 Merge pull request #5748 from remarkablemark/patch-1
doc: Fix code block in cds.rst
2024-10-01 22:46:57 -04:00
Tom Rothamel 623c5f6d1f Merge pull request #5811 from sunrisesarsaparilla/master
remove unused code
2024-10-01 22:33:33 -04:00
Ren'Py Bot b430ca2243 Merge branch 'fix' 2024-09-30 04:30:30 -04:00
Ren'Py Bot 8e21bcbc00 Merge branch 'fix' 2024-09-29 04:30:30 -04:00
Tom Rothamel 31ff6d3e95 py311: Remove now-obsolete AST properties. 2024-09-28 01:36:39 -04:00
Tom Rothamel 6aadfe4d15 py311: Improve analysis of the match statement.
This doesn't bother to try to analyze it, but it will mark the
correct variables as non-constant.
2024-09-28 01:29:38 -04:00
Tom Rothamel 435fec2be6 py311: Wrap the match statement. 2024-09-27 23:53:47 -04:00
Tom Rothamel 6b507d5a57 Merge branch 'fix' 2024-09-27 18:22:25 -04:00
sunrisesarsaparilla 25b0068b1d remove duplicate code line 2024-09-27 01:37:17 -07:00
Ren'Py Bot b1a3171a40 Merge branch 'fix' 2024-09-26 04:30:30 -04:00
sunrisesarsaparilla 30af1dfb70 remove unused code 2024-09-25 22:50:32 -07:00
Tom Rothamel fca89d813e Merge pull request #5809 from sunrisesarsaparilla/fix-parallel-build-bug-that-adds-one-second
fix bug that adds 1 second to each successive parallel build
2024-09-25 09:53:40 -04:00
sunrisesarsaparilla 9050a9d5a5 fix bug that adds 1 second to each successive parallel build 2024-09-25 06:26:53 -07:00
Tom Rothamel d257b9dc70 doc: #5756. 2024-09-25 00:51:26 -04:00
Tom Rothamel 89c79e55fe Merge pull request #5756 from sunrisesarsaparilla/customize-renpy-launcher-file-shorcuts
allow 'Open Directory' and 'Edit File' customization on renpy launch screen #2467
2024-09-24 21:17:49 -04:00
Gouvernathor d7f194072e fix unbound exception 2024-09-25 01:40:55 +02:00
Tom Rothamel 60ca4cd5ae py3: Always use the Python 3 compilation path. 2024-09-23 23:10:17 -04:00
Gouvernathor 20c1f572d7 Revert use of unpicklable MappingProxyType and wrong import order
fixes #5797
2024-09-20 20:25:50 +02:00
Tom Rothamel a73e20288c Merge pull request #5783 from renpy/py3-signature
Modernize the parameters module
2024-09-19 20:22:21 -04:00
Ren'Py Bot 16fe7290fa Merge branch 'fix' 2024-09-19 04:30:30 -04:00
Ren'Py Bot ff04b3ea56 Merge branch 'fix' 2024-09-18 04:30:30 -04:00
Ren'Py Bot 903e971395 Merge branch 'fix' 2024-09-17 04:30:30 -04:00
Tom Rothamel 45e942de05 Merge pull request #5790 from brimbel/patch-1
doc: Fix example of shader text tag
2024-09-17 00:40:04 -04:00
brimbel 24a699ba12 Fix example of shader text tag
jitter text shader does not have a speed uniform
2024-09-16 15:50:07 -07:00
Ren'Py Bot f5ed3d7947 Merge branch 'fix' 2024-09-16 04:30:30 -04:00
Ren'Py Bot c761ba4c75 Merge branch 'fix' 2024-09-15 04:30:30 -04:00
Tom Rothamel 79fbc7718e Avoid distributing renpy.gl. 2024-09-14 16:02:35 -04:00
Tom Rothamel 7d4f7d17f9 Doc: Requirements changes. 2024-09-14 16:00:24 -04:00
Tom Rothamel 439bcf73f3 Do not include armv7l builds. 2024-09-14 15:52:01 -04:00
Gouvernathor dc81a5edeb Simplify defaulted fields 2024-09-14 18:08:39 +02:00
Gouvernathor 331a4f0211 Add from clauses to raises 2024-09-14 16:39:36 +02:00
Gouvernathor 69f1a2bb0b Optimize empty arguments parsing 2024-09-14 16:33:42 +02:00
Gouvernathor cd90dfa789 Clamp down ArgumentInfo fields
the mutable arguments are not converted, but this is better
2024-09-14 16:32:47 +02:00
Gouvernathor d20d9f2aa7 Use emptymap 2024-09-14 16:30:35 +02:00
Gouvernathor fb1d40db3c Use naturally ordered dict
mappingproxy requires an additional pickling handle which would introduce yet another switch
2024-09-14 16:29:23 +02:00
Gouvernathor 4f835f1250 Change Parameter's parameter rules 2024-09-14 16:27:14 +02:00
Tom Rothamel 0843e46e91 Fix an issue with with sdist, versions, and pygame_sdl2. 2024-09-14 00:19:09 -04:00
Tom Rothamel a0963c18ee Call install_headers.py. 2024-09-13 22:22:51 -04:00
Ren'Py Bot f0dfe18bbf Merge branch 'fix' 2024-09-12 04:30:30 -04:00
Tom Rothamel 9c6cc41ec3 doc: Windows 10 required. 2024-09-12 01:27:55 -04:00
Tom Rothamel 41e13911e3 It's no longer requires to install_headers for pygame_sdl2. 2024-09-12 01:12:15 -04:00
Tom Rothamel 35f6794db2 Remove old find_module method.
As it's not needed in recent Pythons.
2024-09-12 00:47:58 -04:00
Tom Rothamel 699c2a1679 Fix deprecation warning. 2024-09-12 00:46:58 -04:00
Tom Rothamel cad9dc37c2 Merge pull request #5686 from onitake/patch/absolute-launcher-imports
Support find_spec in custom module loader
2024-09-12 00:45:51 -04:00
Tom Rothamel deb959a1e7 Change some of the build process to support Python 3.12. 2024-09-12 00:44:29 -04:00
Tom Rothamel 93c4c4e8df Merge pull request #5775 from levicratic/remove-second-push-from-size-segment
Fix size tag preventing outer tags from closing
2024-09-11 21:32:40 -04:00
Ren'Py Bot c634ff91d2 Merge branch 'fix' 2024-09-10 04:30:30 -04:00
levicratic 5b0f4cf5a7 Fix size tag preventing outer tags from closing 2024-09-09 13:51:27 -04:00
Ren'Py Bot ddd0896840 Merge branch 'fix' 2024-09-09 04:30:30 -04:00
the66F95 a175edde26 no seen labels, images, audio in replays
i don't think labels, images and audio files should be marked as seen in replays. basically all other variables are local to that context.
for instance developer would be able now to temporarily unlock content of a replay gallery for higher tier patrons without actually changing the seen status of a label.
to preserve the old behavior one can set the new config var to True.
2024-09-09 10:21:21 +02:00
Tom Rothamel 4ebedb6a20 py2: Remove PY2 conditions from launcher. 2024-09-08 19:50:31 -04:00
Tom Rothamel d3c827fc55 py2: Remove support for PY2 in the compat module.
The compat module still needs to exist to keep support for older
games and saves, but a lot of it went away.
2024-09-08 19:40:13 -04:00
Tom Rothamel fc9744192d py2: Remove things conditioned on PY2. 2024-09-08 19:23:25 -04:00
Tom Rothamel 72602ab6ab py2: Remove py2analysis.
As part of the process of dropping support for Python 2.
2024-09-08 17:07:15 -04:00
Ren'Py Bot b78f1d1b41 Merge branch 'fix' 2024-09-08 04:30:30 -04:00
Ren'Py Bot e8bbfde5b1 Merge branch 'fix' 2024-09-07 04:30:30 -04:00
Ren'Py Bot 79bda8a568 Merge branch 'fix' 2024-09-06 04:30:30 -04:00
Tom Rothamel 1fefbeba52 Remove checks for config.gl2. 2024-09-06 00:40:09 -04:00
Tom Rothamel 1336b5cfb8 Remove the gl, gles, and angle renderers. 2024-09-06 00:34:14 -04:00
Tom Rothamel 935a25490a Merge pull request #5766 from the66F95/render.pyx-debug
Update render.pyx
2024-09-05 13:18:04 -04:00
the66F95 5f73d6f1b2 Update render.pyx
still debug output present.
2024-09-05 19:09:43 +02:00
Ren'Py Bot 595e4d45bd Merge branch 'fix' 2024-09-05 04:30:30 -04:00
Ren'Py Bot c42667b197 Merge branch 'fix' 2024-09-04 04:30:30 -04:00
Ren'Py Bot 00750cb5d5 Merge branch 'fix' 2024-09-03 04:30:30 -04:00
Ren'Py Bot 5f42c6bff9 Merge branch 'fix' 2024-09-02 04:30:30 -04:00
Ren'Py Bot e6e11dab9f Merge branch 'fix' 2024-09-01 04:30:30 -04:00
Tom Rothamel 6351d60213 Merge pull request #5757 from sunrisesarsaparilla/indent-project.json
make project.json easier for humans to read
2024-08-31 23:55:26 -04:00
Sunrise Sarsaparilla 2928e5495c make project.json easier for humans to read 2024-08-31 20:32:52 -07:00
Sunrise Sarsaparilla 0b5a35a208 allow 'Open Directory' and 'Edit File' customization on renpy launch screen 2024-08-31 14:58:01 -07:00
Ren'Py Bot ffd5eddaf9 Merge branch 'fix' 2024-08-31 04:30:30 -04:00
Ren'Py Bot 27b4c46013 Merge branch 'fix' 2024-08-30 04:30:30 -04:00
Ren'Py Bot c8da44781d Merge branch 'fix' 2024-08-29 04:30:30 -04:00
Mark acf2347930 doc: Fix code block in cds.rst 2024-08-28 22:54:52 -04:00
Ren'Py Bot a40d302500 Merge branch 'fix' 2024-08-28 04:30:30 -04:00
Mark 8ad44080ae doc: Fix example in dialogue.rst (#5736) 2024-08-27 16:52:01 -04:00
veydzh3r c41b740ae6 Update ukrainian (#5741)
* Update common.rpy

* Update developer.rpy

* Update gui.rpy

* Update launcher.rpy

* Update options.rpy

* Update screens.rpy

* Update script.rpym
2024-08-27 16:51:34 -04:00
Ren'Py Bot 67e508c979 Merge branch 'fix' 2024-08-27 04:30:30 -04:00
Ren'Py Bot a7e9d39c1d Merge branch 'fix' 2024-08-26 04:30:30 -04:00
Kassy 921ddb07f7 [Web] Only call syncfs once from MultiLocation functions (#5733)
* Do not call syncfs() twice in save_persistent()

With Ren'Py default config, MultiLocation.save_persistent() will call
save_persistent() on 2 different instances of FileLocation. They both
call synfc(), so the warning message "2 FS.syncfs operations in flight
at once, probably just doing extra work" shows up. This commit prevents
that.

* Create and use a context for syncfs pause

* Only call syncfs once from MultiLocation functions

This should handle all the functions of MultiLocation that call
functions of FileLocation that call syncfs().
2024-08-25 23:06:10 -04:00
Ren'Py Bot b2b222f23e Merge branch 'fix' 2024-08-25 04:30:30 -04:00
Ren'Py Bot af202584db Merge branch 'fix' 2024-08-24 22:23:52 -04:00
Ren'Py Bot d38dbc79b2 Merge branch 'fix' 2024-08-24 04:30:30 -04:00
Andrej Klychin 15c6fdf8c1 Merge pull request #5726 from AbdulGoodLooks/patch-4
Fix typos in changelog.rst
2024-08-24 00:21:10 +02:00
Abdul 5e02e88ec0 Fix typos in changelog.rst 2024-08-23 15:52:51 +01:00
Tom Rothamel 3337049968 py2: Disable build process. 2024-08-23 00:56:10 -04:00
Tom Rothamel 2565157a5a Bump versions. 2024-08-23 00:56:10 -04:00
Gregor Riepl 5b0cd0bce7 Use spec_from_loader for the second case 2024-08-10 23:40:10 +02:00
Gregor Riepl 2901314d05 Fix custom importer/finder/loader to be compatible with Python 3.12 2024-08-09 00:36:08 +02:00
Gregor Riepl 9ec1cd3c4f Revert "Make relative imports from rpy scripts absolute"
This reverts commit c213d17436.
2024-08-09 00:31:36 +02:00
Gregor Riepl b120f82f98 Replace deprecated APIs when compiling against newer FFmpeg 2024-08-08 23:56:16 +02:00
Gregor Riepl c213d17436 Make relative imports from rpy scripts absolute 2024-08-08 21:08:43 +02:00
95 changed files with 2592 additions and 7730 deletions
+2 -2
View File
@@ -71,14 +71,14 @@ To return to this virtualenv later, run::
After activating the virtualenv, install additional dependencies::
pip install -U cython future six typing pefile requests ecdsa
pip install -U "setuptools cython<3.0.0 future six typing pefile requests ecdsa"
Then, install pygame_sdl2 by running the following commands::
git clone https://www.github.com/renpy/pygame_sdl2
pushd pygame_sdl2
python setup.py install
python setup.py install_headers
python install_headers.py $VIRTUAL_ENV
popd
Next, set RENPY_DEPS_INSTALL To a \:-separated (\;-separated on Windows)
+3 -13
View File
@@ -76,24 +76,14 @@ class BinFile(object):
self.addr = addr
def tostring(self):
if PY2:
return self.a.tostring() # type: ignore
else:
return self.a.tobytes()
return self.a.tobytes()
def substring(self, start, len): # @ReservedAssignment
if PY2:
return self.a[start:start + len].tostring() # type: ignore
else:
return self.a[start:start + len].tobytes()
return self.a[start:start + len].tobytes()
def __init__(self, data):
self.a = array.array('B')
if PY2:
self.a.fromstring(data) # type: ignore
else:
self.a.frombytes(data)
self.a.frombytes(data)
##############################################################################
# These functions parse data out of the file. In these functions, offset is
+6 -39
View File
@@ -432,10 +432,7 @@ fix_dlc("renios", "renios")
for f in sorted(self, key=lambda a : a.name):
f.hash(sha, distributor)
if PY2:
return sha.hexdigest().decode("utf-8")
else:
return sha.hexdigest()
return sha.hexdigest()
def split_by_prefix(self, prefix):
"""
@@ -661,7 +658,6 @@ fix_dlc("renios", "renios")
# Build the mac app and windows exes.
self.add_mac_files()
self.add_windows_files()
self.add_main_py()
# Add the main.py.
self.add_main_py()
@@ -1075,11 +1071,8 @@ fix_dlc("renios", "renios")
rv = self.temp_filename("Info.plist")
if PY2:
plistlib.writePlist(plist, rv)
else:
with open(rv, "wb") as f:
plistlib.dump(plist, f)
with open(rv, "wb") as f:
plistlib.dump(plist, f)
return rv
@@ -1114,16 +1107,6 @@ fix_dlc("renios", "renios")
os.path.join(config.renpy_base, prefix + "linux-x86_64/renpy"),
True)
armfn = os.path.join(config.renpy_base, prefix + "linux-armv7l/renpy")
if os.path.exists(armfn):
self.add_file(
raspi,
prefix + "linux-armv7l/" + self.executable_name,
armfn,
True)
aarch64fn = os.path.join(config.renpy_base, prefix + "linux-aarch64/renpy")
if os.path.exists(aarch64fn):
@@ -1226,19 +1209,8 @@ fix_dlc("renios", "renios")
if os.path.exists(tmp):
self.add_file(fl, dst, tmp)
if PY2:
if self.build["include_i686"]:
write_exe("lib/py2-windows-i686/renpy.exe", self.exe32, self.exe32, windows_i686)
write_exe("lib/py2-windows-i686/pythonw.exe", "lib/py2-windows-i686/pythonw.exe", "pythonw-32.exe", windows_i686)
write_exe("lib/py2-windows-x86_64/renpy.exe", self.exe, self.exe, windows)
write_exe("lib/py2-windows-x86_64/pythonw.exe", "lib/py2-windows-x86_64/pythonw.exe", "pythonw-64.exe", windows)
else:
write_exe("lib/py3-windows-x86_64/renpy.exe", self.exe, self.exe, windows)
write_exe("lib/py3-windows-x86_64/pythonw.exe", "lib/py3-windows-x86_64/pythonw.exe", "pythonw-64.exe", windows)
write_exe("lib/py3-windows-x86_64/renpy.exe", self.exe, self.exe, windows)
write_exe("lib/py3-windows-x86_64/pythonw.exe", "lib/py3-windows-x86_64/pythonw.exe", "pythonw-64.exe", windows)
def add_main_py(self):
@@ -1536,7 +1508,7 @@ fix_dlc("renios", "renios")
if self.include_update and not format.startswith("app-"):
with open(update_fn, "wb" if PY2 else "w") as f:
with open(update_fn, "w") as f:
json.dump(update, f, indent=2)
if (not dlc) or (format == "update"):
@@ -1572,11 +1544,6 @@ fix_dlc("renios", "renios")
if os.path.exists(update_fn):
shutil.copy(update_fn, final_update_fn)
if not directory:
file_hash = hash_file(path)
else:
file_hash = ""
if format == "tar.bz2" or format == "bare-tar.bz2":
pkg = TarPackage(path, "w:bz2")
elif format == "update":
+4 -10
View File
@@ -158,12 +158,8 @@ screen front_page_project:
frame style "l_indent":
has vbox
textbutton "game" action OpenDirectory(os.path.join(p.path, "game"), absolute=True)
textbutton "base" action OpenDirectory(os.path.join(p.path, "."), absolute=True)
textbutton "images" action OpenDirectory(os.path.join(p.path, "game/images"), absolute=True)
textbutton "audio" action OpenDirectory(os.path.join(p.path, "game/audio"), absolute=True)
textbutton "gui" action OpenDirectory(os.path.join(p.path, "game/gui"), absolute=True)
for button_name, path in p.data["renpy_launcher"]["open_directory"].items():
textbutton button_name action OpenDirectory(os.path.join(p.path, path), absolute=True)
vbox:
if persistent.show_edit_funcs:
@@ -173,10 +169,8 @@ screen front_page_project:
frame style "l_indent":
has vbox
textbutton "script.rpy" action editor.Edit("game/script.rpy", check=True)
textbutton "options.rpy" action editor.Edit("game/options.rpy", check=True)
textbutton "gui.rpy" action editor.Edit("game/gui.rpy", check=True)
textbutton "screens.rpy" action editor.Edit("game/screens.rpy", check=True)
for button_name, path in p.data["renpy_launcher"]["edit_file"].items():
textbutton button_name action editor.Edit(path, check=True)
if editor.CanEditProject():
textbutton _("Open project") action editor.EditProject()
+1 -1
View File
@@ -172,7 +172,7 @@ class CodeGenerator(object):
for l in self.lines:
m = re.match('^(\s*)define (.*?) =', l)
m = re.match(r'^(\s*)define (.*?) =', l)
if m:
indent = m.group(1)
+1 -9
View File
@@ -81,10 +81,7 @@ init python:
label install_live2d:
python hide:
if PY2:
_prefix = r"lib/py2-"
else:
_prefix = r"lib/py3-"
_prefix = r"lib/py3-"
patterns = [
(r".*/Core/dll/linux/x86_64/(libLive2DCubismCore.so)", _prefix + r"linux-x86_64/\1"),
@@ -97,11 +94,6 @@ 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-[45]-*.zip", patterns)
jump front_page
-3
View File
@@ -164,9 +164,6 @@ init python:
print("Eliminating __pycache__...")
if PY2:
return
import pathlib
import sys
-3
View File
@@ -34,9 +34,6 @@ 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:
new_project_language = __("{#language name and font}")
gui_kind = "new_gui_project"
+15 -13
View File
@@ -322,7 +322,7 @@ init python:
"""
if py is True:
py = 'pyo' if PY2 else 'pycache'
py = 'pycache'
if py == 'pycache':
build.classify_renpy(pattern + "/**__pycache__/", binary)
@@ -357,6 +357,7 @@ init python:
build.classify_renpy(pattern + "/**", source)
build.classify_renpy("renpy/gl/", None)
build.classify_renpy("renpy.py", "binary")
source_and_binary("renpy")
@@ -409,22 +410,23 @@ init python:
build.classify_renpy("module/tinyfiledialogs/**", "source")
build.classify_renpy("module/libhydrogen/**", "source")
# No-longer-needed python.
# all-platforms binary.
build.classify_renpy("lib/**/*steam_api*", "steam")
build.classify_renpy("lib/**/*Live2D*", None)
if PY2:
build.classify_renpy("lib/py2-linux-armv7l/**", "linux_arm")
build.classify_renpy("lib/py2-linux-aarch64/**", "linux_arm")
source_and_binary("lib/py2-**", "binary", "binary")
source_and_binary("lib/python2**", "binary", "binary")
build.classify_renpy("renpy2.sh", "binary")
else:
build.classify_renpy("lib/py3-linux-armv7l/**", "linux_arm")
build.classify_renpy("lib/py3-linux-aarch64/**", "linux_arm")
source_and_binary("lib/py3-**", "binary", "binary")
source_and_binary("lib/python3**", "binary", "binary", py='pyc')
build.classify_renpy("renpy3.sh", "binary")
build.classify_renpy("lib/py3-linux-armv7l", None)
build.classify_renpy("lib/py3-linux-aarch64/**", "linux_arm")
source_and_binary("lib/py3-**", "binary", "binary")
libpython = "lib/python{}.{}".format(sys.version_info.major, sys.version_info.minor)
source_and_binary(libpython, "binary", "binary", py='pyc')
build.classify_renpy("lib/python**", None)
build.classify_renpy("lib/py2-**", None)
build.classify_renpy("renpy3.sh", "binary")
build.classify_renpy("lib/", "binary")
+11 -94
View File
@@ -34,106 +34,21 @@ 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
long = int
zlib.Z_DEFAULT_COMPRESSION = 5
if PY2:
class ZipFile(zipfile.ZipFile):
class ZipFile(zipfile.ZipFile):
def write_with_info(self, zinfo, filename):
def write_with_info(self, zinfo, filename):
"""Put the bytes from filename into the archive under the name
arcname."""
if not self.fp:
raise RuntimeError(
"Attempt to write to ZIP archive that was already closed")
if zinfo.filename.endswith("/"):
data = b''
else:
with open(filename, "rb") as f:
data = f.read()
st = os.stat(filename)
isdir = stat.S_ISDIR(st.st_mode)
zinfo.file_size = st.st_size
zinfo.flag_bits = 0x00
zinfo.header_offset = self.fp.tell() # Start of header bytes
self._writecheck(zinfo)
self._didModify = True
if isdir:
zinfo.file_size = 0
zinfo.compress_size = 0
zinfo.CRC = 0
self.filelist.append(zinfo)
self.NameToInfo[zinfo.filename] = zinfo
self.fp.write(zinfo.FileHeader())
return
with open(filename, "rb") as fp:
# Must overwrite CRC and sizes with correct data later
zinfo.CRC = CRC = 0
zinfo.compress_size = compress_size = 0
file_size = 0
zip64 = self._allowZip64 and \
zinfo.file_size * 1.05 > zipfile.ZIP64_LIMIT
self.fp.write(zinfo.FileHeader(zip64))
if zinfo.compress_type == zipfile.ZIP_DEFLATED:
cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
zlib.DEFLATED, -15)
else:
cmpr = None
while 1:
buf = fp.read(1024 * 1024)
if not buf:
break
file_size = file_size + len(buf)
CRC = crc32(buf, CRC) & 0xffffffff
if cmpr:
buf = cmpr.compress(buf)
compress_size = compress_size + len(buf)
self.fp.write(buf)
if cmpr:
buf = cmpr.flush()
compress_size = compress_size + len(buf)
self.fp.write(buf)
zinfo.compress_size = compress_size
else:
zinfo.compress_size = file_size
zinfo.CRC = CRC
zinfo.file_size = file_size
if not zip64 and self._allowZip64:
if file_size > zipfile.ZIP64_LIMIT:
raise RuntimeError('File size has increased during compressing')
if compress_size > zipfile.ZIP64_LIMIT:
raise RuntimeError('Compressed size larger than uncompressed size')
# Seek backwards and write CRC and file sizes
position = self.fp.tell() # Preserve current position in file
self.fp.seek(zinfo.header_offset, 0)
self.fp.write(zinfo.FileHeader(zip64))
self.fp.seek(position, 0)
self.filelist.append(zinfo)
self.NameToInfo[zinfo.filename] = zinfo
else:
class ZipFile(zipfile.ZipFile):
def write_with_info(self, zinfo, filename):
if zinfo.filename.endswith("/"):
data = b''
else:
with open(filename, "rb") as f:
data = f.read()
self.writestr(zinfo, data)
self.writestr(zinfo, data)
class ZipPackage(object):
@@ -469,3 +384,5 @@ init python in distribute:
for i in parallel_threads:
if i.done:
i.done()
parallel_threads.clear()
+20 -7
View File
@@ -116,13 +116,32 @@ init python in project:
try:
with open(os.path.join(self.path, "project.json"), "w") as f:
json.dump(self.data, f)
json.dump(self.data, f, indent=2)
except Exception:
self.load_data()
def update_data(self):
data = self.data
data.setdefault("renpy_launcher",
{
"open_directory":
{
"game": "game",
"base": ".",
"images": "game/images",
"audio": "game/audio",
"gui": "game/gui"
},
"edit_file":
{
"script.rpy": "game/script.rpy",
"options.rpy": "game/options.rpy",
"gui.rpy": "game/gui.rpy",
"screens.rpy": "game/screens.rpy"
}
})
data.setdefault("build_update", False)
data.setdefault("packages", [ "pc", "mac" ])
data.setdefault("add_from", True)
@@ -352,12 +371,6 @@ init python in project:
line = line[:1024]
if PY2:
try:
line = line.decode("utf-8")
except Exception:
continue
m = re.search(r"#\s*TODO(\s*:\s*|\s+)(.*)", line, re.I)
if m is None:
+10 -15
View File
@@ -440,13 +440,12 @@ init python:
# Find the presplash and copy it over.
presplash = None
if not PY2:
for fn in [ "web-presplash.png", "web-presplash.jpg", "web-presplash.webp" ]:
fullfn = os.path.join(project.current.path, fn)
for fn in [ "web-presplash.png", "web-presplash.jpg", "web-presplash.webp" ]:
fullfn = os.path.join(project.current.path, fn)
if os.path.exists(fullfn):
presplash = fn
break
if os.path.exists(fullfn):
presplash = fn
break
if presplash:
os.unlink(os.path.join(destination, "web-presplash.jpg"))
@@ -456,20 +455,16 @@ init python:
with io.open(os.path.join(WEB_PATH, "index.html"), encoding='utf-8') as f:
html = f.read()
if PY2:
html = html.replace("%%TITLE%%", display_name)
else:
html = html.replace("Ren'Py Web Game", display_name)
html = html.replace("Ren'Py Web Game", display_name)
if presplash:
html = html.replace("web-presplash.jpg", presplash)
if presplash:
html = html.replace("web-presplash.jpg", presplash)
with io.open(os.path.join(destination, "index.html"), "w", encoding='utf-8') as f:
f.write(html)
if not PY2:
generate_web_icons(p, destination)
prepare_pwa_files(p, destination)
generate_web_icons(p, destination)
prepare_pwa_files(p, destination)
# Zip up the game.
-6
View File
@@ -6,7 +6,6 @@ renpy.lexersupport gen/renpy.lexersupport.c
renpy.pydict gen/renpy.pydict.c
renpy.style gen/renpy.style.c
renpy.encryption gen/renpy.encryption.c
renpy.compat.dictviews gen/renpy.compat.dictviews.c
renpy.styledata.styleclass gen/renpy.styledata.styleclass.c
renpy.styledata.stylesets gen/renpy.styledata.stylesets.c
renpy.styledata.style_selected_hover_functions gen/renpy.styledata.style_selected_hover_functions.c
@@ -25,11 +24,6 @@ renpy.display.accelerator gen/renpy.display.accelerator.c
renpy.display.quaternion gen/renpy.display.quaternion.c
renpy.uguu.gl gen/renpy.uguu.gl.c
renpy.uguu.uguu gen/renpy.uguu.uguu.c
renpy.gl.gldraw gen/renpy.gl.gldraw.c
renpy.gl.gltexture gen/renpy.gl.gltexture.c
renpy.gl.glenviron_shader gen/renpy.gl.glenviron_shader.c
renpy.gl.glrtt_copy gen/renpy.gl.glrtt_copy.c
renpy.gl.glrtt_fbo gen/renpy.gl.glrtt_fbo.c
renpy.gl2.gl2mesh gen/renpy.gl2.gl2mesh.c
renpy.gl2.gl2mesh2 gen/renpy.gl2.gl2mesh2.c
renpy.gl2.gl2mesh3 gen/renpy.gl2.gl2mesh3.c
+37
View File
@@ -71,7 +71,11 @@ static int rwops_read(void *opaque, uint8_t *buf, int buf_size) {
}
#if (LIBAVFORMAT_VERSION_MAJOR < 61)
static int rwops_write(void *opaque, uint8_t *buf, int buf_size) {
#else
static int rwops_write(void *opaque, const uint8_t *buf, int buf_size) {
#endif
printf("Writing to an SDL_rwops is a really bad idea.\n");
return -1;
}
@@ -690,9 +694,14 @@ static void decode_audio(MediaState *ms) {
}
converted_frame->sample_rate = audio_sample_rate;
#if (LIBAVUTIL_VERSION_MAJOR < 59)
converted_frame->channel_layout = AV_CH_LAYOUT_STEREO;
#else
converted_frame->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
#endif
converted_frame->format = AV_SAMPLE_FMT_S16;
#if (LIBAVUTIL_VERSION_MAJOR < 59)
if (!ms->audio_decode_frame->channel_layout) {
ms->audio_decode_frame->channel_layout = av_get_default_channel_layout(ms->audio_decode_frame->channels);
@@ -711,6 +720,26 @@ static void decode_audio(MediaState *ms) {
swr_set_matrix(ms->swr, stereo_matrix, 1);
}
}
#else
if (ms->audio_decode_frame->ch_layout.order == AV_CHANNEL_ORDER_UNSPEC) {
av_channel_layout_default(&ms->audio_decode_frame->ch_layout, ms->audio_decode_frame->ch_layout.nb_channels);
if (audio_equal_mono && (ms->audio_decode_frame->ch_layout.nb_channels == 1)) {
swr_alloc_set_opts2(
&ms->swr,
&converted_frame->ch_layout,
converted_frame->format,
converted_frame->sample_rate,
&ms->audio_decode_frame->ch_layout,
ms->audio_decode_frame->format,
ms->audio_decode_frame->sample_rate,
0,
NULL);
swr_set_matrix(ms->swr, stereo_matrix, 1);
}
}
#endif
if(swr_convert_frame(ms->swr, converted_frame, ms->audio_decode_frame)) {
av_frame_free(&converted_frame);
@@ -1159,7 +1188,11 @@ static int decode_thread(void *arg) {
// Compute the number of samples we need to play back.
if (ms->audio_duration < 0) {
#if (LIBAVFORMAT_VERSION_MAJOR < 62)
if (av_fmt_ctx_get_duration_estimation_method(ctx) != AVFMT_DURATION_FROM_BITRATE) {
#else
if (ctx->duration_estimation_method != AVFMT_DURATION_FROM_BITRATE) {
#endif
long long duration = ((long long) ctx->duration) * audio_sample_rate;
ms->audio_duration = (unsigned int) (duration / AV_TIME_BASE);
@@ -1319,7 +1352,11 @@ static int decode_sync_start(void *arg) {
// Compute the number of samples we need to play back.
if (ms->audio_duration < 0) {
#if (LIBAVFORMAT_VERSION_MAJOR < 62)
if (av_fmt_ctx_get_duration_estimation_method(ctx) != AVFMT_DURATION_FROM_BITRATE) {
#else
if (ctx->duration_estimation_method != AVFMT_DURATION_FROM_BITRATE) {
#endif
long long duration = ((long long) ctx->duration) * audio_sample_rate;
ms->audio_duration = (unsigned int) (duration / AV_TIME_BASE);
+4 -16
View File
@@ -154,10 +154,6 @@ cython("renpy.style")
cython("renpy.encryption")
# renpy.compat
if PY2:
cython("renpy.compat.dictviews")
# renpy.styledata
cython("renpy.styledata.styleclass")
cython("renpy.styledata.stylesets")
@@ -174,12 +170,6 @@ cython("renpy.display.quaternion", libs=[ 'm' ])
cython("renpy.uguu.gl", libs=sdl)
cython("renpy.uguu.uguu", libs=sdl)
cython("renpy.gl.gldraw", libs=sdl)
cython("renpy.gl.gltexture", libs=sdl)
cython("renpy.gl.glenviron_shader", libs=sdl)
cython("renpy.gl.glrtt_copy", libs=sdl)
cython("renpy.gl.glrtt_fbo", libs=sdl)
cython("renpy.gl2.gl2mesh")
cython("renpy.gl2.gl2mesh2")
cython("renpy.gl2.gl2mesh3")
@@ -201,12 +191,10 @@ cython(
[ "ftsupport.c", "ttgsubtable.c" ],
libs=sdl + [ 'freetype', 'z', 'm' ])
if not (PY2 and emscripten):
cython(
"renpy.text.hbfont",
[ "ftsupport.c" ],
libs=sdl + [ 'harfbuzz', 'freetype', 'z', 'm' ])
cython(
"renpy.text.hbfont",
[ "ftsupport.c" ],
libs=sdl + [ 'harfbuzz', 'freetype', 'z', 'm' ])
generate_all_cython()
find_unnecessary_gen()
+5 -4
View File
@@ -28,11 +28,12 @@ import os
import sys
import re
import threading
import warnings
import setuptools
warnings.simplefilter("ignore", category=setuptools.SetuptoolsDeprecationWarning)
if sys.version_info.major == 2:
import distutils.core as setuptools
else:
import setuptools
# This flag determines if we are compiling for Android or not.
android = "RENPY_ANDROID" in os.environ
+3 -9
View File
@@ -223,7 +223,6 @@ backup_blacklist = {
"renpy.test.testmouse",
"renpy.test.testparser",
"renpy.gl2",
"renpy.gl",
"renpycoverage",
}
@@ -254,6 +253,7 @@ name_blacklist = {
"renpy.exports.sdl_dll",
"renpy.sl2.slast.serial",
"renpy.gl2.gl2draw.default_position",
"renpy.lexer._python_updatecache",
}
class Backup(_object):
@@ -405,12 +405,9 @@ def import_all():
# Adds in the Ren'Py loader.
import renpy.loader
if not PY2:
import renpy.py3analysis
else:
import renpy.py2analysis
import renpy.pyanalysis
sys.modules["renpy.py3analysis"] = renpy.pyanalysis
import renpy.parameter
import renpy.ast
@@ -479,7 +476,6 @@ def import_all():
sys.modules[pystr('renpy.display.text')] = renpy.text.text
import renpy.gl
import renpy.gl2
import renpy.display.layout
@@ -722,8 +718,6 @@ if 1 == 0:
from . import performance
from . import persistent
from . import preferences
from . import py2analysis
from . import py3analysis
from . import pyanalysis
from . import pydict
from . import python
+24 -4
View File
@@ -157,10 +157,7 @@ class PyCode(object):
if isinstance(source, PyExpr):
loc = (source.filename, source.linenumber, source)
if PY2:
self.py = 2
else:
self.py = 3
self.py = 3
# The source code.
self.source = source
@@ -1065,12 +1062,35 @@ def show_imspec(imspec, atl=None):
if expression is not None:
expression = renpy.python.py_eval(expression)
if not renpy.config.old_show_expression:
if isinstance(expression, str):
name = expression
else:
if tag is None:
counter = 0
while True:
tag = "_show_expression_%d" % counter
if not renpy.exports.showing(tag, layer):
break
counter += 1
name = tag
expression = renpy.easy.displayable(expression)
at_list = [ renpy.python.py_eval(i) for i in at_list ]
layer = renpy.exports.default_layer(layer, tag or name, bool(expression) and (tag is None))
renpy.config.show(name,
at_list=at_list,
layer=layer,
+3 -3
View File
@@ -422,7 +422,7 @@ class ATLTransformBase(renpy.object.Object):
rv_parameters = []
for name, value in self.context.context.items():
if name not in self.parameters.parameters:
rv_parameters.append(ValuedParameter(name, ValuedParameter.KEYWORD_ONLY, value))
rv_parameters.append(ValuedParameter(name, ValuedParameter.KEYWORD_ONLY, default=value))
if rv_parameters:
rv_parameters = list(self.parameters.parameters.values()) + rv_parameters
rv_parameters.sort(key=lambda p: p.kind)
@@ -657,10 +657,10 @@ class ATLTransformBase(renpy.object.Object):
# continue
if passed: # turn into elif when possible
param = ValuedParameter(name, param.KEYWORD_ONLY, scope[name])
param = ValuedParameter(name, param.KEYWORD_ONLY, default=scope[name])
elif param.has_default:
param = ValuedParameter(name, pkind, scope[name])
param = ValuedParameter(name, pkind, default=scope[name])
else:
## not passed and no default value
+3 -2
View File
@@ -723,8 +723,9 @@ class Channel(object):
with lock:
for filename in filenames:
filename, _, _ = self.split_filename(filename, False)
renpy.game.persistent._seen_audio[str(filename)] = True # type: ignore
if renpy.exports.is_seen_allowed():
filename, _, _ = self.split_filename(filename, False)
renpy.game.persistent._seen_audio[str(filename)] = True # type: ignore
if not loop_only:
+1 -10
View File
@@ -37,12 +37,6 @@ FSENCODING = sys.getfilesystemencoding() or "utf-8"
old_stdout = sys.stdout
old_stderr = sys.stderr
if PY2:
sys_executable = sys.executable
reload(sys) # type: ignore
sys.setdefaultencoding("utf-8") # type: ignore
sys.executable = sys_executable
def _setdefaultencoding(name):
"""
This is install in sys to prevent games from trying to change the default
@@ -377,10 +371,7 @@ You may be using a system install of python. Please run {0}.sh,
if hasattr(sys, "renpy_executable"):
subprocess.Popen([sys.renpy_executable] + sys.argv[1:]) # type: ignore
else:
if PY2:
subprocess.Popen([sys.executable, "-EO"] + sys.argv)
else:
subprocess.Popen([sys.executable] + sys.argv)
subprocess.Popen([sys.executable] + sys.argv)
except renpy.game.ParseErrorException:
pass
-3
View File
@@ -1295,9 +1295,6 @@ class ADVCharacter(object):
rv = renpy.substitutions.substitute(who)[0]
if PY2:
rv = rv.encode("utf-8")
return rv
def __format__(self, spec):
+1 -1
View File
@@ -111,7 +111,7 @@ init -1600 python hide:
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
raise Exception("The {!r} {} does not exist.".format(key, self.kind)) from e
class ScreenVariable(Accessor):
"""
+2 -6
View File
@@ -544,12 +544,8 @@ init -1500 python:
if type(self) is not type(other):
return False
if PY2:
if self.callable is not other.callable:
return False
else:
if self.callable != other.callable:
return False
if self.callable != other.callable:
return False
if self.args != other.args:
return False
+1 -1
View File
@@ -215,7 +215,7 @@ init -1500 python:
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
raise Exception("The {!r} {} does not exist".format(self.key, self.kind)) from e
@renpy.pure
class FieldValue(__GenericValue):
+8 -18
View File
@@ -63,23 +63,13 @@ init -1500 python in build:
renpy_sh = "renpy.sh"
if PY2:
renpy_patterns = pattern_list([
("renpy/**.pyo", "all"),
("renpy/**__pycache__", None),
])
renpy_patterns = pattern_list([
("renpy/**__pycache__/**.{}.pyc".format(sys.implementation.cache_tag), "all"),
("renpy/**__pycache__", "all"),
])
if os.path.exists(os.path.join(config.renpy_base, "renpy2.sh")):
renpy_sh = "renpy2.sh"
else:
renpy_patterns = pattern_list([
("renpy/**__pycache__/**.{}.pyc".format(sys.implementation.cache_tag), "all"),
("renpy/**__pycache__", "all"),
])
if os.path.exists(os.path.join(config.renpy_base, "renpy3.sh")):
renpy_sh = "renpy3.sh"
if os.path.exists(os.path.join(config.renpy_base, "renpy3.sh")):
renpy_sh = "renpy3.sh"
# Patterns that are used to classify Ren'Py.
@@ -123,7 +113,7 @@ init -1500 python in build:
( "lib/*/pythonw.exe", None),
# Ignore the wrong Python.
( "lib/py3-*/" if PY2 else "lib/py2-*/", None),
( "lib/py2-*/", None),
# Windows patterns.
( "lib/py*-windows-i686/**", "windows_i686"),
@@ -139,7 +129,7 @@ init -1500 python in build:
( "lib/py*-mac-*/**", "mac"),
# Old Python library.
( "lib/python3.*/**" if PY2 else "lib/python2.*/**", None),
( "lib/python2.*/**", None),
# Shared patterns.
( "lib/**", "windows linux mac android ios"),
+3 -1
View File
@@ -217,7 +217,6 @@ init -1100 python:
if version <= (7, 4, 4):
config.pause_after_rollback = True
config.gl2 = False
config.gl_lod_bias = -1.0
config.who_what_sub_compat = 1
@@ -324,6 +323,9 @@ init -1100 python:
config.box_reverse_align = True
config.limit_transform_crop = True
if version <= (8, 3, 99):
config.old_show_expression = True
# The version of Ren'Py this script is intended for, or
# None if it's intended for the current version.
+4 -8
View File
@@ -156,12 +156,8 @@ init -1500 python in _console:
s = s[:i] + self._ellipsis + s[len(s) - i:]
return s
if PY2:
repr_str = _repr_bytes
repr_unicode = _repr_string
else:
repr_bytes = _repr_bytes
repr_str = _repr_string
repr_bytes = _repr_bytes
repr_str = _repr_string
def repr_tuple(self, x, level):
if not x: return "()"
@@ -212,7 +208,7 @@ init -1500 python in _console:
if level <= 0: return "{...}"
iter_keys = self._to_shorted_list(x, self.maxdict, sort=PY2)
iter_keys = self._to_shorted_list(x, self.maxdict, sort=False)
iter_x = self._make_pretty_items(x, iter_keys, '{', '}')
return self._repr_iterable(iter_x, level, '{', '}')
@@ -227,7 +223,7 @@ init -1500 python in _console:
if level <= 0: return left + "...})"
iter_keys = self._to_shorted_list(x, self.maxdict, sort=PY2)
iter_keys = self._to_shorted_list(x, self.maxdict, sort=False)
iter_x = self._make_pretty_items(x, iter_keys, left, '})')
return self._repr_iterable(iter_x, level, left, '})')
+2 -26
View File
@@ -27,8 +27,7 @@ init -1500:
python:
class _SetRenderer(Action):
"""
Sets the preferred renderer to one of "auto", "angle", "gl", or
"sw".
Sets the preferred renderer.
"""
def __init__(self, renderer):
@@ -95,24 +94,6 @@ init -1500:
action _SetRenderer("auto")
style_suffix "radio_button"
if not config.gl2:
if gl:
textbutton _("Force GL Renderer"):
action _SetRenderer("gl")
style_suffix "radio_button"
if angle:
textbutton _("Force ANGLE Renderer"):
action _SetRenderer("angle")
style_suffix "radio_button"
if gles:
textbutton _("Force GLES Renderer"):
action _SetRenderer("gles")
style_suffix "radio_button"
if gl:
textbutton _("Force GL2 Renderer"):
action _SetRenderer("gl2")
@@ -349,16 +330,11 @@ init -1500 python:
renderer_info = renpy.get_renderer_info()
# Software renderer check.
# Software renderer check.
if config.renderer != "sw" and renderer_info["renderer"] == "sw":
problem = "sw"
allow_continue = False
# Game require gl2 that wasn't initialized.
elif config.gl2 and not renderer_info.get("models", False):
problem = "gl2"
allow_continue = False
if problem is None:
return
+2 -2
View File
@@ -273,8 +273,8 @@ init -1510 python:
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
except LookupError as e:
raise Exception("The {!r} local variable does not exist.".format(self.key)) from e
init -1510 python hide:
if config.generating_documentation:
+1 -1
View File
@@ -173,7 +173,7 @@ init -1800:
adjust_spacing True
emoji_font "TwemojiCOLRv0.ttf"
prefer_emoji True
shaper ("harfbuzz" if (not PY2) else "freetype")
shaper "harfbuzz"
# Window properties
background None
+3 -18
View File
@@ -79,9 +79,6 @@ init 1100 python:
else:
config.has_sync = None
if renpy.emscripten and PY2:
config.has_sync = None
init -1100 python in _sync:
# Do not participate in saves.
@@ -149,10 +146,7 @@ init -1100 python in _sync:
for _ in range(10000):
hashed = hashlib.sha256(hashed).digest()
if PY2:
return hashed.encode("hex")
else:
return hashed.hex()
return hashed.hex()
def key_and_hash(sync_id):
@@ -175,10 +169,7 @@ init -1100 python in _sync:
for _ in range(10000):
hashed = hashlib.sha256(hashed).digest()
if PY2:
return key, hashed.encode("hex")
else:
return key, hashed.hex()
return key, hashed.hex()
def verbose_error(e):
renpy.display.log.write("Sync error:")
@@ -260,8 +251,6 @@ init -1100 python in _sync:
sd = renpy.config.save_directory
if sd:
if PY2:
sd = sd.encode("utf-8")
zf.writestr("save_directory", sd)
persistent = location.path("persistent")[1]
@@ -388,11 +377,7 @@ init -1100 python in _sync:
zi = zf.getinfo(fn)
if PY2:
epoch = datetime.datetime.utcfromtimestamp(0)
timestamp = (datetime.datetime(*zi.date_time) - epoch).total_seconds()
else:
timestamp = datetime.datetime(*zi.date_time).timestamp()
timestamp = datetime.datetime(*zi.date_time).timestamp()
data = zf.read(fn)
+17 -5
View File
@@ -117,8 +117,20 @@ screen _image_attributes():
for l in renpy.display.scenelists.ordered_layers:
$ hidden = renpy.get_hidden_tags(l)
$ showing = renpy.get_showing_tags(l, sort=True)
python:
hidden = renpy.get_hidden_tags(l)
showing = renpy.get_showing_tags(l, sort=True)
transforms = renpy.layer_has_transforms(l)
transform_list = [ ]
if transforms.at_list:
transform_list.append("show layer")
if transforms.camera:
transform_list.append("camera")
if transforms.config_layer_transforms:
transform_list.append("config.layer_transforms")
if not hidden and not showing:
continue
@@ -128,6 +140,9 @@ screen _image_attributes():
text _("Layer [l]:")
if transform_list:
text _(" (transforms: [', '.join(transform_list)])")
for name in hidden:
$ attributes = " ".join(renpy.get_attributes(name, layer=l))
text _(" [name!q] [attributes!q] (hidden)")
@@ -499,10 +514,7 @@ label _filename_list:
for fn in files:
fn = os.path.join(dirname, fn)
if PY2:
fn = fn.encode("utf-8", "replace")
print(fn, file=f)
print(fn)
renpy.launch_editor([ FILES_TXT ], transient=1)
+52 -33
View File
@@ -96,7 +96,7 @@ init label _errorhandling:
adjust_spacing True
emoji_font "TwemojiCOLRv0.ttf"
prefer_emoji True
shaper ("harfbuzz" if (not PY2) else "freetype")
shaper "harfbuzz"
# Window properties
background None
@@ -509,50 +509,69 @@ init python:
return "\n".join(rv)
def __format_parse_errors(s):
def __format_parse_errors(errors: list[str]):
"""
Takes list of ParseError messages and transforms it
to string of sane length to be used as child of viewport.
"""
import re
rv = ""
rv = []
rv_len = 0
for error in errors:
lines = error.split("\n")
lines = s.split("\n")
lines = __limit_lines(lines)
len_lines = len(lines)
# Do not count first line in total to include those as much as
# possible.
rv.append(re.sub(
r'(File "(.*)", line (\d+))',
r'{a=edit:\3:\2}\1{/a}',
lines.pop(0)))
ln = 0
error_rv = []
error_len = 0
for line in lines:
# In case we have single insanely lengthy single error.
error_len += len(line)
if error_len > 65536:
error_rv.clear()
break
while ln < len_lines:
line = lines[ln]
ln += 1
if set(line.strip()) == {"^"}:
idx = line.find("^")
count = line.count("^", idx)
if ln < len_lines and lines[ln].endswith("^"):
highlight = len(lines[ln]) - 1
ln += 1
else:
highlight = -1
prev_line = error_rv[-1]
pos = 0
line = prev_line[:idx]
for c in line:
if pos == highlight:
rv += u"{color=#c00}\u2192{/color}"
highlight = -1
if count == 1:
if len(prev_line) < idx + count:
mark = "\u2190"
else:
mark = "\u2192"
count = 0
else:
mark = prev_line[idx:idx + count]
line += "{b}{color=#a00}"
line += mark
line += "{/color}{/b}"
line += prev_line[idx + count:]
pos += 1
error_rv[-1] = line
continue
if c == "{":
rv += "{{"
else:
rv += c
error_rv.append(line)
if highlight > 0:
rv += u"{color=#c00}\u2190{/color}"
rv_len += error_len
if rv_len > 1048576:
break
rv += "\n"
rv.extend(error_rv)
rv.append("")
rv = re.sub(r'(File "(.*)", line (\d+))', r'{a=edit:\3:\2}\1{/a}', rv)
return rv
return "\n".join(rv)
class _EditFile(Action):
def __init__(self, filename, line=1):
@@ -778,7 +797,7 @@ screen _exception:
key "console" action __EnterConsole()
# The screen that is used for error handling.
screen _parse_errors:
screen _parse_errors(errors, error_fn, reload_action):
modal True
layer config.interface_layer
zorder 1090
+20 -96
View File
@@ -58,8 +58,6 @@ Right now, it does the following things:
from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
import future.standard_library
import future.utils
import builtins
import io
@@ -68,25 +66,15 @@ import operator
python_open = open
################################################################################
# Alias the Python 3 standard library.
future.standard_library.install_aliases()
################################################################################
# Determine if this is Python2.
PY2 = future.utils.PY2
PY2 = False
################################################################################
# Make open mimic Python 3.
if PY2:
open = io.open
import re
re.Pattern = re._pattern_type # type:ignore
else:
open = builtins.open
open = builtins.open
def compat_open(*args, **kwargs):
@@ -97,121 +85,57 @@ def compat_open(*args, **kwargs):
################################################################################
# Make strict use surrogateescape error handling on PY2.
# Codecs.
import codecs
strict_error = codecs.lookup_error("strict")
codecs.register_error("python_strict", strict_error)
if PY2:
surrogateescape_error = codecs.lookup_error("surrogateescape")
codecs.register_error("strict", surrogateescape_error)
import renpy
renpy.update_path()
################################################################################
# String (text and binary) types and functions.
basestring = future.utils.string_types
basestring = (str, bytes)
pystr = str
unicode = future.utils.text_type
unicode = str
str = builtins.str
# This tries to help pylance get the types right.
str = builtins.str; globals()["str"] = future.utils.text_type
def bord(s): # type: (bytes) -> int
return s[0]
def bchr(i): # type: (int) -> bytes
return bytes([i])
bord = future.utils.bord
if PY2:
bchr = chr # type: ignore
else:
def bchr(i): # type: (int) -> bytes
return bytes([i])
tobytes = future.utils.tobytes
from future.builtins import chr
################################################################################
# Dictionary views.
# The try block solves a chicken-and-egg problem when dictviews is not
# compiled yet, as part of the Ren'Py build process.
def add_attribute(obj, name, value):
pass
if PY2:
try:
from renpy.compat.dictviews import add_attribute
except ImportError:
print("Could not import renpy.compat.dictviews.", file=sys.stderr)
def tobytes(s):
if isinstance(s, bytes):
return s
else:
if isinstance(s, str):
return s.encode('latin-1')
else:
return bytes(s)
chr = builtins.chr
################################################################################
# Range.
if PY2:
range = xrange # type: ignore
else:
range = builtins.range
range = builtins.range
################################################################################
# Round.
round = builtins.round
################################################################################
# Allow TextIOWrapper to take utf8-bytes.
if PY2:
import types
# io.TextIOWrapper._write = io.TextIOWrapper.write
def text_write(self, s):
if isinstance(s, bytes):
s = s.decode("utf-8", "surrogateescape")
return self._write(s)
add_attribute(io.TextIOWrapper, "_write", io.TextIOWrapper.write)
add_attribute(io.TextIOWrapper, "write", types.MethodType(text_write, None, io.TextIOWrapper)) # type: ignore
################################################################################
# Chance the default for subprocess.Popen.
if PY2:
import subprocess
if hasattr(subprocess, 'Popen'): # Web2 does not have subprocess.Popen
class Popen(subprocess.Popen):
def __init__(self, *args, **kwargs):
if ("stdout" not in kwargs) and ("stderr" not in kwargs) and ("stdin" not in kwargs):
kwargs.setdefault("close_fds", True)
super(Popen, self).__init__(*args, **kwargs)
subprocess.Popen = Popen
################################################################################
# Intern
if PY2:
intern_cache = {}
def intern(s):
return intern_cache.setdefault(s, s)
sys.intern = intern
################################################################################
# Export functions.
__all__ = [ "PY2", "open", "basestring", "str", "pystr", "range",
"round", "bord", "bchr", "tobytes", "chr", "unicode", ]
if PY2:
__all__ = [ bytes(i) for i in __all__ ] # type: ignore
# Generated by scripts/relative_imports.py, do not edit below this line.
if 1 == 0:
-124
View File
@@ -1,124 +0,0 @@
# Copyright 2004-2024 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from cpython.object cimport PyObject
import types
# Import the Python internals. Here be dragons, but at least Python 2.7 isn't
# going to be changing.
cdef extern from "Python.h":
ctypedef struct PyTypeObject
cdef void PyType_Modified(PyTypeObject *)
ctypedef struct PyCodeObject:
int co_flags
PyObject *co_filename
cdef extern from "frameobject.h":
ctypedef struct PyFrameObject:
PyFrameObject *f_back
PyCodeObject *f_code
cdef extern from "Python.h":
ctypedef struct PyThreadState:
PyFrameObject *frame
PyThreadState *PyThreadState_Get()
enum:
CO_FUTURE_DIVISION
CO_FUTURE_WITH_STATEMENT
###############################################################################
# Add attributes to built in types.
cdef class MappingProxy:
"""
This is something that Python's built-in mapping proxy can be converted
to, in order to gain access to the internal dictionary so we can make
changes to it.
"""
cdef dict dict
def add_attribute(cls, name, value):
"""
This adds an attribute to a Python built-in type.
"""
cdef MappingProxy mp = <MappingProxy> <PyObject *> (cls.__dict__)
mp.dict[name] = value
PyType_Modified(<PyTypeObject *> cls)
################################################################################
# dict
#
# For the .items(), .keys(), and .values() methods, check to see if the calling
# code was compiled with "from __future__ import division", or the equivalent.
# If it was, then invoke Python 3 semantics for these methods, which means
# returning a view into a dict. Otherwise, go with Python 2.
#
# Why division? It's the most Python 3-specific of the flags, at least for
# Ren'Py purposes.
add_attribute(dict, "_items", dict.items)
add_attribute(dict, "_keys", dict.keys)
add_attribute(dict, "_values", dict.values)
cdef bint use_view():
"""
Returns true if the methods should use view semantics, or false if
they should use legacy/list semantics.
"""
return ((PyThreadState_Get().frame.f_code.co_flags) & (CO_FUTURE_DIVISION | CO_FUTURE_WITH_STATEMENT)) == (CO_FUTURE_DIVISION | CO_FUTURE_WITH_STATEMENT)
def items(self):
if use_view():
return self.viewitems()
else:
return self._items()
def keys(self):
if use_view():
return self.viewkeys()
else:
return self._keys()
def values(self):
if use_view():
return self.viewvalues()
else:
return self._values()
add_attribute(dict, "items", types.MethodType(items, None, dict))
add_attribute(dict, "keys", types.MethodType(keys, None, dict))
add_attribute(dict, "values", types.MethodType(values, None, dict))
+191 -221
View File
@@ -27,260 +27,230 @@ import renpy
import pickle
import io
# Protocol 2 can be loaded on Python 2 and Python 3.
PROTOCOL = 2
PROTOCOL = pickle.HIGHEST_PROTOCOL
if PY2:
import cPickle # type: ignore
import functools
import datetime
import ast
def make_datetime(cls, *args, **kwargs):
"""
Makes a datetime.date, datetime.time, or datetime.datetime object
from a surrogateescaped str. This is used when unpickling a datetime
object that was first created in Python 2.
"""
if (len(args) == 1) and isinstance(args[0], str):
data = args[0].encode("utf-8", "surrogateescape")
return cls.__new__(cls, data.decode("latin-1"))
return cls.__new__(cls, *args, **kwargs)
class Unpickler(pickle.Unpickler):
date = functools.partial(make_datetime, datetime.date)
time = functools.partial(make_datetime, datetime.time)
datetime = functools.partial(make_datetime, datetime.datetime)
def find_class(self, module, name):
if module == "datetime":
if name == "date":
return self.date
elif name == "time":
return self.time
elif name == "datetime":
return self.datetime
if module == "_ast" and name in REWRITE_NODES:
return REWRITE_NODES[name]
return super().find_class(module, name)
def load(f):
up = Unpickler(f, fix_imports=True, encoding="utf-8", errors="surrogateescape")
return up.load()
def loads(s):
return load(io.BytesIO(s))
def dump(o, f, highest=False):
pickle.dump(o, f, pickle.HIGHEST_PROTOCOL if highest else PROTOCOL)
def dumps(o, highest=False):
return pickle.dumps(o, pickle.HIGHEST_PROTOCOL if highest else PROTOCOL)
# The python AST module changed significantly between python 2 and 3. Old-style
# screenlang support records raw python ast nodes into the rpyc data, making these
# impossible to load normally. This dict contains mappings of nodes that need to be
# modified to wrapper classes with a custom __setstate__ implementation that will
# cause the pickle machinery to do the right thing.
# There are some things that cannot be handled by this
# (as the type of the node to be emitted is not fixed at that point), so those
# are handled by a NodeTransformer in the ast.Module replacement.
# Note: this isn't a complete python 2 -> python 3 ast conversion, we just convert
# what is needed to still support old-style screens (Ren'py 6.17 and below)
# mapping of "classname": WrapperClass
REWRITE_NODES = {}
# NodeTransformer that runs after the ast has been instantiated in the ast.Module
# handler, and allows us to fix some more difficult issues. Currently only
# handles converting ast.Name nodes that contain True/False/None into the appropriate
# ast.Constant nodes.
class AstFixupTransformer(ast.NodeTransformer):
def visit_Name(self, node):
# in python 2 True, False, None are keywords, and get parsed as such.
if node.id == "True":
alt_node = ast.Constant(True)
elif node.id == "False":
alt_node = ast.Constant(False)
elif node.id == "None":
alt_node = ast.Constant(None)
def load(f): # type: ignore
if renpy.config.use_cpickle:
return cPickle.load(f)
else:
return pickle.load(f)
return node
def loads(s): # type: ignore
if renpy.config.use_cpickle:
return cPickle.loads(s)
else:
return pickle.loads(s)
alt_node.lineno = node.lineno
alt_node.col_offset = node.col_offset
return alt_node
def dump(o, f, highest=False):
if renpy.config.use_cpickle:
cPickle.dump(o, f, PROTOCOL)
else:
pickle.dump(o, f,PROTOCOL)
# wrapper classes. They all have __setstate__ defined to handle converting from the
# py2 class, and __reduce__ implemented to convert to the underlying py3 class
# if it gets repickled. Note that py3 ast classes will not hit the REWRITE_NODES check
# as py3 classes report to be from "ast" instead of "_ast"
class CallWrapper(ast.Call):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Call, args, attrs
def dumps(o, highest=False): # type: ignore
if renpy.config.use_cpickle:
return cPickle.dumps(o, PROTOCOL)
else:
return pickle.dumps(o, PROTOCOL)
def __setstate__(self, state):
# source info
self.lineno = state["lineno"]
self.col_offset = state["col_offset"]
else:
# contents
self.func = state["func"]
self.args = state["args"]
self.keywords = state["keywords"]
import functools
import datetime
import ast
# these gained some extra info
for keyword in self.keywords:
keyword.lineno = self.lineno
keyword.col_offset = self.col_offset
def make_datetime(cls, *args, **kwargs):
"""
Makes a datetime.date, datetime.time, or datetime.datetime object
from a surrogateescaped str. This is used when unpickling a datetime
object that was first created in Python 2.
"""
# these are no longer an extra field, they're just part of args and keywords
# as you can now supply multiple of them.
if state["starargs"]:
node = ast.Starred(value=state["starargs"], ctx=ast.Load())
node.lineno = self.lineno
node.col_offset = self.col_offset
self.args.append(node)
if (len(args) == 1) and isinstance(args[0], str):
data = args[0].encode("utf-8", "surrogateescape")
return cls.__new__(cls, data.decode("latin-1"))
if state["kwargs"]:
node = ast.keyword(None, state["kwargs"])
node.lineno = self.lineno
node.col_offset = self.col_offset
self.keywords.append(node)
return cls.__new__(cls, *args, **kwargs)
REWRITE_NODES["Call"] = CallWrapper
class Unpickler(pickle.Unpickler):
date = functools.partial(make_datetime, datetime.date)
time = functools.partial(make_datetime, datetime.time)
datetime = functools.partial(make_datetime, datetime.datetime)
class NumWrapper(ast.Constant):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Constant, args, attrs
def find_class(self, module, name):
if module == "datetime":
if name == "date":
return self.date
elif name == "time":
return self.time
elif name == "datetime":
return self.datetime
def __setstate__(self, state):
# source info
self.lineno = state["lineno"]
self.col_offset = state["col_offset"]
if module == "_ast" and name in REWRITE_NODES:
return REWRITE_NODES[name]
# contents
self.value = state["n"]
return super().find_class(module, name)
REWRITE_NODES["Num"] = NumWrapper
def load(f):
up = Unpickler(f, fix_imports=True, encoding="utf-8", errors="surrogateescape")
return up.load()
class StrWrapper(ast.Constant):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Constant, args, attrs
def loads(s):
return load(io.BytesIO(s))
def __setstate__(self, state):
# source info
self.lineno = state["lineno"]
self.col_offset = state["col_offset"]
def dump(o, f, highest=False):
pickle.dump(o, f, pickle.HIGHEST_PROTOCOL if highest else PROTOCOL)
# contents
self.value = state["s"]
def dumps(o, highest=False):
return pickle.dumps(o, pickle.HIGHEST_PROTOCOL if highest else PROTOCOL)
REWRITE_NODES["Str"] = StrWrapper
# The python AST module changed significantly between python 2 and 3. Old-style
# screenlang support records raw python ast nodes into the rpyc data, making these
# impossible to load normally. This dict contains mappings of nodes that need to be
# modified to wrapper classes with a custom __setstate__ implementation that will
# cause the pickle machinery to do the right thing.
# There are some things that cannot be handled by this
# (as the type of the node to be emitted is not fixed at that point), so those
# are handled by a NodeTransformer in the ast.Module replacement.
# Note: this isn't a complete python 2 -> python 3 ast conversion, we just convert
# what is needed to still support old-style screens (Ren'py 6.17 and below)
# mapping of "classname": WrapperClass
REWRITE_NODES = {}
class ModuleWrapper(ast.Module):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Module, args, attrs
# NodeTransformer that runs after the ast has been instantiated in the ast.Module
# handler, and allows us to fix some more difficult issues. Currently only
# handles converting ast.Name nodes that contain True/False/None into the appropriate
# ast.Constant nodes.
class AstFixupTransformer(ast.NodeTransformer):
def visit_Name(self, node):
# in python 2 True, False, None are keywords, and get parsed as such.
if node.id == "True":
alt_node = ast.Constant(True)
def __setstate__(self, state):
# contents
self.body = state["body"]
self.type_ignores = []
elif node.id == "False":
alt_node = ast.Constant(False)
# this is the root node, so now is a good moment do do some transforms we couldn't
# do earlier because we weren't sure of the node type to be created.
elif node.id == "None":
alt_node = ast.Constant(None)
transformer = AstFixupTransformer()
transformer.visit(self)
else:
return node
REWRITE_NODES["Module"] = ModuleWrapper
alt_node.lineno = node.lineno
alt_node.col_offset = node.col_offset
return alt_node
class ReprWrapper(ast.Call):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Call, args, attrs
# wrapper classes. They all have __setstate__ defined to handle converting from the
# py2 class, and __reduce__ implemented to convert to the underlying py3 class
# if it gets repickled. Note that py3 ast classes will not hit the REWRITE_NODES check
# as py3 classes report to be from "ast" instead of "_ast"
class CallWrapper(ast.Call):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Call, args, attrs
def __setstate__(self, state):
# we need to transform `thing` into repr(thing)
# source info
self.lineno = state["lineno"]
self.col_offset = state["col_offset"]
def __setstate__(self, state):
# source info
self.lineno = state["lineno"]
self.col_offset = state["col_offset"]
# contents
self.func = ast.Name("repr", ast.Load(), lineno=self.lineno, col_offset=self.col_offset)
self.args = [state["value"]]
self.keywords = []
# contents
self.func = state["func"]
self.args = state["args"]
self.keywords = state["keywords"]
REWRITE_NODES["Repr"] = ReprWrapper
# these gained some extra info
for keyword in self.keywords:
keyword.lineno = self.lineno
keyword.col_offset = self.col_offset
class ArgumentsWrapper(ast.arguments):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.arguments, args, attrs
# these are no longer an extra field, they're just part of args and keywords
# as you can now supply multiple of them.
if state["starargs"]:
node = ast.Starred(value=state["starargs"], ctx=ast.Load())
node.lineno = self.lineno
node.col_offset = self.col_offset
self.args.append(node)
def __setstate__(self, state):
# source info: this node doesn't get source info
if state["kwargs"]:
node = ast.keyword(None, state["kwargs"])
node.lineno = self.lineno
node.col_offset = self.col_offset
self.keywords.append(node)
def make_arg(name):
# python 2 just uses bare ast.Name nodes as arguments
# technically it also can support more complex tuple destructuring
# expressions in here, but python 3 just doesn't support that,
# and there's really no good way of exactly handling that crazyness.
assert isinstance(name, ast.Name)
return ast.arg(name.id, lineno=name.lineno, col_offset=name.col_offset)
REWRITE_NODES["Call"] = CallWrapper
# contents. source doesn't record lineno/col_offset for vararg/kwarg
self.posonlyargs = []
self.args = [make_arg(i) for i in state["args"]]
self.vararg = ast.arg(state["vararg"], lineno=1, col_offset=0)
self.kwonlyargs = []
self.kw_defaults = []
self.kwarg = ast.arg(state["kwarg"], lineno=1, col_offset=0)
self.defaults = state["defaults"]
class NumWrapper(ast.Constant):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Constant, args, attrs
REWRITE_NODES["arguments"] = ArgumentsWrapper
def __setstate__(self, state):
# source info
self.lineno = state["lineno"]
self.col_offset = state["col_offset"]
class ParamWrapper(ast.Load):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Load, args, attrs
# contents
self.value = state["n"]
REWRITE_NODES["Num"] = NumWrapper
class StrWrapper(ast.Constant):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Constant, args, attrs
def __setstate__(self, state):
# source info
self.lineno = state["lineno"]
self.col_offset = state["col_offset"]
# contents
self.value = state["s"]
REWRITE_NODES["Str"] = StrWrapper
class ModuleWrapper(ast.Module):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Module, args, attrs
def __setstate__(self, state):
# contents
self.body = state["body"]
self.type_ignores = []
# this is the root node, so now is a good moment do do some transforms we couldn't
# do earlier because we weren't sure of the node type to be created.
transformer = AstFixupTransformer()
transformer.visit(self)
REWRITE_NODES["Module"] = ModuleWrapper
class ReprWrapper(ast.Call):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Call, args, attrs
def __setstate__(self, state):
# we need to transform `thing` into repr(thing)
# source info
self.lineno = state["lineno"]
self.col_offset = state["col_offset"]
# contents
self.func = ast.Name("repr", ast.Load(), lineno=self.lineno, col_offset=self.col_offset)
self.args = [state["value"]]
self.keywords = []
REWRITE_NODES["Repr"] = ReprWrapper
class ArgumentsWrapper(ast.arguments):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.arguments, args, attrs
def __setstate__(self, state):
# source info: this node doesn't get source info
def make_arg(name):
# python 2 just uses bare ast.Name nodes as arguments
# technically it also can support more complex tuple destructuring
# expressions in here, but python 3 just doesn't support that,
# and there's really no good way of exactly handling that crazyness.
assert isinstance(name, ast.Name)
return ast.arg(name.id, lineno=name.lineno, col_offset=name.col_offset)
# contents. source doesn't record lineno/col_offset for vararg/kwarg
self.posonlyargs = []
self.args = [make_arg(i) for i in state["args"]]
self.vararg = ast.arg(state["vararg"], lineno=1, col_offset=0)
self.kwonlyargs = []
self.kw_defaults = []
self.kwarg = ast.arg(state["kwarg"], lineno=1, col_offset=0)
self.defaults = state["defaults"]
REWRITE_NODES["arguments"] = ArgumentsWrapper
class ParamWrapper(ast.Load):
def __reduce__(self):
_, args, attrs = super().__reduce__()
return ast.Load, args, attrs
REWRITE_NODES["Param"] = ParamWrapper
REWRITE_NODES["Param"] = ParamWrapper
+4
View File
@@ -1503,7 +1503,11 @@ interface_layer = "screens"
# Should Transform crop be limited to the width and height of the image being cropped?
limit_transform_crop = False
# Marking labels, images and audio in replays as seen is not allowed.
no_replay_seen = False
# Should we use pre-8.4 show expression behavior?
old_show_expression = False
del os
del collections
+1 -4
View File
@@ -36,10 +36,7 @@ import builtins
import io
import time
if PY2:
real_open = io.open
else:
real_open = builtins.open
real_open = builtins.open
report = True
+1 -1
View File
@@ -3002,7 +3002,7 @@ class WebInput(renpy.display.displayable.Displayable):
@staticmethod
def post_find_focusable():
if PY2 or not renpy.emscripten:
if not renpy.emscripten:
return
if WebInput.active is None:
+5 -19
View File
@@ -1201,24 +1201,15 @@ class Interface(object):
renpy.config.renderer = renderer
if renpy.android or renpy.ios or renpy.emscripten:
renderers = [ "gles" ]
renderers = [ "gles2" ]
elif renpy.windows:
renderers = [ "gl", "angle", "gles" ]
renderers = [ "gl2", "angle2", "gles2" ]
else:
renderers = [ "gl", "gles" ]
gl2_renderers = [ ]
for i in [ "gl", "angle", "gles" ]:
if i in renderers:
gl2_renderers.append(i + "2")
renderers = gl2_renderers + renderers
renderers = [ "gl2", "gles2" ]
# Prevent a performance warning if the renderer
# is taken from old persistent data.
if renderer not in gl2_renderers and (renpy.macintosh or renpy.android or renpy.config.gl2):
if renderer not in renderers:
renderer = "auto"
# Software renderer is the last hope for PC .
@@ -1254,10 +1245,6 @@ class Interface(object):
return False
make_draw("gl", "renpy.gl.gldraw", "GLDraw", "gl")
make_draw("angle", "renpy.gl.gldraw", "GLDraw", "angle")
make_draw("gles", "renpy.gl.gldraw", "GLDraw", "gles")
make_draw("gl2", "renpy.gl2.gl2draw", "GL2Draw", "gl2")
make_draw("angle2", "renpy.gl2.gl2draw", "GL2Draw", "angle2")
make_draw("gles2", "renpy.gl2.gl2draw", "GL2Draw", "gles2")
@@ -1364,7 +1351,6 @@ class Interface(object):
raise Exception("Could not set video mode.")
renpy.session["renderer"] = draw.info["renderer"]
renpy.game.persistent._gl2 = renpy.config.gl2
if renpy.android:
android.init()
@@ -2841,7 +2827,7 @@ class Interface(object):
renpy.game.preferences.fullscreen = False
if renpy.game.preferences.fullscreen != self.fullscreen:
if (not PY2) and renpy.emscripten:
if renpy.emscripten:
if renpy.game.preferences.fullscreen:
emscripten.run_script("setFullscreen(true);")
else:
+7 -7
View File
@@ -22,7 +22,7 @@
# This file contains code to handle GUI-based error reporting.
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 renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # *
import os
@@ -60,8 +60,6 @@ def init_display():
The minimum amount of code required to init the display.
"""
renpy.config.gl2 = getattr(renpy.game.persistent, "_gl2", True)
# Ensure we have correctly-typed preferences.
renpy.game.preferences.check()
@@ -145,7 +143,7 @@ def report_exception(short, full, traceback_fn):
reload_action=reload_action,
ignore_action=ignore_action,
traceback_fn=traceback_fn,
)
)
renpy.display.im.ignored_images |= renpy.display.im.images_to_ignore
@@ -164,7 +162,7 @@ def report_exception(short, full, traceback_fn):
raise
def report_parse_errors(errors, error_fn):
def report_parse_errors(errors: list[str], error_fn: str) -> bool:
"""
Reports an exception to the user. Returns True if the exception should
be raised by the normal reporting mechanisms. Otherwise, should raise
@@ -176,7 +174,7 @@ def report_parse_errors(errors, error_fn):
error_dump()
if renpy.game.args.command != "run": # @UndefinedVariable
if renpy.game.args.command != "run":
return True
if "RENPY_SIMPLE_EXCEPTIONS" in os.environ:
@@ -201,7 +199,7 @@ def report_parse_errors(errors, error_fn):
reload_action=reload_action,
errors=errors,
error_fn=error_fn,
)
)
except renpy.game.CONTROL_EXCEPTIONS:
raise
@@ -210,3 +208,5 @@ def report_parse_errors(errors, error_fn):
renpy.display.log.write("While handling exception:")
renpy.display.log.exception()
raise
return False
+4
View File
@@ -407,10 +407,14 @@ class ImageReference(renpy.display.displayable.Displayable):
if not name:
error("Image '%s' not found." % ' '.join(self.name))
if renpy.game.lint:
renpy.lint.report("References image '%s', which does not exist.", ' '.join(self.name))
return False
if name and (self._args.name == name):
error("Image '{}' refers to itself.".format(' '.join(name)))
if renpy.game.lint:
renpy.lint.report("Image '%s' refers to itself.", ' '.join(name))
return False
args += self._args.args
+1 -5
View File
@@ -31,11 +31,7 @@ import renpy
from renpy.display.render import render, Render
if PY2:
def compute_raw(value, room):
return renpy.display.core.absolute.compute_raw(value, room)
else:
compute_raw = renpy.display.core.absolute.compute_raw
compute_raw = renpy.display.core.absolute.compute_raw
def xyminimums(style, width, height):
+6 -8
View File
@@ -248,14 +248,12 @@ def progress(kind, done, total):
if done == total:
return
if not PY2:
if progress_kind != kind:
print()
print(kind)
progress_kind = kind
sys.stdout.flush()
if progress_kind != kind:
print()
print(kind)
progress_kind = kind
sys.stdout.flush()
emscripten.run_script(r"""progress(%d, %d);""" % (done, total))
emscripten.run_script(r"""progress(%d, %d);""" % (done, total))
emscripten.sleep(0)
+35
View File
@@ -979,6 +979,41 @@ class SceneLists(renpy.object.Object):
sl.sort(key=lambda sle : sle.zorder)
class _HasTransforms:
"""
This is returned from layer_has_transforms.
"""
at_list : bool
camera: bool
config_layer_transforms: bool
def layer_has_transforms(layer):
"""
:doc: undocumented
Used to determine if a layer has transforms associated with it. Returns
an object with the following attributes:
at_list
True unless the at_list is empty.
camera
True unless the camera list is empty.
config_layer_transforms
True unless the config.layer_transforms list is empty.
"""
rv = _HasTransforms()
rv.at_list = bool(scene_lists().layer_at_list[layer][1])
rv.camera = bool(scene_lists().camera_list[layer][1])
rv.config_layer_transforms = bool(renpy.config.layer_transforms.get(layer, [ ]))
return rv
def scene_lists(index=-1):
"""
Returns either the current scenelists object, or the one for the
+8 -8
View File
@@ -606,6 +606,14 @@ class TransformState(renpy.object.Object):
xycenter = property(get_pos, set_xycenter)
def simplify_position(v):
if isinstance(v, tuple):
return tuple(simplify_position(i) for i in v)
elif isinstance(v, position):
return v.simplify()
else:
return v
class Proxy(object):
"""
This class proxies a field from the transform to its state.
@@ -616,14 +624,6 @@ class Proxy(object):
def __get__(self, instance, owner):
def simplify_position(v):
if isinstance(v, tuple):
return tuple(simplify_position(i) for i in v)
elif isinstance(v, position):
return v.simplify()
else:
return v
return simplify_position(getattr(instance.state, self.name))
def __set__(self, instance, value):
+2 -6
View File
@@ -249,12 +249,8 @@ def dump(error):
if filename != "-":
new = filename + ".new"
if PY2:
with open(new, "wb") as f:
json.dump(result, f) # type: ignore
else:
with open(new, "w") as f:
json.dump(result, f)
with open(new, "w") as f:
json.dump(result, f)
if os.path.exists(filename):
os.unlink(filename)
+2 -4
View File
@@ -34,10 +34,8 @@ import renpy
Color = renpy.color.Color
color = renpy.color.Color
if PY2:
from collections import Iterable # type: ignore
else:
from collections.abc import Iterable
from collections.abc import Iterable
def lookup_displayable_prefix(d):
+3 -2
View File
@@ -654,8 +654,9 @@ class Context(renpy.object.Object):
renpy.store._kwargs = e.kwargs
if self.seen:
renpy.game.persistent._seen_ever[self.current] = True # type: ignore
renpy.game.seen_session[self.current] = True
if renpy.exports.is_seen_allowed():
renpy.game.persistent._seen_ever[self.current] = True # type: ignore
renpy.game.seen_session[self.current] = True
renpy.plog(2, " end {} ({}:{})", type_node_name, this_node.filename, this_node.linenumber)
+6 -1
View File
@@ -40,7 +40,7 @@ import pygame_sdl2
try:
import emscripten
except ImportError:
pass
emscripten = None
import renpy.audio.sound as sound
import renpy.audio.music as music
@@ -113,6 +113,10 @@ from renpy.display.predict import (
screen as predict_screen,
)
from renpy.display.scenelists import (
layer_has_transforms,
)
from renpy.display.screen import (
ScreenProfile as profile_screen,
current_screen,
@@ -484,6 +488,7 @@ from renpy.exports.persistentexports import (
mark_image_unseen,
save_persistent,
is_seen,
is_seen_allowed,
)
from renpy.exports.platformexports import (
+2 -4
View File
@@ -492,7 +492,8 @@ def show(name, at_list=[ ], layer=None, what=None, zorder=None, tag=None, behind
img._unique()
# Update the list of images we have ever seen.
renpy.game.persistent._seen_images[tuple(str(i) for i in name)] = True
if renpy.exports.is_seen_allowed():
renpy.game.persistent._seen_images[tuple(str(i) for i in name)] = True
if tag and munge_name:
name = (tag,) + name[1:]
@@ -1278,9 +1279,6 @@ def get_refresh_rate(precision=5):
to 1 disables this.
"""
if PY2:
precision = float(precision)
info = renpy.display.get_info()
rv = info.refresh_rate # type: ignore
rv = round(rv / precision) * precision
+3 -10
View File
@@ -34,18 +34,11 @@ except ImportError:
import renpy
if PY2:
from urllib import urlencode as _urlencode # type: ignore
else:
from urllib.parse import urlencode as _urlencode
from urllib.parse import urlencode as _urlencode
try:
if PY2:
import urllib
proxies = urllib.getproxies() # type: ignore
else:
import urllib.request
proxies = urllib.request.getproxies()
import urllib.request
proxies = urllib.request.getproxies()
except Exception as e:
proxies = {}
+1 -1
View File
@@ -122,7 +122,7 @@ def input(prompt, default='', allow=None, exclude='{}', length=None, with_none=N
fixed = renpy.exports.in_fixed_rollback()
if (not PY2) and renpy.emscripten and renpy.config.web_input and not fixed:
if renpy.emscripten and renpy.config.web_input and not fixed:
return web_input(prompt, default, allow, exclude, length, bool(mask))
renpy.exports.mode('input')
+1 -1
View File
@@ -164,7 +164,7 @@ def fsencode(s, force=False): # type: (str, bool) -> str
Converts s from unicode to the filesystem encoding.
"""
if (not PY2) and (not force):
if not force:
return s
if not isinstance(s, str):
+10
View File
@@ -161,3 +161,13 @@ def is_seen(ever=True):
"""
return renpy.game.context().seen_current(ever)
def is_seen_allowed():
"""
:doc: other
Returns False only if in a replay and no seen events allowed by setting :var:`config.no_replay_seen` to True.
"""
return not (renpy.store._in_replay and renpy.config.no_replay_seen)
-17
View File
@@ -1,17 +0,0 @@
from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # *
import renpy
renpy.update_path()
# Generated by scripts/relative_imports.py, do not edit below this line.
if 1 == 0:
from . import gldraw
from . import glenviron_shader
from . import glfunctions
from . import glrtt_copy
from . import glrtt_fbo
from . import gltexture
-96
View File
@@ -1,96 +0,0 @@
# Copyright 2004-2014 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 renpy.display.matrix cimport Matrix, Matrix2D
cimport renpy.display.render as render
cdef class Environ
cdef class GLDraw:
cdef bint gles
cdef bint angle
cdef bint did_init
cdef bint did_texture_test
cdef Environ environ
cdef public object rtt
cdef object window
cdef tuple virtual_size
cdef public tuple physical_size
cdef public tuple drawable_size
cdef public tuple virtual_box
cdef public tuple physical_box
cdef object texture_cache
cdef double last_redraw_time
cdef double redraw_period
cdef public dict info
cdef object old_fullscreen
cdef public object fullscreen_surface
cdef object display_info
cdef tuple clip_cache
cdef bint fast_dissolve
cdef tuple default_clip
cdef bint did_render_to_texture
cdef float dpi_scale
cdef object ready_texture_queue
cdef public tuple clip_rtt_box
# The number of drawable pixels per virtual pixel.
cdef public object draw_per_virt
# Matrices that transform drawable to virtual, and vice versa.
cdef public Matrix virt_to_draw
cdef public Matrix draw_to_virt
cdef public int fast_redraw_frames
cpdef set_clip(GLDraw self, tuple clip)
cpdef int draw_render_textures(
GLDraw self,
object what,
bint non_aligned) except 1
cpdef int draw_transformed(
GLDraw self,
object what,
tuple clip,
double xo,
double yo,
double alpha,
double over,
Matrix reverse,
bint nearest,
bint subpixel) except 1
cdef class Environ:
cdef void blit(self)
cdef void blend(self, double fraction)
cdef void imageblend(self, double fraction, int ramp)
cdef void set_vertex(self, float *vertices)
cdef void set_texture(self, int unit, float *coords)
cdef void set_color(self, float r, float g, float b, float a)
cdef void set_clip(self, tuple clip_box, GLDraw draw)
cdef void unset_clip(self, GLDraw draw)
cdef void ortho(self, double left, double right, double bottom, double top, double near, double far)
cdef void viewport(self, int x, int y, int width, int height)
-1417
View File
File diff suppressed because it is too large Load Diff
-671
View File
@@ -1,671 +0,0 @@
#cython: profile=False
# Copyright 2004-2024 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
from renpy.uguu.gl cimport *
from renpy.gl.gldraw cimport *
cdef int round(double d):
return <int> (d + .5)
GLES_PORTABILITY = """\
#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
# else
precision mediump float;
# endif
#endif
"""
VERTEX_SHADER1 = GLES_PORTABILITY + """\
uniform mat4 Projection;
attribute vec4 Vertex;
attribute vec2 VertexTexCoord0;
varying vec2 TexCoord0;
varying vec2 pos;
void main() {
TexCoord0 = VertexTexCoord0;
pos = Vertex.xy;
gl_Position = Projection * Vertex;
}
"""
VERTEX_SHADER2 = GLES_PORTABILITY + """\
uniform mat4 Projection;
attribute vec4 Vertex;
attribute vec2 VertexTexCoord0;
attribute vec2 VertexTexCoord1;
varying vec2 TexCoord0;
varying vec2 TexCoord1;
varying vec2 pos;
void main() {
TexCoord0 = VertexTexCoord0;
TexCoord1 = VertexTexCoord1;
pos = Vertex.xy;
gl_Position = Projection * Vertex;
}
"""
VERTEX_SHADER3 = GLES_PORTABILITY + """\
uniform mat4 Projection;
attribute vec4 Vertex;
attribute vec2 VertexTexCoord0;
attribute vec2 VertexTexCoord1;
attribute vec2 VertexTexCoord2;
varying vec2 TexCoord0;
varying vec2 TexCoord1;
varying vec2 TexCoord2;
varying vec2 pos;
void main() {
TexCoord0 = VertexTexCoord0;
TexCoord1 = VertexTexCoord1;
TexCoord2 = VertexTexCoord2;
pos = Vertex.xy;
gl_Position = Projection * Vertex;
}
"""
BLIT_SHADER = GLES_PORTABILITY + """\
uniform vec4 Color;
uniform sampler2D tex0;
varying vec2 TexCoord0;
varying vec2 pos;
uniform vec2 clip0;
uniform vec2 clip1;
void main()
{
vec4 color0 = texture2D(tex0, TexCoord0.st);
gl_FragColor = color0 * Color;
}
"""
BLIT_CLIP_SHADER = GLES_PORTABILITY + """\
uniform vec4 Color;
uniform sampler2D tex0;
varying vec2 TexCoord0;
varying vec2 pos;
uniform vec2 clip0;
uniform vec2 clip1;
void main()
{
if (pos.x < clip0.x || pos.y < clip0.y || pos.x >= clip1.x || pos.y >= clip1.y) {
discard;
}
vec4 color0 = texture2D(tex0, TexCoord0.st);
gl_FragColor = color0 * Color;
}
"""
BLEND_SHADER = GLES_PORTABILITY + """\
uniform vec4 Color;
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform float done;
varying vec2 TexCoord0;
varying vec2 TexCoord1;
varying vec2 pos;
uniform vec2 clip0;
uniform vec2 clip1;
void main()
{
vec4 color0 = texture2D(tex0, TexCoord0.st);
vec4 color1 = texture2D(tex1, TexCoord1.st);
gl_FragColor = mix(color0, color1, done) * Color;
}
"""
BLEND_CLIP_SHADER = GLES_PORTABILITY + """\
uniform vec4 Color;
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform float done;
varying vec2 TexCoord0;
varying vec2 TexCoord1;
varying vec2 pos;
uniform vec2 clip0;
uniform vec2 clip1;
void main()
{
if (pos.x < clip0.x || pos.y < clip0.y || pos.x >= clip1.x || pos.y >= clip1.y) {
discard;
}
vec4 color0 = texture2D(tex0, TexCoord0.st);
vec4 color1 = texture2D(tex1, TexCoord1.st);
gl_FragColor = mix(color0, color1, done) * Color;
}
"""
IMAGEBLEND_SHADER = GLES_PORTABILITY + """\
uniform vec4 Color;
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform float offset;
uniform float multiplier;
varying vec2 TexCoord0;
varying vec2 TexCoord1;
varying vec2 TexCoord2;
varying vec2 pos;
uniform vec2 clip0;
uniform vec2 clip1;
void main()
{
vec4 color0 = texture2D(tex0, TexCoord0.st);
vec4 color1 = texture2D(tex1, TexCoord1.st);
vec4 color2 = texture2D(tex2, TexCoord2.st);
float a = clamp((color0.a + offset) * multiplier, 0.0, 1.0);
gl_FragColor = mix(color1, color2, a) * Color;
}
"""
IMAGEBLEND_CLIP_SHADER = GLES_PORTABILITY + """\
uniform vec4 Color;
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform float offset;
uniform float multiplier;
varying vec2 TexCoord0;
varying vec2 TexCoord1;
varying vec2 TexCoord2;
varying vec2 pos;
uniform vec2 clip0;
uniform vec2 clip1;
void main()
{
if (pos.x < clip0.x || pos.y < clip0.y || pos.x >= clip1.x || pos.y >= clip1.y) {
discard;
}
vec4 color0 = texture2D(tex0, TexCoord0.st);
vec4 color1 = texture2D(tex1, TexCoord1.st);
vec4 color2 = texture2D(tex2, TexCoord2.st);
float a = clamp((color0.a + offset) * multiplier, 0.0, 1.0);
gl_FragColor = mix(color1, color2, a) * Color;
}
"""
def check_status(shader, handle, type):
"""
Checks the status of a shader or program. If it fails, then an
exception is raised.
"""
cdef GLint status = 0
cdef GLint log_length = 0
if shader:
glGetShaderiv(handle, type, &status)
else:
glGetProgramiv(handle, type, &status)
if status == 1: # 0 for problems.
return
if shader:
glGetShaderiv(handle, GL_INFO_LOG_LENGTH, &log_length)
else:
glGetProgramiv(handle, GL_INFO_LOG_LENGTH, &log_length)
log = ' ' * log_length
if shader:
glGetShaderInfoLog(handle, log_length, &log_length, <char *> log)
else:
glGetProgramInfoLog(handle, log_length, &log_length, <char *> log)
raise Exception("Shader error: %s" % log)
def compile_shader(kind, source):
"""
Allocates and compiles a shader.
"""
source = source.encode("utf-8")
cdef char *sourceptr = <char *> source
cdef int lensource = len(source)
handle = glCreateShader(kind)
glShaderSource(handle, 1, <const GLchar * const *> &sourceptr, &lensource)
glCompileShader(handle)
check_status(True, handle, GL_COMPILE_STATUS)
return handle
def compile_program(vertex, fragment):
"""
Compiles a pair of shaders into a program.
"""
vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex)
fragment_shader = compile_shader(GL_FRAGMENT_SHADER, fragment)
program = glCreateProgram()
glAttachShader(program, vertex_shader)
glAttachShader(program, fragment_shader)
glLinkProgram(program)
check_status(False, program, GL_LINK_STATUS)
glUseProgram(program)
glDeleteShader(vertex_shader)
glDeleteShader(fragment_shader)
return program
cdef class Program(object):
"""
Encapsulates a program.
"""
cdef GLuint program
# Attributes.
cdef GLint Vertex
cdef GLint VertexTexCoord0
cdef GLint VertexTexCoord1
cdef GLint VertexTexCoord2
# Uniforms.
cdef GLint Projection
cdef GLint Color
cdef GLint tex0
cdef GLint tex1
cdef GLint tex2
cdef GLint clip0
cdef GLint clip1
cdef GLint offset
cdef GLint multiplier
cdef GLint done
def __init__(self, vertex, fragment):
self.program = compile_program(vertex, fragment)
self.Vertex = glGetAttribLocation(self.program, "Vertex")
self.VertexTexCoord0 = glGetAttribLocation(self.program, "VertexTexCoord0")
self.VertexTexCoord1 = glGetAttribLocation(self.program, "VertexTexCoord1")
self.VertexTexCoord2 = glGetAttribLocation(self.program, "VertexTexCoord2")
self.Projection = glGetUniformLocation(self.program, "Projection")
self.tex0 = glGetUniformLocation(self.program, "tex0")
self.tex1 = glGetUniformLocation(self.program, "tex1")
self.tex2 = glGetUniformLocation(self.program, "tex2")
self.offset = glGetUniformLocation(self.program, "offset")
self.multiplier = glGetUniformLocation(self.program, "multiplier")
self.done = glGetUniformLocation(self.program, "done")
self.Color = glGetUniformLocation(self.program, "Color")
self.clip0 = glGetUniformLocation(self.program, "clip0")
self.clip1 = glGetUniformLocation(self.program, "clip1")
def disable_attribs(self):
# Disable the vertex attributes used by this program.
if self.Vertex != -1:
glDisableVertexAttribArray(self.Vertex)
if self.VertexTexCoord0 != -1:
glDisableVertexAttribArray(self.VertexTexCoord0)
if self.VertexTexCoord1 != -1:
glDisableVertexAttribArray(self.VertexTexCoord1)
if self.VertexTexCoord2 != -1:
glDisableVertexAttribArray(self.VertexTexCoord2)
def delete(self):
glDeleteProgram(self.program)
cdef class ShaderEnviron(Environ):
"""
This is an environment that uses shaders.
"""
cdef Program program
cdef float projection[16]
cdef Program blit_program
cdef Program blend_program
cdef Program imageblend_program
cdef Program blit_clip_program
cdef Program blend_clip_program
cdef Program imageblend_clip_program
cdef bint clipping
cdef double clip_x0
cdef double clip_y0
cdef double clip_x1
cdef double clip_y1
cdef int viewport_x
cdef int viewport_y
cdef int viewport_w
cdef int viewport_h
def init(self):
self.blit_program = Program(VERTEX_SHADER1, BLIT_SHADER)
self.blit_clip_program = Program(VERTEX_SHADER1, BLIT_CLIP_SHADER)
self.blend_program = Program(VERTEX_SHADER2, BLEND_SHADER)
self.blend_clip_program = Program(VERTEX_SHADER2, BLEND_CLIP_SHADER)
self.imageblend_program = Program(VERTEX_SHADER3, IMAGEBLEND_SHADER)
self.imageblend_clip_program = Program(VERTEX_SHADER3, IMAGEBLEND_CLIP_SHADER)
# The current program.
self.program = None
def deinit(self):
"""
Called before changing the GL context.
"""
if self.program is not None:
self.program.disable_attribs()
self.program = None
self.blit_program.delete()
self.blend_program.delete()
self.imageblend_program.delete()
def activate(self, Program program):
if self.program is not None:
self.program.disable_attribs()
self.program = program
glUseProgram(program.program)
glUniformMatrix4fv(program.Projection, 1, GL_FALSE, self.projection)
if self.clipping:
glUniform2f(program.clip0, self.clip_x0 - .01, self.clip_y0 - .01)
glUniform2f(program.clip1, self.clip_x1 + .01, self.clip_y1 + .01)
cdef void blit(self):
if self.clipping:
program = self.blit_clip_program
else:
program = self.blit_program
if self.program is not program:
self.activate(program)
glUniform1i(program.tex0, 0)
cdef void blend(self, double fraction):
if self.clipping:
program = self.blend_clip_program
else:
program = self.blend_program
if self.program is not program:
self.activate(program)
glUniform1i(program.tex0, 0)
glUniform1i(program.tex1, 1)
glUniform1f(program.done, fraction)
cdef void imageblend(self, double fraction, int ramp):
if self.clipping:
program = self.imageblend_clip_program
else:
program = self.imageblend_program
if self.program is not program:
self.activate(program)
glUniform1i(program.tex0, 0)
glUniform1i(program.tex1, 1)
glUniform1i(program.tex2, 2)
# Prevent a DBZ if the user gives us a 0 ramp.
if ramp < 1:
ramp = 1
# Compute the offset to apply to the alpha.
start = -1.0
end = ramp / 256.0
offset = start + ( end - start) * fraction
# Setup the multiplier and the offset.
glUniform1f(program.multiplier, 256.0 / ramp)
glUniform1f(program.offset, offset)
cdef void set_vertex(self, float *vertices):
glEnableVertexAttribArray(self.program.Vertex)
glVertexAttribPointer(self.program.Vertex, 2, GL_FLOAT, GL_FALSE, 0, <GLubyte *> vertices)
cdef void set_texture(self, int unit, float *coords):
cdef tex
if unit == 0:
tex = self.program.VertexTexCoord0
elif unit == 1:
tex = self.program.VertexTexCoord1
elif unit == 2:
tex = self.program.VertexTexCoord2
else:
return
if tex < 0:
return
if coords != NULL:
glVertexAttribPointer(tex, 2, GL_FLOAT, GL_FALSE, 0, <GLubyte *> coords)
glEnableVertexAttribArray(tex)
else:
glDisableVertexAttribArray(tex)
cdef void set_color(self, float r, float g, float b, float a):
glUniform4f(self.program.Color, r, g, b, a)
cdef void ortho(self, double left, double right, double bottom, double top, double near, double far):
self.projection[ 0] = 2 / (right - left)
self.projection[ 4] = 0
self.projection[ 8] = 0
self.projection[12] = -(right + left) / (right - left)
self.projection[ 1] = 0
self.projection[ 5] = 2 / (top - bottom)
self.projection[ 9] = 0
self.projection[13] = -(top + bottom) / (top - bottom)
self.projection[ 2] = 0
self.projection[ 6] = 0
self.projection[10] = -2 / (far - near)
self.projection[14] = -(far + near) / (far - near)
self.projection[ 3] = 0
self.projection[ 7] = 0
self.projection[11] = 0
self.projection[15] = 1
if self.program is not None:
self.program.disable_attribs()
self.program = None
cdef void project(self, x, y, z, double *rv_x, double *rv_y, double *rv_z):
"""
Given a point, projects it using the projection.
"""
rv_x[0] = x * self.projection[0] + y * self.projection[4] + z * self.projection[8] + self.projection[12]
rv_y[0] = x * self.projection[1] + y * self.projection[5] + z * self.projection[9] + self.projection[13]
rv_z[0] = x * self.projection[2] + y * self.projection[6] + z * self.projection[10] + self.projection[14]
cdef void set_clip(self, tuple clip_box, GLDraw draw):
cdef double minx, miny, maxx, maxy, z
cdef double vwidth, vheight
cdef double px, py, pw, ph
cdef int cx, cy, cw, ch
cdef int psw, psh
if clip_box == draw.default_clip:
self.unset_clip(draw)
return
minx, miny, maxx, maxy = clip_box
psw, psh = draw.physical_size
# The clipping box.
self.clipping = True
self.clip_x0 = minx
self.clip_y0 = miny
self.clip_x1 = maxx
self.clip_y1 = maxy
# Set the scissor rectangle to be slightly larger than the
# clipping box. This ensures everything that needs to be drawn
# is drawn, and we don't spend a lot of time shading clipped
# fragments.
if draw.clip_rtt_box is None:
z = 0
# Project to normalized coordinates.
self.project(minx, miny, z, &minx, &miny, &z)
self.project(maxx, maxy, z, &maxx, &maxy, &z)
# Convert to window coordinates.
minx = (minx + 1) * self.viewport_w / 2 + self.viewport_x
maxx = (maxx + 1) * self.viewport_w / 2 + self.viewport_x
miny = (miny + 1) * self.viewport_h / 2 + self.viewport_y
maxy = (maxy + 1) * self.viewport_h / 2 + self.viewport_y
# Increase the bounding box, to ensure every relevant pixel is
# in it. The shader will take care of enforcing the actual box.
minx -= 1
maxx += 1
miny += 1
maxy -= 1
if minx < 0:
minx = 0
if miny < 0:
miny = 0
glEnable(GL_SCISSOR_TEST)
glScissor(<GLint> round(minx), <GLint> round(maxy), <GLint> round(maxx - minx), <GLsizei> round(miny - maxy))
else:
cx, cy, cw, ch = draw.clip_rtt_box
glEnable(GL_SCISSOR_TEST)
glScissor(<GLint> round(minx - cx), <GLint> round(miny - cy), <GLint> round(maxx - minx), <GLint> round(maxy - miny))
if self.program is not None:
self.program.disable_attribs()
self.program = None
cdef void unset_clip(self, GLDraw draw):
glDisable(GL_SCISSOR_TEST)
self.clipping = False
self.clip_x0 = 0
self.clip_y0 = 0
self.clip_x1 = 65535
self.clip_y1 = 65535
if self.program is not None:
self.program.disable_attribs()
self.program = None
cdef void viewport(self, int x, int y, int width, int height):
glViewport(x, y, width, height)
self.viewport_x = x
self.viewport_y = y
self.viewport_w = width
self.viewport_h = height
-59
View File
@@ -1,59 +0,0 @@
# This file has been automatically generated by module/uguu/generate_required_functions.py,
# please do not edit it by hand.
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 # *
required_functions = [
"glActiveTexture",
"glAttachShader",
"glBindFramebuffer",
"glBindTexture",
"glBlendFunc",
"glClear",
"glClearColor",
"glCompileShader",
"glCopyTexImage2D",
"glCopyTexSubImage2D",
"glCreateProgram",
"glCreateShader",
"glDeleteFramebuffers",
"glDeleteProgram",
"glDeleteShader",
"glDeleteTextures",
"glDisable",
"glDisableVertexAttribArray",
"glDrawArrays",
"glEnable",
"glEnableVertexAttribArray",
"glFinish",
"glFramebufferTexture2D",
"glGenFramebuffers",
"glGenTextures",
"glGetAttribLocation",
"glGetError",
"glGetIntegerv",
"glGetProgramInfoLog",
"glGetProgramiv",
"glGetShaderInfoLog",
"glGetShaderiv",
"glGetString",
"glGetUniformLocation",
"glLinkProgram",
"glReadPixels",
"glScissor",
"glShaderSource",
"glTexImage2D",
"glTexParameteri",
"glTexSubImage2D",
"glUniform1f",
"glUniform1i",
"glUniform2f",
"glUniform4f",
"glUniformMatrix4fv",
"glUseProgram",
"glVertexAttribPointer",
"glViewport",
]
-72
View File
@@ -1,72 +0,0 @@
#@PydevCodeAnalysisIgnore
#cython: profile=False
# Copyright 2004-2024 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
from renpy.uguu.gl cimport *
from renpy.gl.gldraw cimport *
from renpy.gl.gldraw import Rtt
class CopyRtt(Rtt):
"""
This class uses texture copying to implement Render-to-texture.
"""
def init(self):
return
def deinit(self):
return
def render(self, Environ environ, texture, x, y, w, h, draw_func):
"""
This function is called to trigger a rendering to a texture.
`x`, `y`, `w`, and `h` specify the location and dimensions of
the sub-image to render to the texture. `draw_func` is called
to render the texture.
"""
environ.viewport(0, 0, w, h)
environ.ortho(x, x + w, y, y + h, -1, 1)
draw_func(x, y, w, h)
glBindTexture(GL_TEXTURE_2D, texture)
glCopyTexSubImage2D(
GL_TEXTURE_2D,
0,
0,
0,
0,
0,
w,
h)
def end(self):
"""
This is called when a Render-to-texture session ends.
"""
def get_size_limit(self, dimension):
return dimension
-115
View File
@@ -1,115 +0,0 @@
#@PydevCodeAnalysisIgnore
#cython: profile=False
# Copyright 2004-2024 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
from renpy.uguu.gl cimport *
from renpy.gl.gldraw cimport *
from renpy.gl.gldraw import Rtt
import renpy
# The framebuffer object we use.
cdef GLuint fbo
# The root framebuffer.
cdef GLint root_fbo
# The renderbuffer object we use.
cdef GLuint texture
class FboRtt(Rtt):
"""
This class uses texture copying to implement Render-to-texture.
"""
def init(self):
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &root_fbo);
renpy.display.log.write("Root FBO is: %d", root_fbo)
glGenFramebuffers(1, &fbo)
glGenTextures(1, &texture)
cdef int i
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &i)
self.size_limit = min(i, 2048)
renpy.display.log.write("FBO Maximum Texture Size: %d", self.size_limit)
glBindTexture(GL_TEXTURE_2D, texture)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.size_limit, self.size_limit, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL)
glBindFramebuffer(GL_FRAMEBUFFER, fbo)
glFramebufferTexture2D(
GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
texture,
0)
glBindFramebuffer(GL_FRAMEBUFFER, root_fbo)
def deinit(self):
"""
Called before changing the GL context.
"""
glBindFramebuffer(GL_FRAMEBUFFER, root_fbo)
glDeleteFramebuffers(1, &fbo)
glDeleteTextures(1, &texture)
def begin(self):
"""
This function should be called when a Render-to-texture
session begins. It's responsible for setting the GPU to
RTT mode.
"""
def render(self, Environ environ, texture, x, y, w, h, draw_func):
"""
This function is called to trigger a rendering to a texture.
`x`, `y`, `w`, and `h` specify the location and dimensions of
the sub-image to render to the texture. `draw_func` is called
to render the texture.
"""
try:
glBindFramebuffer(GL_FRAMEBUFFER, fbo)
environ.viewport(0, 0, w, h)
environ.ortho(x, x + w, y, y + h, -1, 1)
draw_func(x, y, w, h)
glBindTexture(GL_TEXTURE_2D, texture)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL)
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, w, h, 0)
finally:
glBindFramebuffer(GL_FRAMEBUFFER, root_fbo)
def get_size_limit(self, dimension):
return self.size_limit
-104
View File
@@ -1,104 +0,0 @@
# Copyright 2004-2014 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 renpy.display.matrix cimport Matrix
cimport renpy.display.render as render
from renpy.gl.gldraw cimport Environ
cdef class TextureCore:
cdef public int width
cdef public int height
cdef public int generation
cdef public unsigned int number
cdef unsigned int format
cdef double xmul
cdef double xadd
cdef double ymul
cdef double yadd
cdef object premult
cdef tuple premult_size
cdef int premult_left
cdef int premult_right
cdef int premult_top
cdef int premult_bottom
cdef bint nearest
cdef public list free_list
cdef void make_ready(TextureCore)
cdef void make_nearest(TextureCore)
cdef void make_linear(TextureCore)
cpdef int allocate(TextureCore)
cdef public object debug
cdef class TextureGrid:
cdef object __weakref__
cdef public int width
cdef public int height
cdef list rows
cdef list columns
cdef list tiles # list of lists.
cdef public TextureGrid half_cache
cpdef void make_ready(self, bint nearest)
cdef public object debug
cdef public bint ready
cpdef blit(
TextureGrid tg,
double sx,
double sy,
Matrix transform,
double alpha,
double over,
Environ environ,
bint nearest)
cpdef blend(
TextureGrid tg0,
TextureGrid tg1,
double sx,
double sy,
Matrix transform,
double alpha,
double over,
double fraction,
Environ environ,
bint nearest)
cpdef imageblend(
TextureGrid tg0,
TextureGrid tg1,
TextureGrid tg2,
double sx,
double sy,
Matrix transform,
double alpha,
double over,
double fraction,
int ramp,
Environ environ,
bint nearest)
File diff suppressed because it is too large Load Diff
+1 -5
View File
@@ -63,8 +63,7 @@ def onetime_init():
if os.path.exists(fn):
dll = fn
if not PY2:
dll = dll.encode("utf-8")
dll = dll.encode("utf-8")
if not renpy.gl2.live2dmodel.load(dll): # type: ignore
raise Exception("Could not load Live2D. {} was not found.".format(dll))
@@ -88,9 +87,6 @@ def init():
if live2dmodel is None:
raise Exception("Live2D has not been built.")
if not renpy.config.gl2:
raise Exception("Live2D requires that config.gl2 be True.")
if renpy.emscripten:
raise Exception("Live2D is not supported the web platform.")
+195 -144
View File
@@ -23,114 +23,141 @@
from __future__ import division, absolute_import, with_statement, print_function, unicode_literals # type: ignore
from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # *
from typing import Any
import codecs
import re
import sys
import os
import time
import linecache
import contextlib
import renpy
from renpy.lexersupport import match_logical_word
# The filename that's in the line text cache.
line_text_filename = ""
# The content of the line text cache.
line_text_cache = [ ]
_python_updatecache = linecache.updatecache
def get_line_text(filename, lineno):
def _updatecache(filename: str, module_globals: dict[str, Any] | None = None) -> list[str]:
"""
Gets the text of a line, in a best-effort way, for debugging purposes. May
return just a newline, if the line doesn't exist.
Monkeypatch internal linecache.updatecache to work around RenPy python code
being in different files even in the same namespace. Assume filename is one
of '_ren.py', '.rpy' or '.rpym'.
This is needed because a lot of Python modules (namely traceback) assume
that the compiled python code is always comes from the python file and uses
linecache to get the source code, which tries to use PEP302 loader, which
for RenPy python code points to renpy.minstore.
"""
global line_text_filename
global line_text_cache
import linecache
full_filename = renpy.exports.unelide_filename(filename)
if full_filename != line_text_filename:
line_text_filename = full_filename
if filename.endswith(("_ren.py", ".rpy", ".rpym")):
# Assume filename also relative to basedir or renpy_base.
full_fn = renpy.lexer.unelide_filename(filename)
# If we can't find absolute path that way, assume we are
# in build, so we can't show source.
if not (os.path.isabs(full_fn) and os.path.exists(full_fn)):
linecache.cache.pop(filename, None)
return []
try:
with open(full_filename, "rb") as f:
with open(full_fn, "rb") as f:
data = f.read().decode("utf-8", "python_strict")
if full_filename.endswith("_ren.py"):
if full_fn.endswith("_ren.py"):
data = ren_py_to_rpy(data, None)
data += "\n\n"
line_text_cache = data.split("\n")
lines = data.split("\n")
if lines and lines[0].startswith("\ufeff"):
lines[0] = lines[0][1:]
lines = [line.removesuffix("\r") + "\n" for line in lines]
stat = os.stat(full_fn)
linecache.cache[filename] = stat.st_size, stat.st_mtime, lines, full_fn
return lines
except Exception:
line_text_cache = [ ]
return []
if lineno <= len(line_text_cache):
return line_text_cache[lineno - 1] + "\n"
else:
return "\n"
return _python_updatecache(filename, module_globals)
class ParseError(Exception):
linecache.updatecache = _updatecache
def __init__(self, filename, number, msg, line=None, pos=None, first=False):
message = u"File \"%s\", line %d: %s" % (unicode_filename(filename), number, msg)
if line:
if isinstance(line, list):
line = "".join(line)
class ParseError(SyntaxError):
"""
Special exception type for syntax errors in Ren'Py.
This exception includes syntax errors of Python code, converted to
appropriate report style, and Ren'Py own syntax errors in user script.
"""
lines = line.split('\n')
_message: str | None = None
if len(lines) > 1:
open_string = None
i = 0
def __init__(
self,
message: str,
filename: str,
lineno: int,
offset: int | None = None,
text: str | None = None,
end_lineno: int | None = None,
end_offset: int | None = None,
):
super().__init__(message, (
unicode_filename(filename),
lineno, offset,
text,
end_lineno, end_offset))
while i < len(lines[0]):
c = lines[0][i]
@property
def message(self) -> str:
"""
Fully formatted message of the error close to the result of
`traceback.print_exception_only`.
"""
if self._message is None:
message = f'File "{self.filename}", line {self.lineno}: {self.msg}'
if self.text is not None:
# Neither Python nor this class does not support multiline syntax error code.
# Just strip the first line of provided code.
text = self.text.split("\n")[0]
if c == "\\":
i += 1
elif c == open_string:
open_string = None
elif open_string:
pass
elif c == '`' or c == '\'' or c == '"':
open_string = c
# Remove ending escape chars, so we can render it.
text = text.rstrip()
i += 1
# And also replace any escape chars at the start with an indent.
message += f'\n {text.lstrip()}'
if open_string:
message += "\n(Perhaps you left out a %s at the end of the first line.)" % open_string
if self.offset is not None:
offset = self.offset
for l in lines:
message += "\n " + l
if pos is not None:
if pos <= len(l):
message += "\n " + " " * pos + "^"
pos = None
# Fallback to single caret for cases end_offset is before offset.
if self.end_offset is None or self.end_offset <= offset:
end_offset = offset + 1
else:
pos -= len(l)
end_offset = self.end_offset
if first:
break
left_spaces = len(text) - len(text.lstrip())
offset -= left_spaces
end_offset -= left_spaces
self.message = message
if offset >= 1:
caret_space = ' ' * (offset - 1)
carets = '^' * (end_offset - offset)
message += f"\n {caret_space}{carets}"
Exception.__init__(self, message)
for note in getattr(self, "__notes__", ()):
message += f"\n{note}"
def __unicode__(self):
return self.message
self._message = message
return self._message
def defer(self, queue):
renpy.parser.deferred_parse_errors[queue].append(self.message)
@@ -356,7 +383,8 @@ def list_logical_lines(filename, filedata=None, linenumber=1, add_lines=False):
c = data[pos]
if c == u'\t':
raise ParseError(filename, number, "Tab characters are not allowed in Ren'Py scripts.")
raise ParseError("Tab characters are not allowed in Ren'Py scripts.",
filename, number)
if c == u'\n' and not parendepth:
@@ -496,14 +524,80 @@ def list_logical_lines(filename, filedata=None, linenumber=1, add_lines=False):
pos = end
if (pos - startpos) > 65536:
raise ParseError(filename, start_number, "Overly long logical line. (Check strings and parenthesis.)", line=line, first=True)
err = ParseError(
"Overly long logical line.",
filename, start_number,
text="".join(line))
err.add_note("Check strings and parenthesis.")
raise err
if line:
raise ParseError(filename, start_number, "is not terminated with a newline. (Check strings and parenthesis.)", line=line, first=True)
err = ParseError("is not terminated with a newline.",
filename, start_number,
text="".join(line))
err.add_note("Check strings and parenthesis.")
raise err
return rv
def depth_split(l):
"""
Returns the length of the line's prefix, and the rest of the line.
"""
depth = 0
index = 0
while True:
if l[index] == ' ':
depth += 1
index += 1
continue
break
return depth, l[index:]
# i, min_depth -> block, new_i
def gll_core(lines, i, min_depth):
"""
Recursively groups lines into blocks.
Given the line
"""
rv = []
depth = None
while i < len(lines):
filename, number, text = lines[i]
line_depth, rest = depth_split(text)
# This catches a block exit.
if line_depth < min_depth:
break
if depth is None:
depth = line_depth
if depth != line_depth:
raise ParseError(
"Indentation mismatch.",
filename, number, text=text)
# Advance to the next line.
i += 1
# Try parsing a block associated with this line.
block, i = gll_core(lines, i, depth + 1)
rv.append((filename, number, rest, block))
return rv, i
def group_logical_lines(lines):
"""
This takes as input the list of logical line triples output from
@@ -513,67 +607,16 @@ def group_logical_lines(lines):
no block is associated with this line.)
"""
# Returns the depth of a line, and the rest of the line.
def depth_split(l):
depth = 0
index = 0
while True:
if l[index] == ' ':
depth += 1
index += 1
continue
# if l[index] == '\t':
# index += 1
# depth = depth + 8 - (depth % 8)
# continue
break
return depth, l[index:]
# i, min_depth -> block, new_i
def gll_core(i, min_depth):
rv = []
depth = None
while i < len(lines):
filename, number, text = lines[i]
line_depth, rest = depth_split(text)
# This catches a block exit.
if line_depth < min_depth:
break
if depth is None:
depth = line_depth
if depth != line_depth:
raise ParseError(filename, number, "Indentation mismatch.")
# Advance to the next line.
i += 1
# Try parsing a block associated with this line.
block, i = gll_core(i, depth + 1)
rv.append((filename, number, rest, block))
return rv, i
if lines:
filename, number, text = lines[0]
if depth_split(text)[0] != 0:
raise ParseError(filename, number, "Unexpected indentation at start of file.")
raise ParseError(
"Unexpected indentation at start of file.",
filename, number, text=text)
return gll_core(0, 0)[0]
return gll_core(lines, 0, 0)[0]
# A list of keywords which should not be parsed as names, because
@@ -820,7 +863,12 @@ class Lexer(object):
if (self.line == -1) and self.block:
self.filename, self.number, self.text, self.subblock = self.block[0]
raise ParseError(self.filename, self.number, msg, self.text, self.pos)
raise ParseError(
msg,
self.filename,
self.number,
self.pos,
self.text)
def deferred_error(self, queue, msg):
"""
@@ -834,7 +882,10 @@ class Lexer(object):
if (self.line == -1) and self.block:
self.filename, self.number, self.text, self.subblock = self.block[0]
ParseError(self.filename, self.number, msg, self.text, self.pos).defer(queue)
ParseError(
msg, self.filename,
self.number, self.pos + 1,
self.text).defer(queue)
def eol(self):
"""
@@ -1486,6 +1537,20 @@ class Lexer(object):
self.pos = len(self.text)
return self.text[pos:].strip()
def _process_python_block(self, block, indent, rv, line_holder):
for _fn, ln, text, subblock in block:
while line_holder.line < ln:
rv.append(indent + '\n')
line_holder.line += 1
linetext = indent + text + '\n'
rv.append(linetext)
line_holder.line += linetext.count('\n')
self._process_python_block(subblock, indent + ' ', rv, line_holder)
def python_block(self):
"""
Returns the subblock of this code, and subblocks of that
@@ -1495,25 +1560,10 @@ class Lexer(object):
rv = [ ]
o = LineNumberHolder()
o.line = self.number
line_holder = LineNumberHolder()
line_holder.line = self.number
def process(block, indent):
for _fn, ln, text, subblock in block:
while o.line < ln:
rv.append(indent + '\n')
o.line += 1
linetext = indent + text + '\n'
rv.append(linetext)
o.line += linetext.count('\n')
process(subblock, indent + ' ')
process(self.subblock, '')
self._process_python_block(self.subblock, '', rv, line_holder)
return ''.join(rv)
def arguments(self):
@@ -1581,7 +1631,7 @@ class Lexer(object):
return sp
def ren_py_to_rpy(text, filename):
def ren_py_to_rpy(text: str, filename: str | None) -> str:
"""
Transforms an _ren.py file into the equivalent .rpy file. This should retain line numbers.
@@ -1659,8 +1709,9 @@ def ren_py_to_rpy(text, filename):
raise Exception('In {!r}, there are no """renpy blocks, so every line is ignored.'.format(filename))
if state == RENPY:
raise Exception('In {!r}, there is a """renpy block at line {} that is not terminated by """.'.format(filename,
open_linenumber))
raise Exception(
'In {!r}, there is a """renpy block at line {} that is not terminated by """.'.format(
filename, open_linenumber))
rv = "\n".join(result)
+7 -3
View File
@@ -36,6 +36,8 @@ import io
import unicodedata
import time
from importlib.util import spec_from_loader
from pygame_sdl2.rwobject import RWopsIO
from renpy.compat.pickle import loads
@@ -833,14 +835,16 @@ class RenpyImporter(object):
return None
def find_module(self, fullname, path=None):
def find_spec(self, fullname, path, target=None):
if path is not None:
for i in path:
if self.translate(fullname, i):
return RenpyImporter(i)
return spec_from_loader(name=fullname, loader=RenpyImporter(i), origin=path)
if self.translate(fullname):
return self
return spec_from_loader(name=fullname, loader=self, origin=path)
def load_module(self, fullname, mode="full"):
"""
+2 -8
View File
@@ -82,16 +82,10 @@ def save_dump(roots, log):
elif isinstance(o, types.MethodType):
if PY2:
o_repr = "<method {0}.{1}>".format(o.__self__.__class__.__name__, o.__func__.__name__) # type: ignore
else:
o_repr = "<method {0}.{1}>".format(o.__self__.__class__.__name__, o.__name__)
o_repr = "<method {0}.{1}>".format(o.__self__.__class__.__name__, o.__name__)
elif isinstance(o, types.FunctionType):
if PY2:
name = o.__name__
else:
name = o.__qualname__ or o.__name__
name = o.__qualname__ or o.__name__
o_repr = o.__module__ + '.' + name
+1 -4
View File
@@ -121,10 +121,7 @@ def cycle_finder(o, name):
o_repr = "<" + o.__class__.__name__ + ">"
elif isinstance(o, types.MethodType):
if PY2:
o_repr = "<method {0}.{1}>".format(o.__self__.__class__.__name__, o.__func__.__name__) # type: ignore
else:
o_repr = "<method {0}.{1}>".format(o.__self__.__class__.__name__, o.__name__)
o_repr = "<method {0}.{1}>".format(o.__self__.__class__.__name__, o.__name__)
elif isinstance(o, object):
o_repr = "<{0}>".format(type(o).__name__)
-3
View File
@@ -201,6 +201,3 @@ __all__ = [
'ui',
'unicode',
]
if PY2:
__all__ = [ bytes(i) for i in __all__ ] # type: ignore
+2 -6
View File
@@ -26,12 +26,8 @@ from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, r
# Allow pickling NoneType.
if PY2:
import __builtin__ # type: ignore
__builtin__.NoneType = type(None)
else:
import builtins
builtins.NoneType = type(None) # type: ignore
import builtins
builtins.NoneType = type(None) # type: ignore
class Object(object):
+15 -22
View File
@@ -24,7 +24,6 @@ from __future__ import division, absolute_import, with_statement, print_function
from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # *
from itertools import chain as _chain
import collections
import renpy
@@ -43,8 +42,7 @@ class Parameter(object):
empty = None
def __init__(self, name, kind, default=empty):
# default should only be passed by keyword, todo when PY3-only
def __init__(self, name, kind, *, default=empty):
self.name = name
self.kind = kind
self.default = default
@@ -90,10 +88,9 @@ class ValuedParameter(Parameter):
class empty: pass # singleton, should be picklable
def __init__(self, name, kind, default=empty):
# default should only be passed by keyword, todo when PY3-only
def __init__(self, name, kind, *, default=empty):
# this method is redefined in order to change default's default value
super(ValuedParameter, self).__init__(name, kind, default)
super(ValuedParameter, self).__init__(name, kind, default=default)
def default_value(self, *args, **kwargs):
return self.default
@@ -123,8 +120,7 @@ class Signature(object):
if parameters is None:
self.parameters = {}
else:
# when in PY3-only, turn this into MappingProxyType o dict
self.parameters = collections.OrderedDict((param.name, param) for param in parameters)
self.parameters = {param.name: param for param in parameters}
@staticmethod
def legacy_params(parameters, positional, extrapos, extrakw, last_posonly=None, first_kwonly=None):
@@ -241,8 +237,6 @@ class Signature(object):
- applies the defaults automatically (and lazily, as per the above)
"""
# when in PY3-only, uncomment the "from None" in the raise statements
if not renpy.config.developer:
ignore_errors = True
@@ -250,7 +244,7 @@ class Signature(object):
def _raise(exct, msg, *argz, **kwargz):
if not ignore_errors:
raise exct(msg.format(*argz, **kwargz)) # from None
raise exct(msg.format(*argz, **kwargz)) from None
# code mostly taken from stdlib's inspect.Signature._bind
arguments = {}
@@ -304,7 +298,7 @@ class Signature(object):
argtype = ''
msg = 'missing a required{argtype} argument: {arg!r}'
msg = msg.format(arg=param.name, argtype=argtype)
raise TypeError(msg) # from None
raise TypeError(msg) from None
else:
# We have a positional argument to process
try:
@@ -362,7 +356,7 @@ class Signature(object):
if (not (partial or ignore_errors) and param.kind != param.VAR_POSITIONAL and
param.default is param.empty):
raise TypeError('missing a required argument: {arg!r}'. \
format(arg=param_name)) # from None
format(arg=param_name)) from None
else:
if param.kind == param.POSITIONAL_ONLY:
@@ -458,8 +452,8 @@ def apply_arguments(parameters, args, kwargs, ignore_errors=False):
class ArgumentInfo(renpy.object.Object):
__version__ = 1
starred_indexes = set()
doublestarred_indexes = set()
starred_indexes = frozenset()
doublestarred_indexes = frozenset()
def after_upgrade(self, version):
if version < 1:
@@ -468,16 +462,13 @@ class ArgumentInfo(renpy.object.Object):
extrakw = self.extrakw # type: ignore
length = len(arguments) + bool(extrapos) + bool(extrakw)
if extrapos:
self.starred_indexes = { length - 1 }
self.starred_indexes = { length - 1 - bool(extrakw) }
arguments.append((None, extrapos))
if extrakw:
self.doublestarred_indexes = { length - 1 }
arguments.append((None, extrakw))
if extrapos and extrakw:
self.starred_indexes = { length - 2 }
def __init__(self, arguments, starred_indexes=None, doublestarred_indexes=None):
# A list of (keyword, expression) pairs.
@@ -485,10 +476,12 @@ class ArgumentInfo(renpy.object.Object):
self.arguments = arguments
# Indexes of arguments to be considered as * unpacking
self.starred_indexes = starred_indexes or set()
if starred_indexes is not None:
self.starred_indexes = starred_indexes
# Indexes of arguments to be considered as ** unpacking.
self.doublestarred_indexes = doublestarred_indexes or set()
if doublestarred_indexes is not None:
self.doublestarred_indexes = doublestarred_indexes
def evaluate(self, scope=None):
"""
@@ -541,4 +534,4 @@ class ArgumentInfo(renpy.object.Object):
EMPTY_PARAMETERS = Signature()
EMPTY_ARGUMENTS = ArgumentInfo([ ], None, None)
EMPTY_ARGUMENTS = ArgumentInfo((), None, None)
+10 -4
View File
@@ -31,7 +31,7 @@ import time
import renpy
import renpy.ast as ast
from renpy.parameter import Parameter
from renpy.parameter import EMPTY_ARGUMENTS, Parameter
from renpy.lexer import (
list_logical_lines,
@@ -43,7 +43,6 @@ from renpy.lexer import (
munge_filename,
elide_filename,
unelide_filename,
get_line_text,
SubParse,
)
@@ -475,6 +474,9 @@ def parse_arguments(l):
if not l.match(r'\('):
return None
if l.match(r'\)'):
return EMPTY_ARGUMENTS
arguments = [ ]
starred_indexes = set()
doublestarred_indexes = set()
@@ -1766,8 +1768,10 @@ def report_parse_errors():
full_text = ""
f, error_fn = renpy.error.open_error_file("errors.txt", "w")
screen_parse_errors = []
with f:
f.write("\ufeff") # BOM
f.write("\ufeff") # BOM
print("I'm sorry, but errors were detected in your script. Please correct the", file=f)
print("errors listed below, and try again.", file=f)
@@ -1784,6 +1788,8 @@ def report_parse_errors():
print("", file=f)
print(i, file=f)
screen_parse_errors.append(i)
try:
print("")
print(i)
@@ -1794,7 +1800,7 @@ def report_parse_errors():
print("Ren'Py Version:", renpy.version, file=f)
print(str(time.ctime()), file=f)
renpy.display.error.report_parse_errors(full_text, error_fn)
renpy.display.error.report_parse_errors(screen_parse_errors, error_fn)
try:
if renpy.game.args.command == "run" or renpy.game.args.errors_in_editor: # type: ignore
+1 -1
View File
@@ -233,7 +233,7 @@ def init():
disk, so that we can configure the savelocation system.
"""
if renpy.config.early_developer and not PY2:
if renpy.config.early_developer:
init_debug_pickler()
filename = os.path.join(renpy.config.savedir, "persistent.new") # type: ignore
-789
View File
@@ -1,789 +0,0 @@
# Copyright 2004-2024 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # *
import renpy # @UnusedImport
from renpy.python import py_compile
# Import the Python AST module, instead of the Ren'Py ast module.
import ast
import zlib
from renpy.compat.pickle import loads, dumps
# The set of names that should be treated as constants.
always_constants = { 'True', 'False', 'None' }
# The set of names that should be treated as pure functions.
pure_functions = {
# Python 2 builtins.
"abs", "all", "any", "apply", "bin", "bool", "bytes", "callable", "chr",
"cmp", "dict", "divmod",
"filter", "float", "frozenset",
"getattr", "globals", "hasattr", "hash", "hex", "int", "isinstance",
"len", "list", "long", "map", "max", "min", "oct", "ord", "pow",
"range", "reduce", "repr", "round", "set", "sorted",
"str", "sum", "tuple", "unichr", "unicode", "vars", "zip",
# minstore.py
"_",
"_p",
"absolute",
"position",
"__renpy__list__",
"__renpy__dict__",
"__renpy__set__",
# defaultstore.py
"ImageReference", "Image", "Frame", "Solid", "LiveComposite", "LiveCrop",
"LiveTile", "Flatten", "Null", "Window", "Viewport", "DynamicDisplayable",
"ConditionSwitch", "ShowingSwitch", "Transform", "Animation", "Movie",
"Particles", "SnowBlossom", "Text", "ParameterizedText", "FontGroup",
"Drag", "Alpha", "AlphaMask", "Position", "Pan", "Move", "Motion", "Revolve", "Zoom",
"RotoZoom", "FactorZoom", "SizeZoom", "Fade", "Dissolve", "ImageDissolve",
"AlphaDissolve", "CropMove", "PushMove", "Pixellate", "OldMoveTransition",
"MoveTransition", "MoveFactory", "MoveIn", "MoveOut", "ZoomInOut",
"RevolveInOut", "MultipleTransition", "ComposeTransition", "Pause",
"SubTransition", "ADVSpeaker", "ADVCharacter", "Speaker", "Character",
"DynamicCharacter", "Fixed", "HBox", "VBox", "Grid", "AlphaBlend", "At",
"color", "Color",
# ui.py
"ui.returns",
"ui.jumps",
"ui.jumpsoutofcontext",
"ui.callsinnewcontext",
"ui.invokesinnewcontext",
"ui.gamemenus",
# renpy.py
"renpy.version_string",
"renpy.version_only",
"renpy.version_tuple",
"renpy.version_name",
"renpy.license",
}
constants = { "config", "style" } | always_constants | pure_functions
# A set of names that should not be treated as global constants.
not_constants = set()
# The base set for the local constants.
local_constants = set()
def const(name):
"""
:doc: const
Declares a variable in the store to be constant.
A variable is constant if nothing can change its value, or any value
reached by indexing it or accessing its attributes. Variables must
remain constant out of define, init, and translate python blocks.
`name`
A string giving the name of the variable to declare constant.
"""
if name not in not_constants:
constants.add(name)
def not_const(name):
"""
:doc: const
Declares a name in the store to be not constant.
This undoes the effect of calls to :func:`renpy.const` and
:func:`renpy.pure`.
`name`
The name to declare not constant.
"""
constants.discard(name)
pure_functions.discard(name)
not_constants.add(name)
def pure(fn):
"""
:doc: const
Declares a function as pure. A pure function must always return the
same value when it is called with the same arguments, outside of
define, init, and translate python blocks.
`fn`
The name of the function to declare pure. This may either be a string
containing the name of the function, or the function itself.
If a string is passed and the function is inside the module,
this string should contain the module name with the dot.
Returns `fn`, allowing this function to be used as a decorator.
"""
name = fn
if not isinstance(name, basestring):
name = fn.__name__
module = fn.__module__
name = module + "." + name
if name.startswith("store."):
name = name[6:]
if name not in not_constants:
pure_functions.add(name)
constants.add(name)
return fn
class Control(object):
"""
Represents control flow.
`const`
True if this statement always executes.
`loop`
True if this corresponds to a loop.
`imagemap`
True if this control is in a non-constant imagemap.
"""
def __init__(self, const, loop, imagemap):
self.const = const
self.loop = loop
self.imagemap = imagemap
# Three levels of constness.
GLOBAL_CONST = 2 # Expressions that are const everywhere.
LOCAL_CONST = 1 # Expressions that are const with regard to a screen + parameters.
NOT_CONST = 0 # Expressions that are not const.
class DeltaSet(object):
def __init__(self, base, copy=None):
"""
Represents a set that stores its contents as differences from a base
set.
"""
self.base = base
if copy is not None:
self.added = set(copy.added)
self.removed = set(copy.removed)
else:
self.added = set()
self.removed = set()
self.changed = False
def add(self, v):
if v in self.removed:
self.removed.discard(v)
self.changed = True
elif v not in self.base and v not in self.added:
self.added.add(v)
self.changed = True
def discard(self, v):
if v in self.added:
self.added.discard(v)
self.changed = True
elif v in self.base and v not in self.removed:
self.removed.add(v)
self.changed = True
def __contains__(self, v):
return (v in self.added) or ((v in self.base) and (v not in self.removed))
def copy(self):
return DeltaSet(self.base, self)
def __iter__(self):
for i in self.base:
if i not in self.removed:
yield i
for i in self.added:
yield i
class Analysis(object):
"""
Represents the result of code analysis, and provides tools to perform
code analysis.
"""
def __init__(self, parent=None):
# The parent context transcludes run in, or None if there is no parent
# context.
self.parent = parent
# Analyses of children, such a screens we use.
self.children = { }
# The variables we consider to be not-constant.
self.not_constant = DeltaSet(not_constants)
# Variables we consider to be locally constant.
self.local_constant = DeltaSet(local_constants)
# Variables we consider to be globally constant.
self.global_constant = DeltaSet(always_constants)
# The functions we consider to be pure.
self.pure_functions = DeltaSet(pure_functions)
# Represents what we know about the current control.
self.control = Control(True, False, False)
# The stack of const_flow values.
self.control_stack = [ self.control ]
def get_child(self, identifier):
if identifier in self.children:
return self.children[identifier]
rv = Analysis(self)
self.children[identifier] = rv
return rv
def push_control(self, const=True, loop=False, imagemap=False):
self.control = Control(self.control.const and const, loop, self.control.imagemap or imagemap)
self.control_stack.append(self.control) # type: ignore
def pop_control(self):
rv = self.control_stack.pop()
self.control = self.control_stack[-1]
return rv
def imagemap(self):
"""
Returns NOT_CONST if we're in a non-constant imagemap.
"""
if self.control.imagemap:
return NOT_CONST
else:
return GLOBAL_CONST
def exit_loop(self):
"""
Call this to indicate the current loop is being exited by the
continue or break statements.
"""
l = list(self.control_stack)
l.reverse()
for i in l:
i.const = False
if i.loop:
break
def at_fixed_point(self):
"""
Returns True if we've reached a fixed point, where the analysis has
not changed since the last time we called this function.
"""
for i in self.children.values():
if not i.at_fixed_point():
return False
if (self.not_constant.changed or
self.global_constant.changed or
self.local_constant.changed or
self.pure_functions.changed):
self.not_constant.changed = False
self.global_constant.changed = False
self.local_constant.changed = False
self.pure_functions.changed = False
return False
return True
def mark_constant(self, name):
"""
Marks `name` as a potential local constant.
"""
if not name in self.not_constant:
self.local_constant.add(name)
self.global_constant.discard(name)
self.pure_functions.discard(name)
def mark_not_constant(self, name):
"""
Marks `name` as definitely not-constant.
"""
self.not_constant.add(name)
self.pure_functions.discard(name)
self.local_constant.discard(name)
self.global_constant.discard(name)
def is_constant(self, node):
"""
Returns true if `node` is constant for the purpose of screen
language. Node should be a python AST node.
Screen language ignores object identity for the purposes of
object equality.
"""
def check_slice(slice): # @ReservedAssignment
if isinstance(slice, ast.Index):
return check_node(slice.value) # type: ignore
elif isinstance(slice, ast.Slice):
consts = [ ]
if slice.lower:
consts.append(check_node(slice.lower))
if slice.upper:
consts.append(check_node(slice.upper))
if slice.step:
consts.append(check_node(slice.step))
if not consts:
return GLOBAL_CONST
else:
return min(consts)
return NOT_CONST
def check_name(node):
"""
Check nodes that make up a name. This returns a pair:
* The first element is True if the node is constant, and False
otherwise.
* The second element is None if the node is constant or the name is
not known, and the name otherwise.
"""
if isinstance(node, ast.Name):
const = NOT_CONST
name = node.id
elif isinstance(node, ast.Attribute):
const, name = check_name(node.value)
if name is not None:
name = name + "." + node.attr
else:
return check_node(node), None
if name in self.not_constant:
return NOT_CONST, name
elif name in self.global_constant:
return GLOBAL_CONST, name
elif name in self.local_constant:
return LOCAL_CONST, name
else:
return const, name
def check_nodes(nodes):
"""
Checks a list of nodes for constness.
"""
nodes = list(nodes)
if not nodes:
return GLOBAL_CONST
return min(check_node(i) for i in nodes)
def check_node(node):
"""
Returns true if the ast node `node` is constant.
"""
# This handles children that do not exist.
if node is None:
return GLOBAL_CONST
# PY3: see if there are new node types.
if isinstance(node, (ast.Num, ast.Str)):
return GLOBAL_CONST
elif isinstance(node, (ast.List, ast.Tuple)):
return check_nodes(node.elts)
elif isinstance(node, (ast.Attribute, ast.Name)):
return check_name(node)[0]
elif isinstance(node, ast.BoolOp):
return check_nodes(node.values)
elif isinstance(node, ast.BinOp):
return min(
check_node(node.left),
check_node(node.right),
)
elif isinstance(node, ast.UnaryOp):
return check_node(node.operand)
elif isinstance(node, ast.Call):
const, name = check_name(node.func)
# The function must have a name, and must be declared pure.
if (const != GLOBAL_CONST) or (name not in self.pure_functions):
return NOT_CONST
consts = [ ]
# Arguments and keyword arguments must be pure.
consts.append(check_nodes(node.args))
consts.append(check_nodes(i.value for i in node.keywords))
if node.starargs is not None: # type: ignore
consts.append(check_node(node.starargs)) # type: ignore
if node.kwargs is not None: # type: ignore
consts.append(check_node(node.kwargs)) # type: ignore
return min(consts)
elif isinstance(node, ast.IfExp):
return min(
check_node(node.test),
check_node(node.body),
check_node(node.orelse),
)
elif isinstance(node, ast.Dict):
return min(
check_nodes(node.keys),
check_nodes(node.values)
)
elif isinstance(node, ast.Set):
return check_nodes(node.elts)
elif isinstance(node, ast.Compare):
return min(
check_node(node.left),
check_nodes(node.comparators),
)
elif isinstance(node, ast.Repr): # type: ignore
return check_node(node.value)
elif isinstance(node, ast.Subscript):
return min(
check_node(node.value),
check_slice(node.slice),
)
return NOT_CONST
return check_node(node)
def is_constant_expr(self, expr):
"""
Compiles `expr` into an AST node, then returns the result of
self.is_constant called on that node.
"""
node, literal = ccache.ast_eval_literal(expr)
if literal:
return GLOBAL_CONST
else:
return self.is_constant(node)
def python(self, code):
"""
Performs analysis on a block of python code.
"""
nodes = ccache.ast_exec(code)
a = PyAnalysis(self)
for i in nodes:
a.visit(i)
def parameters(self, parameters):
"""
Analyzes the parameters to the screen.
"""
self.global_constant = DeltaSet(constants)
# As we have parameters, analyze with those parameters.
for name in parameters.parameters:
self.mark_not_constant(name)
class PyAnalysis(ast.NodeVisitor):
"""
This analyzes Python code to determine which variables should be
marked const, and which should be marked non-const.
"""
def __init__(self, analysis):
self.analysis = analysis
def visit_Name(self, node):
if isinstance(node.ctx, ast.AugStore):
self.analysis.mark_not_constant(node.id)
elif isinstance(node.ctx, ast.Store):
if self.analysis.control.const:
self.analysis.mark_constant(node.id)
else:
self.analysis.mark_not_constant(node.id)
def visit_Assign(self, node):
const = self.analysis.is_constant(node.value)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_AugAssign(self, node):
self.analysis.push_control(False, False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_For(self, node):
const = self.analysis.is_constant(node.iter)
self.analysis.push_control(const=const, loop=True)
old_const = self.analysis.control.const
self.generic_visit(node)
if self.analysis.control.const != old_const:
self.generic_visit(node)
self.analysis.pop_control()
def visit_While(self, node):
const = self.analysis.is_constant(node.test)
self.analysis.push_control(const=const, loop=True)
old_const = self.analysis.control.const
self.generic_visit(node)
if self.analysis.control.const != old_const:
self.generic_visit(node)
self.analysis.pop_control()
def visit_If(self, node):
const = self.analysis.is_constant(node.test)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
# The continue and break statements should be pretty rare, so if they
# occur, we mark everything later in the loop as non-const.
def visit_Break(self, node):
self.analysis.exit_loop()
def visit_Continue(self, node):
self.analysis.exit_loop()
class CompilerCache(object):
"""
Objects of this class are used to cache the compiliation of Python code.
"""
def __init__(self):
self.ast_eval_cache = { }
self.ast_exec_cache = { }
# True if we've changed the caches.
self.updated = False
# The version of this object.
self.version = 1
def ast_eval_literal(self, expr):
"""
Compiles an expression into an AST.
"""
if isinstance(expr, renpy.ast.PyExpr):
filename = expr.filename
linenumber = expr.linenumber
else:
filename = None
linenumber = None
key = (expr, filename, linenumber)
rv = self.ast_eval_cache.get(key, None)
if rv is None:
expr = py_compile(expr, 'eval', ast_node=True)
try:
ast.literal_eval(expr)
literal = True
except Exception:
literal = False
rv = (expr, literal)
self.ast_eval_cache[key] = rv
self.updated = True
new_ccache.ast_eval_cache[key] = rv
return rv
def ast_eval(self, expr):
return self.ast_eval_literal(expr)[0]
def ast_exec(self, code):
"""
Compiles a block into an AST.
"""
if isinstance(code, renpy.ast.PyExpr):
key = (code, code.filename, code.linenumber)
else:
key = (code, None, None)
rv = self.ast_exec_cache.get(key, None)
if rv is None:
rv = py_compile(code, 'exec', ast_node=True)
self.ast_exec_cache[key] = rv
self.updated = True
new_ccache.ast_exec_cache[key] = rv
return rv
ccache = CompilerCache()
new_ccache = CompilerCache()
CACHE_FILENAME = "cache/pyanalysis.rpyb"
def load_cache():
if renpy.game.args.compile: # type: ignore
return
try:
with renpy.loader.load(CACHE_FILENAME) as f:
c = loads(zlib.decompress(f.read()))
if c.version == ccache.version:
ccache.ast_eval_cache.update(c.ast_eval_cache)
ccache.ast_exec_cache.update(c.ast_exec_cache)
except Exception:
pass
def save_cache():
if not ccache.updated:
return
if renpy.macapp:
return
try:
data = zlib.compress(dumps(new_ccache, True), 3)
with open(renpy.loader.get_path(CACHE_FILENAME), "wb") as f:
f.write(data)
except Exception:
pass
__all__ = [
"always_constants",
"Analysis",
"ccache",
"CompilerCache",
"const",
"constants",
"Control",
"DeltaSet",
"GLOBAL_CONST",
"load_cache",
"LOCAL_CONST",
"local_constants",
"new_ccache",
"not_const",
"NOT_CONST",
"not_constants",
"pure",
"pure_functions",
"PyAnalysis",
"save_cache",
]
-861
View File
@@ -1,861 +0,0 @@
# Copyright 2004-2024 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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 builtins
import renpy # @UnusedImport
from renpy.python import py_compile
# Import the Python AST module, instead of the Ren'Py ast module.
import ast
import zlib
from renpy.compat.pickle import loads, dumps
# The set of names that should be treated as constants.
always_constants = { 'True', 'False', 'None' }
# The set of names that should be treated as pure functions.
pure_functions = {
# Python 3 builtins.
'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytes', 'callable',
'chr', 'complex', 'dict', 'dir', 'divmod', 'enumerate', 'filter', 'float',
'format', 'frozenset', 'getattr', 'hasattr', 'hash', 'hex', 'int',
'isinstance', 'issubclass', 'len', 'list', 'map', 'max', 'min', 'oct',
'ord', 'pow', 'range', 'repr', 'reversed', 'round', 'set', 'slice', 'sorted',
'str', 'sum', 'tuple', 'type', 'zip',
# minstore.py
"_",
"_p",
"absolute",
"position",
"__renpy__list__",
"__renpy__dict__",
"__renpy__set__",
# defaultstore.py
"ImageReference", "Image", "Frame", "Solid", "LiveComposite", "LiveCrop",
"LiveTile", "Flatten", "Null", "Window", "Viewport", "DynamicDisplayable",
"ConditionSwitch", "ShowingSwitch", "Transform", "Animation", "Movie",
"Particles", "SnowBlossom", "Text", "ParameterizedText", "FontGroup",
"Drag", "Alpha", "AlphaMask", "Position", "Pan", "Move", "Motion", "Revolve", "Zoom",
"RotoZoom", "FactorZoom", "SizeZoom", "Fade", "Dissolve", "ImageDissolve",
"AlphaDissolve", "CropMove", "PushMove", "Pixellate", "OldMoveTransition",
"MoveTransition", "MoveFactory", "MoveIn", "MoveOut", "ZoomInOut",
"RevolveInOut", "MultipleTransition", "ComposeTransition", "Pause",
"SubTransition", "ADVSpeaker", "ADVCharacter", "Speaker", "Character",
"DynamicCharacter", "Fixed", "HBox", "VBox", "Grid", "AlphaBlend", "At",
"color", "Color",
# ui.py
"ui.returns",
"ui.jumps",
"ui.jumpsoutofcontext",
"ui.callsinnewcontext",
"ui.invokesinnewcontext",
"ui.gamemenus",
# renpy.py
"renpy.version_string",
"renpy.version_only",
"renpy.version_tuple",
"renpy.version_name",
"renpy.license",
}
constants = { "config", "style" } | always_constants | pure_functions
# A set of names that should not be treated as global constants.
not_constants = set()
# The base set for the local constants.
local_constants = set()
def const(name):
"""
:doc: const
Declares a variable in the store to be constant.
A variable is constant if nothing can change its value, or any value
reached by indexing it or accessing its attributes. Variables must
remain constant out of define, init, and translate python blocks.
`name`
A string giving the name of the variable to declare constant.
"""
if name not in not_constants:
constants.add(name)
def not_const(name):
"""
:doc: const
Declares a name in the store to be not constant.
This undoes the effect of calls to :func:`renpy.const` and
:func:`renpy.pure`.
`name`
The name to declare not constant.
"""
constants.discard(name)
pure_functions.discard(name)
not_constants.add(name)
def pure(fn):
"""
:doc: const
Declares a function as pure. A pure function must always return the
same value when it is called with the same arguments, outside of
define, init, and translate python blocks.
`fn`
The name of the function to declare pure. This may either be a string
containing the name of the function, or the function itself.
If a string is passed and the function is inside a module,
this string should contain the module name with the dot.
Returns `fn`, allowing this function to be used as a decorator.
"""
name = fn
if not isinstance(name, basestring):
name = fn.__name__
module = fn.__module__
name = module + "." + name
if name.startswith("store."):
name = name[6:]
if name not in not_constants:
pure_functions.add(name)
constants.add(name)
return fn
class Control(object):
"""
Represents control flow.
`const`
True if this statement always executes.
`loop`
True if this corresponds to a loop.
`imagemap`
True if this control is in a non-constant imagemap.
"""
def __init__(self, const, loop, imagemap):
self.const = const
self.loop = loop
self.imagemap = imagemap
def __repr__(self):
return "<Control const={0} loop={1} imagemap={2}>".format(self.const, self.loop, self.imagemap)
# Three levels of constness.
# An expression is globally constant if it will evaluate to the same value
# whenever it is run.
GLOBAL_CONST = 2
# An expression is locally const if it will evaluate to the same value when
# run in the same place - the same screen, with the same parameters, the same
# statement, and the same iteration of a for loop.
LOCAL_CONST = 1
# An expression is not const if it wilk change it's value.
NOT_CONST = 0
class DeltaSet(object):
def __init__(self, base, copy=None):
"""
Represents a set that stores its contents as differences from a base
set.
"""
self.base = base
if copy is not None:
self.added = set(copy.added)
self.removed = set(copy.removed)
else:
self.added = set()
self.removed = set()
self.changed = False
def add(self, v):
if v in self.removed:
self.removed.discard(v)
self.changed = True
elif v not in self.base and v not in self.added:
self.added.add(v)
self.changed = True
def discard(self, v):
if v in self.added:
self.added.discard(v)
self.changed = True
elif v in self.base and v not in self.removed:
self.removed.add(v)
self.changed = True
def __contains__(self, v):
return (v in self.added) or ((v in self.base) and (v not in self.removed))
def copy(self):
return DeltaSet(self.base, self)
def __iter__(self):
for i in self.base:
if i not in self.removed:
yield i
for i in self.added:
yield i
class Analysis(object):
"""
Represents the result of code analysis, and provides tools to perform
code analysis.
"""
def __init__(self, parent=None):
# The parent context transcludes run in, or None if there is no parent
# context.
self.parent = parent
# Analyses of children, such a screens we use.
self.children = { }
# The variables we consider to be not-constant.
self.not_constant = DeltaSet(not_constants)
# Variables we consider to be locally constant.
self.local_constant = DeltaSet(local_constants)
# Variables we consider to be globally constant.
self.global_constant = DeltaSet(always_constants)
# The functions we consider to be pure.
self.pure_functions = DeltaSet(pure_functions)
# Represents what we know about the current control.
self.control = Control(True, False, False)
# The stack of const_flow values.
self.control_stack = [ self.control ]
def get_child(self, identifier):
if identifier in self.children:
return self.children[identifier]
rv = Analysis(self)
self.children[identifier] = rv
return rv
def push_control(self, const=True, loop=False, imagemap=False):
self.control = Control(self.control.const and const, loop, self.control.imagemap or imagemap)
self.control_stack.append(self.control) # type: ignore
def pop_control(self):
rv = self.control_stack.pop()
self.control = self.control_stack[-1]
return rv
def imagemap(self):
"""
Returns NOT_CONST if we're in a non-constant imagemap.
"""
if self.control.imagemap:
return NOT_CONST
else:
return GLOBAL_CONST
def exit_loop(self):
"""
Call this to indicate the current loop is being exited by the
continue or break statements.
"""
l = list(self.control_stack)
l.reverse()
for i in l:
i.const = False
if i.loop:
break
def at_fixed_point(self):
"""
Returns True if we've reached a fixed point, where the analysis has
not changed since the last time we called this function.
"""
for i in self.children.values():
if not i.at_fixed_point():
return False
if (self.not_constant.changed or
self.global_constant.changed or
self.local_constant.changed or
self.pure_functions.changed):
self.not_constant.changed = False
self.global_constant.changed = False
self.local_constant.changed = False
self.pure_functions.changed = False
return False
return True
def mark_constant(self, name):
"""
Marks `name` as a potential local constant.
"""
if not name in self.not_constant:
self.local_constant.add(name)
self.global_constant.discard(name)
self.pure_functions.discard(name)
def mark_not_constant(self, name):
"""
Marks `name` as definitely not-constant.
"""
self.not_constant.add(name)
self.pure_functions.discard(name)
self.local_constant.discard(name)
self.global_constant.discard(name)
def is_constant(self, node):
"""
Returns true if `node` is constant for the purpose of screen
language. Node should be a python AST node.
Screen language ignores object identity for the purposes of
object equality.
"""
def check_name(node):
"""
Check nodes that make up a name. This returns a pair:
* The first element is True if the node is constant, and False
otherwise.
* The second element is None if the node is constant or the name is
not known, and the name otherwise.
"""
if isinstance(node, ast.Name):
const = NOT_CONST
name = node.id
elif isinstance(node, ast.Attribute):
const, name = check_name(node.value)
if name is not None:
name = name + "." + node.attr
else:
return check_node(node), None
if name in self.not_constant:
return NOT_CONST, name
elif name in self.global_constant:
return GLOBAL_CONST, name
elif name in self.local_constant:
return LOCAL_CONST, name
else:
return const, name
def check_nodes(nodes):
"""
Checks a list of nodes for constness.
"""
nodes = list(nodes)
if not nodes:
return GLOBAL_CONST
return min(check_node(i) for i in nodes)
def check_node(node):
"""
When given `node`, part of a Python expression, returns how
const the expression is.
"""
# This handles children that do not exist.
if node is None:
return GLOBAL_CONST
# PY3: see if there are new node types.
if isinstance(node, ast.Constant):
return GLOBAL_CONST
elif isinstance(node, ast.BoolOp):
return check_nodes(node.values)
elif isinstance(node, ast.NamedExpr):
return check_node(node.value)
elif isinstance(node, ast.BinOp):
return min(
check_node(node.left),
check_node(node.right),
)
elif isinstance(node, ast.UnaryOp):
return check_node(node.operand)
# ast.Lambda is NOT_CONST.
elif isinstance(node, ast.IfExp):
return min(
check_node(node.test),
check_node(node.body),
check_node(node.orelse),
)
elif isinstance(node, ast.Dict):
return min(
check_nodes(node.keys),
check_nodes(node.values)
)
elif isinstance(node, ast.Set):
return check_nodes(node.elts)
# ast.ListComp is NOT_CONST.
# ast.SetComp is NOT_CONST.
# ast.DictComp is NOT_CONST.
# ast.GeneratorExp is NOT_CONST.
# ast.Await is NOT_CONST.
# ast.Yield is NOT_CONST.
# ast.YieldFrom is NOT_CONST.
elif isinstance(node, ast.Compare):
return min(
check_node(node.left),
check_nodes(node.comparators),
)
elif isinstance(node, ast.Call):
const, name = check_name(node.func)
# The function must have a name, and must be declared pure.
if (const != GLOBAL_CONST) or (name not in self.pure_functions):
return NOT_CONST
return min(
check_nodes(node.args),
check_nodes(i.value for i in node.keywords),
)
elif isinstance(node, ast.FormattedValue):
return min(
check_node(node.value),
check_node(node.format_spec),
)
elif isinstance(node, ast.JoinedStr):
return check_nodes(node.values)
elif isinstance(node, (ast.Attribute, ast.Name)):
return check_name(node)[0]
elif isinstance(node, ast.Subscript):
return min(
check_node(node.value),
check_node(node.slice),
)
elif isinstance(node, ast.Starred):
return check_node(node.value)
elif isinstance(node, (ast.List, ast.Tuple)):
return check_nodes(node.elts)
elif isinstance(node, ast.Slice):
return min(
check_node(node.lower),
check_node(node.upper),
check_node(node.step),
)
return NOT_CONST
return check_node(node)
def is_constant_expr(self, expr):
"""
Compiles `expr` into an AST node, then returns the result of
self.is_constant called on that node.
"""
node, literal = ccache.ast_eval_literal(expr)
if literal:
return GLOBAL_CONST
else:
return self.is_constant(node)
def python(self, code):
"""
Performs analysis on a block of python code.
"""
nodes = ccache.ast_exec(code)
a = PyAnalysis(self)
for i in nodes:
a.visit(i)
def parameters(self, parameters):
"""
Analyzes the parameters to the screen.
"""
self.global_constant = DeltaSet(constants)
# As we have parameters, analyze with those parameters.
for name in parameters.parameters:
self.mark_not_constant(name)
class PyAnalysis(ast.NodeVisitor):
"""
This analyzes Python code to determine which variables should be
marked const, and which should be marked non-const.
"""
def __init__(self, analysis):
self.analysis = analysis
# Expressions that assign names.
def visit_Name(self, node):
if isinstance(node.ctx, ast.AugStore):
self.analysis.mark_not_constant(node.id)
elif isinstance(node.ctx, ast.Store):
if self.analysis.control.const:
self.analysis.mark_constant(node.id)
else:
self.analysis.mark_not_constant(node.id)
def visit_NamedExpr(self, node):
const = self.analysis.is_constant(node.value)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
# Statements that assign names or control constness.
def visit_FunctionDef(self, node):
self.analysis.mark_constant(node.name)
def visit_AsyncFunctionDef(self, node):
self.analysis.mark_constant(node.name)
def visit_ClassDef(self, node):
self.analysis.mark_constant(node.name)
# Return can't assign a name.
# Delete doesn't assign a name - so it would be something else making
# the name non-const, not delete.
def visit_Assign(self, node):
const = self.analysis.is_constant(node.value)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_AugAssign(self, node):
self.analysis.push_control(False, False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_AnnAssign(self, node):
const = self.analysis.is_constant(node.value)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_For(self, node): # type: (ast.For|ast.AsyncFor) -> None
const = self.analysis.is_constant(node.iter)
self.analysis.push_control(const=const, loop=True)
old_const = self.analysis.control.const
self.generic_visit(node) # All nodes in the loop depend on node.test.
if self.analysis.control.const != old_const:
self.generic_visit(node)
self.analysis.pop_control()
def visit_AsyncFor(self, node):
return self.visit_For(node)
def visit_While(self, node):
const = self.analysis.is_constant(node.test)
self.analysis.push_control(const=const, loop=True)
old_const = self.analysis.control.const
self.generic_visit(node) # All nodes in the loop depend on node.test.
if self.analysis.control.const != old_const:
self.generic_visit(node)
self.analysis.pop_control()
def visit_If(self, node):
const = self.analysis.is_constant(node.test)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
# Nothing special for visit_With or visit_AsyncWith, when withitem is
# defined as below.
def visit_withitem(self, node):
const = self.analysis.is_constant(node.context_expr)
self.visit(node.context_expr)
self.analysis.push_control(const, False)
if node.optional_vars is not None:
self.visit(node.optional_vars)
self.analysis.pop_control()
# Match is barely implemented. We assume that it's always going to be
# performed on something non-constant, which means that every variable
# assigned inside the match is also non-constant. This is probably a
# reasonable assumption.
def visit_Match(self, node):
self.analysis.push_control(False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_Try(self, node):
for i in node.handlers:
if i.name:
self.analysis.mark_not_constant(i.name)
self.generic_visit(node)
# Import and Import from can only assign to a variable in a way that
# keeps it constant.
# Global and NonLocal only make sense inside Python functions, and we don't
# analyze Python functions.
# Expr can be ignored, as it can't assign.
# The continue and break statements should be pretty rare, so if they
# occur, we mark everything later in the loop as non-const.
def visit_Break(self, node):
self.analysis.exit_loop()
def visit_Continue(self, node):
self.analysis.exit_loop()
class CompilerCache(object):
"""
Objects of this class are used to cache the compiliation of Python code.
"""
def __init__(self):
self.ast_eval_cache = { }
self.ast_exec_cache = { }
# True if we've changed the caches.
self.updated = False
# The version of this object.
self.version = 1
def ast_eval_literal(self, expr):
"""
Compiles an expression into an AST.
"""
if isinstance(expr, renpy.ast.PyExpr):
filename = expr.filename
linenumber = expr.linenumber
else:
filename = None
linenumber = None
key = (expr, filename, linenumber)
rv = self.ast_eval_cache.get(key, None)
if rv is None:
expr = py_compile(expr, 'eval', ast_node=True)
try:
ast.literal_eval(expr)
literal = True
except Exception:
literal = False
rv = (expr, literal)
self.ast_eval_cache[key] = rv
self.updated = True
new_ccache.ast_eval_cache[key] = rv
return rv
def ast_eval(self, expr):
return self.ast_eval_literal(expr)[0]
def ast_exec(self, code):
"""
Compiles a block into an AST.
"""
if isinstance(code, renpy.ast.PyExpr):
key = (code, code.filename, code.linenumber)
else:
key = (code, None, None)
rv = self.ast_exec_cache.get(key, None)
if rv is None:
rv = py_compile(code, 'exec', ast_node=True)
self.ast_exec_cache[key] = rv
self.updated = True
new_ccache.ast_exec_cache[key] = rv
return rv
ccache = CompilerCache()
new_ccache = CompilerCache()
CACHE_FILENAME = "cache/py3analysis.rpyb"
def load_cache():
if renpy.game.args.compile: # type: ignore
return
try:
with renpy.loader.load(CACHE_FILENAME) as f:
c = loads(zlib.decompress(f.read()))
if c.version == ccache.version:
ccache.ast_eval_cache.update(c.ast_eval_cache)
ccache.ast_exec_cache.update(c.ast_exec_cache)
except Exception:
pass
def save_cache():
if not ccache.updated:
return
if renpy.macapp:
return
try:
data = zlib.compress(dumps(new_ccache, True), 3)
with open(renpy.loader.get_path(CACHE_FILENAME), "wb") as f:
f.write(data)
except Exception:
pass
+845 -4
View File
@@ -23,7 +23,848 @@ from __future__ import division, absolute_import, with_statement, print_function
from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # *
if PY2:
from renpy.py2analysis import *
else:
from renpy.py3analysis import *
import builtins
import renpy # @UnusedImport
from renpy.python import py_compile
# Import the Python AST module, instead of the Ren'Py ast module.
import ast
import zlib
from renpy.compat.pickle import loads, dumps
# The set of names that should be treated as constants.
always_constants = { 'True', 'False', 'None' }
# The set of names that should be treated as pure functions.
pure_functions = {
# Python 3 builtins.
'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytes', 'callable',
'chr', 'complex', 'dict', 'dir', 'divmod', 'enumerate', 'filter', 'float',
'format', 'frozenset', 'getattr', 'hasattr', 'hash', 'hex', 'int',
'isinstance', 'issubclass', 'len', 'list', 'map', 'max', 'min', 'oct',
'ord', 'pow', 'range', 'repr', 'reversed', 'round', 'set', 'slice', 'sorted',
'str', 'sum', 'tuple', 'type', 'zip',
# minstore.py
"_",
"_p",
"absolute",
"position",
"__renpy__list__",
"__renpy__dict__",
"__renpy__set__",
# defaultstore.py
"ImageReference", "Image", "Frame", "Solid", "LiveComposite", "LiveCrop",
"LiveTile", "Flatten", "Null", "Window", "Viewport", "DynamicDisplayable",
"ConditionSwitch", "ShowingSwitch", "Transform", "Animation", "Movie",
"Particles", "SnowBlossom", "Text", "ParameterizedText", "FontGroup",
"Drag", "Alpha", "AlphaMask", "Position", "Pan", "Move", "Motion", "Revolve", "Zoom",
"RotoZoom", "FactorZoom", "SizeZoom", "Fade", "Dissolve", "ImageDissolve",
"AlphaDissolve", "CropMove", "PushMove", "Pixellate", "OldMoveTransition",
"MoveTransition", "MoveFactory", "MoveIn", "MoveOut", "ZoomInOut",
"RevolveInOut", "MultipleTransition", "ComposeTransition", "Pause",
"SubTransition", "ADVSpeaker", "ADVCharacter", "Speaker", "Character",
"DynamicCharacter", "Fixed", "HBox", "VBox", "Grid", "AlphaBlend", "At",
"color", "Color",
# ui.py
"ui.returns",
"ui.jumps",
"ui.jumpsoutofcontext",
"ui.callsinnewcontext",
"ui.invokesinnewcontext",
"ui.gamemenus",
# renpy.py
"renpy.version_string",
"renpy.version_only",
"renpy.version_tuple",
"renpy.version_name",
"renpy.license",
}
constants = { "config", "style" } | always_constants | pure_functions
# A set of names that should not be treated as global constants.
not_constants = set()
# The base set for the local constants.
local_constants = set()
def const(name):
"""
:doc: const
Declares a variable in the store to be constant.
A variable is constant if nothing can change its value, or any value
reached by indexing it or accessing its attributes. Variables must
remain constant out of define, init, and translate python blocks.
`name`
A string giving the name of the variable to declare constant.
"""
if name not in not_constants:
constants.add(name)
def not_const(name):
"""
:doc: const
Declares a name in the store to be not constant.
This undoes the effect of calls to :func:`renpy.const` and
:func:`renpy.pure`.
`name`
The name to declare not constant.
"""
constants.discard(name)
pure_functions.discard(name)
not_constants.add(name)
def pure(fn):
"""
:doc: const
Declares a function as pure. A pure function must always return the
same value when it is called with the same arguments, outside of
define, init, and translate python blocks.
`fn`
The name of the function to declare pure. This may either be a string
containing the name of the function, or the function itself.
If a string is passed and the function is inside a module,
this string should contain the module name with the dot.
Returns `fn`, allowing this function to be used as a decorator.
"""
name = fn
if not isinstance(name, basestring):
name = fn.__name__
module = fn.__module__
name = module + "." + name
if name.startswith("store."):
name = name[6:]
if name not in not_constants:
pure_functions.add(name)
constants.add(name)
return fn
class Control(object):
"""
Represents control flow.
`const`
True if this statement always executes.
`loop`
True if this corresponds to a loop.
`imagemap`
True if this control is in a non-constant imagemap.
"""
def __init__(self, const, loop, imagemap):
self.const = const
self.loop = loop
self.imagemap = imagemap
def __repr__(self):
return "<Control const={0} loop={1} imagemap={2}>".format(self.const, self.loop, self.imagemap)
# Three levels of constness.
# An expression is globally constant if it will evaluate to the same value
# whenever it is run.
GLOBAL_CONST = 2
# An expression is locally const if it will evaluate to the same value when
# run in the same place - the same screen, with the same parameters, the same
# statement, and the same iteration of a for loop.
LOCAL_CONST = 1
# An expression is not const if it wilk change it's value.
NOT_CONST = 0
class DeltaSet(object):
def __init__(self, base, copy=None):
"""
Represents a set that stores its contents as differences from a base
set.
"""
self.base = base
if copy is not None:
self.added = set(copy.added)
self.removed = set(copy.removed)
else:
self.added = set()
self.removed = set()
self.changed = False
def add(self, v):
if v in self.removed:
self.removed.discard(v)
self.changed = True
elif v not in self.base and v not in self.added:
self.added.add(v)
self.changed = True
def discard(self, v):
if v in self.added:
self.added.discard(v)
self.changed = True
elif v in self.base and v not in self.removed:
self.removed.add(v)
self.changed = True
def __contains__(self, v):
return (v in self.added) or ((v in self.base) and (v not in self.removed))
def copy(self):
return DeltaSet(self.base, self)
def __iter__(self):
for i in self.base:
if i not in self.removed:
yield i
for i in self.added:
yield i
class Analysis(object):
"""
Represents the result of code analysis, and provides tools to perform
code analysis.
"""
def __init__(self, parent=None):
# The parent context transcludes run in, or None if there is no parent
# context.
self.parent = parent
# Analyses of children, such a screens we use.
self.children = { }
# The variables we consider to be not-constant.
self.not_constant = DeltaSet(not_constants)
# Variables we consider to be locally constant.
self.local_constant = DeltaSet(local_constants)
# Variables we consider to be globally constant.
self.global_constant = DeltaSet(always_constants)
# The functions we consider to be pure.
self.pure_functions = DeltaSet(pure_functions)
# Represents what we know about the current control.
self.control = Control(True, False, False)
# The stack of const_flow values.
self.control_stack = [ self.control ]
def get_child(self, identifier):
if identifier in self.children:
return self.children[identifier]
rv = Analysis(self)
self.children[identifier] = rv
return rv
def push_control(self, const=True, loop=False, imagemap=False):
self.control = Control(self.control.const and const, loop, self.control.imagemap or imagemap)
self.control_stack.append(self.control) # type: ignore
def pop_control(self):
rv = self.control_stack.pop()
self.control = self.control_stack[-1]
return rv
def imagemap(self):
"""
Returns NOT_CONST if we're in a non-constant imagemap.
"""
if self.control.imagemap:
return NOT_CONST
else:
return GLOBAL_CONST
def exit_loop(self):
"""
Call this to indicate the current loop is being exited by the
continue or break statements.
"""
l = list(self.control_stack)
l.reverse()
for i in l:
i.const = False
if i.loop:
break
def at_fixed_point(self):
"""
Returns True if we've reached a fixed point, where the analysis has
not changed since the last time we called this function.
"""
for i in self.children.values():
if not i.at_fixed_point():
return False
if (self.not_constant.changed or
self.global_constant.changed or
self.local_constant.changed or
self.pure_functions.changed):
self.not_constant.changed = False
self.global_constant.changed = False
self.local_constant.changed = False
self.pure_functions.changed = False
return False
return True
def mark_constant(self, name):
"""
Marks `name` as a potential local constant.
"""
if not name in self.not_constant:
self.local_constant.add(name)
self.global_constant.discard(name)
self.pure_functions.discard(name)
def mark_not_constant(self, name):
"""
Marks `name` as definitely not-constant.
"""
self.not_constant.add(name)
self.pure_functions.discard(name)
self.local_constant.discard(name)
self.global_constant.discard(name)
def _check_name(self, node):
"""
Check nodes that make up a name. This returns a pair:
* The first element is True if the node is constant, and False
otherwise.
* The second element is None if the node is constant or the name is
not known, and the name otherwise.
"""
if isinstance(node, ast.Name):
const = NOT_CONST
name = node.id
elif isinstance(node, ast.Attribute):
const, name = self._check_name(node.value)
if name is not None:
name = name + "." + node.attr
else:
return self._check_node(node), None
if name in self.not_constant:
return NOT_CONST, name
elif name in self.global_constant:
return GLOBAL_CONST, name
elif name in self.local_constant:
return LOCAL_CONST, name
else:
return const, name
def _check_nodes(self, nodes):
"""
Checks a list of nodes for constness.
"""
nodes = list(nodes)
if not nodes:
return GLOBAL_CONST
return min(self._check_node(i) for i in nodes)
def _check_node(self, node):
"""
When given `node`, part of a Python expression, returns how
const the expression is.
"""
# This handles children that do not exist.
if node is None:
return GLOBAL_CONST
# PY3: see if there are new node types.
if isinstance(node, ast.Constant):
return GLOBAL_CONST
elif isinstance(node, ast.BoolOp):
return self._check_nodes(node.values)
elif isinstance(node, ast.NamedExpr):
return self._check_node(node.value)
elif isinstance(node, ast.BinOp):
return min(
self._check_node(node.left),
self._check_node(node.right),
)
elif isinstance(node, ast.UnaryOp):
return self._check_node(node.operand)
# ast.Lambda is NOT_CONST.
elif isinstance(node, ast.IfExp):
return min(
self._check_node(node.test),
self._check_node(node.body),
self._check_node(node.orelse),
)
elif isinstance(node, ast.Dict):
return min(
self._check_nodes(node.keys),
self._check_nodes(node.values)
)
elif isinstance(node, ast.Set):
return self._check_nodes(node.elts)
# ast.ListComp is NOT_CONST.
# ast.SetComp is NOT_CONST.
# ast.DictComp is NOT_CONST.
# ast.GeneratorExp is NOT_CONST.
# ast.Await is NOT_CONST.
# ast.Yield is NOT_CONST.
# ast.YieldFrom is NOT_CONST.
elif isinstance(node, ast.Compare):
return min(
self._check_node(node.left),
self._check_nodes(node.comparators),
)
elif isinstance(node, ast.Call):
const, name = self._check_name(node.func)
# The function must have a name, and must be declared pure.
if (const != GLOBAL_CONST) or (name not in self.pure_functions):
return NOT_CONST
return min(
self._check_nodes(node.args),
self._check_nodes(i.value for i in node.keywords),
)
elif isinstance(node, ast.FormattedValue):
return min(
self._check_node(node.value),
self._check_node(node.format_spec),
)
elif isinstance(node, ast.JoinedStr):
return self._check_nodes(node.values)
elif isinstance(node, (ast.Attribute, ast.Name)):
return self._check_name(node)[0]
elif isinstance(node, ast.Subscript):
return min(
self._check_node(node.value),
self._check_node(node.slice),
)
elif isinstance(node, ast.Starred):
return self._check_node(node.value)
elif isinstance(node, (ast.List, ast.Tuple)):
return self._check_nodes(node.elts)
elif isinstance(node, ast.Slice):
return min(
self._check_node(node.lower),
self._check_node(node.upper),
self._check_node(node.step),
)
return NOT_CONST
def is_constant(self, node):
"""
Returns true if `node` is constant for the purpose of screen
language. Node should be a python AST node.
Screen language ignores object identity for the purposes of
object equality.
"""
return self._check_node(node)
def is_constant_expr(self, expr):
"""
Compiles `expr` into an AST node, then returns the result of
self.is_constant called on that node.
"""
node, literal = ccache.ast_eval_literal(expr)
if literal:
return GLOBAL_CONST
else:
return self.is_constant(node)
def python(self, code):
"""
Performs analysis on a block of python code.
"""
nodes = ccache.ast_exec(code)
a = PyAnalysis(self)
for i in nodes:
a.visit(i)
def parameters(self, parameters):
"""
Analyzes the parameters to the screen.
"""
self.global_constant = DeltaSet(constants)
# As we have parameters, analyze with those parameters.
for name in parameters.parameters:
self.mark_not_constant(name)
class PyAnalysis(ast.NodeVisitor):
"""
This analyzes Python code to determine which variables should be
marked const, and which should be marked non-const.
"""
def __init__(self, analysis):
self.analysis = analysis
# Expressions that assign names.
def visit_Name(self, node):
if isinstance(node.ctx, ast.AugStore):
self.analysis.mark_not_constant(node.id)
elif isinstance(node.ctx, ast.Store):
if self.analysis.control.const:
self.analysis.mark_constant(node.id)
else:
self.analysis.mark_not_constant(node.id)
def visit_NamedExpr(self, node):
const = self.analysis.is_constant(node.value)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
# Statements that assign names or control constness.
def visit_FunctionDef(self, node):
self.analysis.mark_constant(node.name)
def visit_AsyncFunctionDef(self, node):
self.analysis.mark_constant(node.name)
def visit_ClassDef(self, node):
self.analysis.mark_constant(node.name)
# Return can't assign a name.
# Delete doesn't assign a name - so it would be something else making
# the name non-const, not delete.
def visit_Assign(self, node):
const = self.analysis.is_constant(node.value)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_AugAssign(self, node):
self.analysis.push_control(False, False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_AnnAssign(self, node):
const = self.analysis.is_constant(node.value)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
def visit_For(self, node): # type: (ast.For|ast.AsyncFor) -> None
const = self.analysis.is_constant(node.iter)
self.analysis.push_control(const=const, loop=True)
old_const = self.analysis.control.const
self.generic_visit(node) # All nodes in the loop depend on node.test.
if self.analysis.control.const != old_const:
self.generic_visit(node)
self.analysis.pop_control()
def visit_AsyncFor(self, node):
return self.visit_For(node)
def visit_While(self, node):
const = self.analysis.is_constant(node.test)
self.analysis.push_control(const=const, loop=True)
old_const = self.analysis.control.const
self.generic_visit(node) # All nodes in the loop depend on node.test.
if self.analysis.control.const != old_const:
self.generic_visit(node)
self.analysis.pop_control()
def visit_If(self, node):
const = self.analysis.is_constant(node.test)
self.analysis.push_control(const, False)
self.generic_visit(node)
self.analysis.pop_control()
# Nothing special for visit_With or visit_AsyncWith, when withitem is
# defined as below.
def visit_withitem(self, node):
const = self.analysis.is_constant(node.context_expr)
self.visit(node.context_expr)
self.analysis.push_control(const, False)
if node.optional_vars is not None:
self.visit(node.optional_vars)
self.analysis.pop_control()
# Match is barely implemented. We assume that it's always going to be
# performed on something non-constant, which means that every variable
# assigned inside the match is also non-constant. This is probably a
# reasonable assumption.
def visit_MatchMapping(self, node):
if node.rest:
self.analysis.mark_not_constant(node.rest)
def visit_MatchStar(self, node):
if node.name is not None:
self.analysis.mark_not_constant(node.name)
def visit_MatchAs(self, node):
if node.name is not None:
self.analysis.mark_not_constant(node.name)
def visit_Try(self, node):
for i in node.handlers:
if i.name:
self.analysis.mark_not_constant(i.name)
self.generic_visit(node)
# Import and Import from can only assign to a variable in a way that
# keeps it constant.
# Global and NonLocal only make sense inside Python functions, and we don't
# analyze Python functions.
# Expr can be ignored, as it can't assign.
# The continue and break statements should be pretty rare, so if they
# occur, we mark everything later in the loop as non-const.
def visit_Break(self, node):
self.analysis.exit_loop()
def visit_Continue(self, node):
self.analysis.exit_loop()
class CompilerCache(object):
"""
Objects of this class are used to cache the compiliation of Python code.
"""
def __init__(self):
self.ast_eval_cache = { }
self.ast_exec_cache = { }
# True if we've changed the caches.
self.updated = False
# The version of this object.
self.version = 1
def ast_eval_literal(self, expr):
"""
Compiles an expression into an AST.
"""
if isinstance(expr, renpy.ast.PyExpr):
filename = expr.filename
linenumber = expr.linenumber
else:
filename = None
linenumber = None
key = (expr, filename, linenumber)
rv = self.ast_eval_cache.get(key, None)
if rv is None:
expr = py_compile(expr, 'eval', ast_node=True)
try:
ast.literal_eval(expr)
literal = True
except Exception:
literal = False
rv = (expr, literal)
self.ast_eval_cache[key] = rv
self.updated = True
new_ccache.ast_eval_cache[key] = rv
return rv
def ast_eval(self, expr):
return self.ast_eval_literal(expr)[0]
def ast_exec(self, code):
"""
Compiles a block into an AST.
"""
if isinstance(code, renpy.ast.PyExpr):
key = (code, code.filename, code.linenumber)
else:
key = (code, None, None)
rv = self.ast_exec_cache.get(key, None)
if rv is None:
rv = py_compile(code, 'exec', ast_node=True)
self.ast_exec_cache[key] = rv
self.updated = True
new_ccache.ast_exec_cache[key] = rv
return rv
ccache = CompilerCache()
new_ccache = CompilerCache()
CACHE_FILENAME = "cache/py3analysis.rpyb"
def load_cache():
if renpy.game.args.compile: # type: ignore
return
try:
with renpy.loader.load(CACHE_FILENAME) as f:
c = loads(zlib.decompress(f.read()))
if c.version == ccache.version:
ccache.ast_eval_cache.update(c.ast_eval_cache)
ccache.ast_exec_cache.update(c.ast_exec_cache)
except Exception:
pass
def save_cache():
if not ccache.updated:
return
if renpy.macapp:
return
try:
data = zlib.compress(dumps(new_ccache, True), 3)
with open(renpy.loader.get_path(CACHE_FILENAME), "wb") as f:
f.write(data)
except Exception:
pass
+234 -171
View File
@@ -26,7 +26,7 @@
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, Any
from typing import Optional, Any, cast
import contextlib
# Import the python ast module, not ours.
@@ -351,13 +351,6 @@ def reset_store_changes(name):
# Code that replaces literals will calls to magic constructors.
def b(s):
if PY2:
return s.encode("utf-8")
else:
return s
class LoadedVariables(ast.NodeVisitor):
"""
This is used to implement find_loaded_variables.
@@ -368,15 +361,21 @@ class LoadedVariables(ast.NodeVisitor):
self.loaded.add(node.id)
elif isinstance(node.ctx, ast.Store):
self.stored.add(node.id)
elif PY2 and isinstance(node.ctx, ast.Param):
# there's no guarantee that ast.Param will keep existing in future versions of python3
# it's only present in asts made in py2
self.stored.add(node.id)
if not PY2:
# we could remove this if, but the method wouldn't be called in py2 anyway
def visit_arg(self, node):
self.stored.add(node.arg)
def visit_MatchMapping(self, node):
if node.rest:
self.stored.add(node.rest)
def visit_MatchStar(self, node):
if node.name is not None:
self.stored.add(node.name)
def visit_MatchAs(self, node):
if node.name is not None:
self.stored.add(node.name)
def visit_arg(self, node):
self.stored.add(node.arg)
def find(self, node):
self.loaded = set()
@@ -444,6 +443,34 @@ class WrapFormattedValue(ast.NodeTransformer):
wrap_formatted_value = WrapFormattedValue().visit
class FindStarredMatchPatterns(ast.NodeVisitor):
"""
Given a match patten, return a list of (name, type) tuples, where type is "dict" or "list".
"""
def __init__(self):
self.vars = [ ]
def visit_MatchStar(self, node: ast.MatchStar) -> Any:
self.generic_visit(node)
if node.name is not None:
self.vars.append((node.name, "list"))
def visit_MatchMapping(self, node: ast.MatchMapping) -> Any:
self.generic_visit(node)
if node.rest is not None:
self.vars.append((node.rest, "dict"))
def find_starred_match_patterns(node):
"""
Given a match pattern, return a list of (name, type) tuples, where type is "dict" or "list".
"""
visitor = FindStarredMatchPatterns()
visitor.visit(node)
return visitor.vars
class WrapNode(ast.NodeTransformer):
@@ -470,53 +497,26 @@ class WrapNode(ast.NodeTransformer):
call_args =[ ]
for var in variables:
if PY2:
lambda_args.append(ast.Name(id=var, ctx=ast.Param()))
else:
lambda_args.append(ast.arg(arg=var))
lambda_args.append(ast.arg(arg=var))
call_args.append(ast.Name(id=var, ctx=ast.Load()))
if PY2:
return ast.Call(
func=ast.Lambda(
args=ast.arguments(
args=lambda_args,
vararg=None,
kwarg=None,
defaults=[]
),
body=node,
return ast.Call(
func=ast.Lambda(
args=ast.arguments(
posonlyargs=[ ],
args=lambda_args,
kwonlyargs=[ ],
kw_defaults=[ ],
defaults=[ ],
),
args=call_args,
keywords=[ ],
starargs=None,
kwargs=None,
)
else:
return ast.Call(
func=ast.Lambda(
args=ast.arguments(
posonlyargs=[ ],
args=lambda_args,
kwonlyargs=[ ],
kw_defaults=[ ],
defaults=[ ],
),
body=node,
),
args=call_args,
keywords=[ ],
)
body=node,
),
args=call_args,
keywords=[ ],
)
def wrap_starred_assign(self, n, targets):
if PY2:
return n
starred = find_starred_variables(targets)
if not starred:
@@ -528,15 +528,13 @@ class WrapNode(ast.NodeTransformer):
call = ast.Call(
func=ast.Name(
id=b("__renpy__list__"),
id="__renpy__list__",
ctx=ast.Load()
),
args=[
ast.Name(id=var, ctx=ast.Load())
],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
assign = ast.Assign(
targets=[ ast.Name(id=var, ctx=ast.Store()) ],
@@ -554,9 +552,6 @@ class WrapNode(ast.NodeTransformer):
def wrap_starred_for(self, node):
if PY2:
return node
starred = find_starred_variables([ node.target ])
if not starred:
@@ -566,15 +561,13 @@ class WrapNode(ast.NodeTransformer):
call = ast.Call(
func=ast.Name(
id=b("__renpy__list__"),
id="__renpy__list__",
ctx=ast.Load()
),
args=[
ast.Name(id=var, ctx=ast.Load())
],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
assign = ast.Assign(
targets=[ ast.Name(id=var, ctx=ast.Store()) ],
@@ -588,9 +581,6 @@ class WrapNode(ast.NodeTransformer):
def wrap_starred_with(self, node):
if PY2:
return node
optional_vars = [ ]
for i in node.items:
@@ -609,15 +599,13 @@ class WrapNode(ast.NodeTransformer):
call = ast.Call(
func=ast.Name(
id=b("__renpy__list__"),
id="__renpy__list__",
ctx=ast.Load()
),
args=[
ast.Name(id=var, ctx=ast.Load())
],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
assign = ast.Assign(
targets=[ ast.Name(id=var, ctx=ast.Store()) ],
@@ -628,6 +616,52 @@ class WrapNode(ast.NodeTransformer):
return node
def wrap_match_case(self, node):
starred = find_starred_match_patterns(node.pattern)
if not starred:
return node
for var, kind in reversed(starred):
if kind == "list":
call = ast.Call(
func=ast.Name(
id="__renpy__list__",
ctx=ast.Load()
),
args=[
ast.Name(id=var, ctx=ast.Load())
],
keywords=[ ])
assign = ast.Assign(
targets=[ ast.Name(id=var, ctx=ast.Store()) ],
value=call,
)
node.body.insert(0, assign)
elif kind == "dict":
call = ast.Call(
func=ast.Name(
id="__renpy__dict__",
ctx=ast.Load()
),
args=[
ast.Name(id=var, ctx=ast.Load())
],
keywords=[ ])
assign = ast.Assign(
targets=[ ast.Name(id=var, ctx=ast.Store()) ],
value=call,
)
node.body.insert(0, assign)
return node
def visit_Assign(self, n):
n = self.generic_visit(n)
return self.wrap_starred_assign(n, n.targets) # type: ignore
@@ -656,7 +690,7 @@ class WrapNode(ast.NodeTransformer):
n = self.generic_visit(n)
if not n.bases: # type: ignore
n.bases.append(ast.Name(id=b("object"), ctx=ast.Load())) # type: ignore
n.bases.append(ast.Name(id="object", ctx=ast.Load())) # type: ignore
return n
@@ -666,80 +700,75 @@ class WrapNode(ast.NodeTransformer):
def visit_SetComp(self, n):
return ast.Call(
func=ast.Name(
id=b("__renpy__set__"),
id="__renpy__set__",
ctx=ast.Load()
),
args=[ self.wrap_generator(n) ],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
def visit_Set(self, n):
return ast.Call(
func=ast.Name(
id=b("__renpy__set__"),
id="__renpy__set__",
ctx=ast.Load()
),
args=[ self.generic_visit(n) ],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
def visit_ListComp(self, n):
return ast.Call(
func=ast.Name(
id=b("__renpy__list__"),
id="__renpy__list__",
ctx=ast.Load()
),
args=[ self.wrap_generator(n) ],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
def visit_List(self, n):
if not isinstance(n.ctx, ast.Load):
return self.generic_visit(n)
return ast.Call(
func=ast.Name(
id=b("__renpy__list__"),
id="__renpy__list__",
ctx=ast.Load()
),
args=[ self.generic_visit(n) ],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
def visit_DictComp(self, n):
return ast.Call(
func=ast.Name(
id=b("__renpy__dict__"),
id="__renpy__dict__",
ctx=ast.Load()
),
args=[ self.wrap_generator(n) ],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
def visit_Dict(self, n):
return ast.Call(
func=ast.Name(
id=b("__renpy__dict__"),
id="__renpy__dict__",
ctx=ast.Load()
),
args=[ self.generic_visit(n) ],
keywords=[ ],
starargs=None,
kwargs=None)
keywords=[ ])
def visit_FormattedValue(self, n):
n = wrap_formatted_value(n)
return self.generic_visit(n)
def visit_Match(self, node : ast.Match) -> Any:
n : ast.Match = self.generic_visit(node) # type: ignore
n.cases = [ self.wrap_match_case(i) for i in n.cases ]
return n
wrap_node = WrapNode()
@@ -847,35 +876,86 @@ py_compile_cache = { }
old_py_compile_cache = { }
def fix_locations(node, lineno, col_offset):
def fix_locations(
node: ast.AST,
lineno_increment: int = 0,
col_offset: int = 0,
strip_ends: bool = False,
_parent_offset=(0, 0),
_first=True,
):
"""
Assigns locations to the given node, and all of its children, adding
any missing line numbers and column offsets.
This function does `ast.increment_lineno`, `ast.fix_missing_locations`
and `[end_]col_offset` increments in one pass.
`lineno_increment`
Amount of lines to increment `lineno` by.
`col_offset`
0-indexed offset of source code. This should be used in case
source code have extra indent relative to compiled source.
`strip_ends`
If `True`, removes `end_lineno` and `end_col_offset`
so traceback does not render incorrect carets for given nodes tree.
"""
start = max(
(lineno, col_offset),
(getattr(node, "lineno", None) or 1, getattr(node, "col_offset", None) or 0)
)
if _first:
if not isinstance(node, (ast.Module, ast.Expression)):
raise ValueError(f"'fix_locations' called with unsupported top level node: {node}")
lineno, col_offset = start
# TypeIgnore is a special case where lineno is not an attribute
# but rather a field of the node itself.
if isinstance(node, ast.TypeIgnore):
node.lineno = getattr(node, 'lineno', 0) + lineno_increment
return
node.lineno = lineno
node.col_offset = col_offset
parent_lineno, parent_col_offset = _parent_offset
ends = [ start, (getattr(node, "end_lineno", None) or 1, getattr(node, "end_col_offset", None) or 0) ]
# lineno and col_offset must exist on every node.
# If there are none, we assume ast creator knows what they
# are doing and we can just assign to parent values.
if 'lineno' in node._attributes:
node = cast("ast.expr", node)
node.lineno = getattr(node, 'lineno', parent_lineno)
parent_lineno = node.lineno
node.lineno += lineno_increment
if 'col_offset' in node._attributes:
node = cast("ast.expr", node)
node.col_offset = getattr(node, 'col_offset', parent_col_offset)
parent_col_offset = node.col_offset
node.col_offset += col_offset
for child in ast.iter_child_nodes(node):
fix_locations(child, lineno, col_offset)
ends.append((child.end_lineno, child.end_col_offset))
fix_locations(
child,
lineno_increment,
col_offset,
strip_ends,
(parent_lineno, parent_col_offset),
False)
end = max(ends)
# end_lineno and end_col_offset are optional, and we remove them if
# strip_ends is True (for generated code that doesn't have source).
if 'end_lineno' in node._attributes:
node = cast("ast.expr", node)
if strip_ends:
node.end_lineno = None
else:
end_lineno = getattr(node, 'end_lineno', None)
node.end_lineno = (end_lineno or parent_lineno) + lineno_increment
node.end_lineno = end[0]
node.end_col_offset = end[1]
if 'end_col_offset' in node._attributes:
node = cast("ast.expr", node)
if strip_ends:
node.end_col_offset = None
else:
end_col_offset = getattr(node, 'end_col_offset', None)
node.end_col_offset = (end_col_offset or parent_col_offset) + col_offset
def quote_eval(s):
def quote_eval(s: str):
"""
Quotes a string for `eval`. This is necessary when it's in certain places,
like as part of an argument string. We need to stick a single backslash
@@ -968,18 +1048,17 @@ def quote_eval(s):
compile_filename = ""
def py_compile(source, mode, filename='<none>', lineno=1, ast_node=False, cache=True, py=None):
def py_compile(source, mode, filename='<none>', lineno=1, col_offset=None, ast_node=False, cache=True, py=None):
"""
Compiles the given source code using the supplied codegenerator.
Compiles the given source code using the supplied code generator.
Lists, List Comprehensions, and Dictionaries are wrapped when
appropriate.
`source`
The source code, as a either a string, pyexpr, or ast module
node.
The source code, as a either a string or PyExpr.
`mode`
One of "exec" or "eval".
One of "exec", "hide" or "eval".
`filename`
The filename the source comes from. If a pyexpr is given, the
@@ -987,7 +1066,12 @@ def py_compile(source, mode, filename='<none>', lineno=1, ast_node=False, cache=
`lineno`
The line number of the first line of source code. If a pyexpr is
given, the filename embedded in the pyexpr is used.
given, the lineno embedded in the pyexpr is used.
`col_offset`
If not None, the column offset of the first line of source code,
otherwise offset information is stripped from the source code.
If a pyexpr is given, the col_offset embedded in the pyexpr is used.
`ast_node`
Rather than returning compiled bytecode, returns the AST object
@@ -1000,9 +1084,6 @@ def py_compile(source, mode, filename='<none>', lineno=1, ast_node=False, cache=
if ast_node:
cache = False
if isinstance(source, ast.Module):
return compile(source, filename, mode)
if isinstance(source, renpy.ast.PyExpr):
filename = source.filename
lineno = source.linenumber
@@ -1011,10 +1092,7 @@ def py_compile(source, mode, filename='<none>', lineno=1, ast_node=False, cache=
py = source.py
if py is None:
if PY2:
py = 2
else:
py = 3
py = 3
if cache:
key = (lineno, filename, str(source), mode, renpy.script.MAGIC)
@@ -1052,8 +1130,10 @@ def py_compile(source, mode, filename='<none>', lineno=1, ast_node=False, cache=
if mode == "eval":
source = quote_eval(source)
line_offset = lineno - 1
compile_filename = filename
line_offset = lineno - 1
code_offset = col_offset or 0
strip_ends = col_offset is None
try:
@@ -1063,42 +1143,38 @@ def py_compile(source, mode, filename='<none>', lineno=1, ast_node=False, cache=
py_mode = mode
flags = file_compiler_flags.get(filename, 0)
flags |= new_compile_flags
if (not PY2) or flags:
try:
with save_warnings():
# filename should not be a valid file name, so Python tokenizer
# function 'get_error_line_from_tokenizer_buffers' does not try
# to read from the file to get the size of the buffer of error
# line.
tree = compile(source, "<tree>", py_mode, ast.PyCF_ONLY_AST | flags, 1)
except SyntaxError as orig_e:
# Set back filename to correct one.
# For what it worth, '_PyPegen_raise_error_known_location'
# fixes lineno, col_offset and end_col_offset, but not end_lineno.
# We don't use it, so whatever.
orig_e.filename = filename
flags |= new_compile_flags
# Inserting artificial tokens can broke locations.
strip_ends = True
try:
fixed_source = renpy.compat.fixes.fix_tokens(source)
with save_warnings():
tree = compile(source, filename, py_mode, ast.PyCF_ONLY_AST | flags, 1)
except SyntaxError as orig_e:
try:
fixed_source = renpy.compat.fixes.fix_tokens(source)
with save_warnings():
tree = compile(fixed_source, filename, py_mode, ast.PyCF_ONLY_AST | flags, 1)
except Exception:
raise orig_e
else:
try:
flags = new_compile_flags
with save_warnings():
tree = compile(source, filename, py_mode, ast.PyCF_ONLY_AST | flags, 1)
tree = compile(fixed_source, "<tree>", py_mode, ast.PyCF_ONLY_AST | flags, 1)
except Exception:
flags = old_compile_flags
source = escape_unicode(source)
with save_warnings():
tree = compile(source, filename, py_mode, ast.PyCF_ONLY_AST | flags, 1)
raise orig_e
tree = wrap_node.visit(tree)
if mode == "hide":
wrap_hide(tree)
fix_locations(tree, 1, 0)
ast.increment_lineno(tree, lineno - 1)
fix_locations(tree, line_offset, code_offset, strip_ends)
line_offset = 0
@@ -1111,7 +1187,8 @@ def py_compile(source, mode, filename='<none>', lineno=1, ast_node=False, cache=
except SyntaxError as orig_e:
try:
tree = renpy.compat.fixes.fix_ast(tree)
fix_locations(tree, 1, 0)
# Creating artificial nodes can broke locations.
fix_locations(tree, strip_ends=True)
with save_warnings():
rv = compile(tree, filename, py_mode, flags, 1)
except Exception:
@@ -1252,20 +1329,6 @@ class StoreProxy(object):
def method_unpickle(obj, name):
return getattr(obj, name)
if PY2:
# Code for pickling bound methods.
def method_pickle(method):
name = method.im_func.__name__
obj = method.im_self
if obj is None:
obj = method.im_class
return method_unpickle, (obj, name)
copyreg.pickle(types.MethodType, method_pickle)
# Code for pickling modules.
+28 -73
View File
@@ -65,18 +65,10 @@ copyreg._reconstructor = _reconstructor # type: ignore
# this to check to see if a background-save is valid.
mutate_flag = True
# In Python 2 functools.wraps does not check for the existence of WRAPPER_ASSIGNMENTS elements,
# so for the C-defined methods we have AttributeError: 'method_descriptor' object has no attribute '__module__'.
# To work around this, we only keep attributes that surely exist.
if PY2:
def _method_wrapper(method):
return functools.wraps(method, ("__name__", "__doc__"), ())
else:
_method_wrapper = functools.wraps # type: ignore
def mutator(method):
@_method_wrapper(method)
@functools.wraps(method)
def do_mutation(self, *args, **kwargs):
global mutate_flag
@@ -176,11 +168,7 @@ class RevertableList(list):
list.__init__(self, *args)
__delitem__ = mutator(list.__delitem__)
if PY2:
__delslice__ = mutator(list.__delslice__) # type: ignore
__setitem__ = mutator(list.__setitem__)
if PY2:
__setslice__ = mutator(list.__setslice__) # type: ignore
__iadd__ = mutator(list.__iadd__)
__imul__ = mutator(list.__imul__)
append = mutator(list.append)
@@ -193,7 +181,7 @@ class RevertableList(list):
def wrapper(method): # type: ignore
@_method_wrapper(method)
@functools.wraps(method)
def newmethod(*args, **kwargs):
l = method(*args, **kwargs) # type: ignore
if l is NotImplemented:
@@ -203,8 +191,6 @@ class RevertableList(list):
return newmethod
__add__ = wrapper(list.__add__) # type: ignore
if PY2:
__getslice__ = wrapper(list.__getslice__) # type: ignore
__mul__ = wrapper(list.__mul__) # type: ignore
__rmul__ = wrapper(list.__rmul__) # type: ignore
@@ -288,58 +274,31 @@ class RevertableDict(dict):
setdefault = mutator(dict.setdefault)
update = mutator(dict.update)
if PY2:
itervalues = dict.values
iterkeys = dict.keys
iteritems = dict.items
def keys(self):
rv = dict.keys(self)
def has_key(self, key):
return (key in self)
if (sys._getframe(1).f_code.co_flags & FUTURE_FLAGS) != FUTURE_FLAGS:
rv = RevertableList(rv)
# https://peps.python.org/pep-0584 methods
def __or__(self, other):
if not isinstance(other, dict):
return NotImplemented
rv = RevertableDict(self)
rv.update(other)
return rv
return rv
def __ror__(self, other):
if not isinstance(other, dict):
return NotImplemented
rv = RevertableDict(other)
rv.update(self)
return rv
def values(self):
rv = dict.values(self)
if (sys._getframe(1).f_code.co_flags & FUTURE_FLAGS) != FUTURE_FLAGS:
rv = RevertableList(rv)
return rv
def items(self):
rv = dict.items(self)
if (sys._getframe(1).f_code.co_flags & FUTURE_FLAGS) != FUTURE_FLAGS:
rv = RevertableList(rv)
return rv
else:
itervalues = dict.values
iterkeys = dict.keys
iteritems = dict.items
def has_key(self, key):
return (key in self)
# https://peps.python.org/pep-0584 methods
def __or__(self, other):
if not isinstance(other, dict):
return NotImplemented
rv = RevertableDict(self)
rv.update(other)
return rv
def __ror__(self, other):
if not isinstance(other, dict):
return NotImplemented
rv = RevertableDict(other)
rv.update(self)
return rv
def __ior__(self, other):
self.update(other)
return self
def __ior__(self, other):
self.update(other)
return self
def copy(self):
rv = RevertableDict()
@@ -429,7 +388,7 @@ class RevertableSet(set):
def wrapper(method): # type: ignore
@_method_wrapper(method)
@functools.wraps(method)
def newmethod(*args, **kwargs):
rv = method(*args, **kwargs) # type: ignore
if isinstance(rv, set):
@@ -548,7 +507,7 @@ class MultiRevertable(object):
def checkpointing(method):
@_method_wrapper(method)
@functools.wraps(method)
def do_checkpoint(self, *args, **kwargs):
renpy.game.context().force_checkpoint = True
@@ -560,7 +519,7 @@ def checkpointing(method):
def list_wrapper(method):
@_method_wrapper(method)
@functools.wraps(method)
def newmethod(*args, **kwargs):
l = method(*args, **kwargs)
return RevertableList(l)
@@ -592,10 +551,7 @@ class RollbackRandom(random.Random):
setstate = checkpointing(mutator(random.Random.setstate))
if PY2:
jumpahead = checkpointing(mutator(random.Random.jumpahead)) # type: ignore
else:
choices = list_wrapper(random.Random.choices)
choices = list_wrapper(random.Random.choices)
sample = list_wrapper(random.Random.sample)
getrandbits = checkpointing(mutator(random.Random.getrandbits))
@@ -623,8 +579,7 @@ class DetRandom(random.Random):
super(DetRandom, self).__init__()
self.stack = [ ]
if not PY2:
choices = list_wrapper(random.Random.choices)
choices = list_wrapper(random.Random.choices)
sample = list_wrapper(random.Random.sample)
def random(self):
+1 -4
View File
@@ -47,10 +47,7 @@ import renpy
class StoreDeleted(object):
def __reduce__(self):
if PY2:
return b"deleted"
else:
return "deleted"
return "deleted"
deleted = StoreDeleted()
+14 -30
View File
@@ -47,20 +47,10 @@ script_version = renpy.script_version
# The version of the bytecode cache.
BYTECODE_VERSION = 1
# The python magic code.
if PY2:
import heapq
import imp
MAGIC = imp.get_magic()
from importlib.util import MAGIC_NUMBER as MAGIC
# Change this to force a recompile when required.
MAGIC += b'_v2.1'
else:
from importlib.util import MAGIC_NUMBER as MAGIC
# Change this to force a recompile when required.
MAGIC += b'_v3.1'
# Change this to force a recompile when required.
MAGIC += b'_v3.1'
# A string at the start of each rpycv2 file.
RPYC2_HEADER = b"RENPY RPC2"
@@ -223,10 +213,7 @@ class Script(object):
base, ext = os.path.splitext(short_fn)
if PY2:
hex_checksum = checksum[:8].encode("hex")
else:
hex_checksum = checksum[:8].hex()
hex_checksum = checksum[:8].hex()
target_fn = os.path.join(
backupdir,
@@ -573,12 +560,13 @@ class Script(object):
if renpy.config.allow_duplicate_labels:
return
import linecache
self.duplicate_labels.append(
u'The label {} is defined twice, at File "{}", line {}:\n{}and File "{}", line {}:\n{}'.format(
bad_name, old_node.filename, old_node.linenumber,
renpy.lexer.get_line_text(old_node.filename, old_node.linenumber),
linecache.getline(old_node.filename, old_node.linenumber),
bad_node.filename, bad_node.linenumber,
renpy.lexer.get_line_text(bad_node.filename, bad_node.linenumber),
linecache.getline(bad_node.filename, bad_node.linenumber),
))
self.update_bytecode()
@@ -1028,18 +1016,14 @@ class Script(object):
code = renpy.python.py_compile_eval_bytecode(i.source, filename=i.location[0], lineno=i.location[1], py=i.py)
except SyntaxError as e:
text = e.text
if text is None:
text = ''
assert e.filename is not None
assert e.lineno is not None
pem = renpy.parser.ParseError(
filename=e.filename,
number=e.lineno,
msg=e.msg,
line=text,
pos=e.offset)
e.msg,
e.filename,
e.lineno, e.offset,
e.text,
e.end_lineno, e.end_offset)
renpy.parser.parse_errors.append(pem.message)
+5 -1
View File
@@ -457,7 +457,11 @@ class ScaledImageFont(ImageFont):
w, h = v.get_size()
nw = scale(w)
nh = scale(h)
self.chars[k] = renpy.display.scale.smoothscale(v, (nw, nh))
if renpy.config.nearest_neighbor:
self.chars[k] = renpy.display.pgrender.transform_scale(v, (nw, nh))
else:
self.chars[k] = renpy.display.scale.smoothscale(v, (nw, nh))
def register_sfont(name=None, size=None, bold=False, italics=False, underline=False,
+4 -4
View File
@@ -198,10 +198,10 @@ class ScriptTranslator(object):
if n.identifier in self.default_translates:
old_node = self.default_translates[n.identifier]
renpy.lexer.ParseError(n.filename, n.linenumber, "Line with id %s appears twice. The other line is %s:%d" % (
n.identifier,
old_node.filename, old_node.linenumber)
).defer("duplicate_id")
renpy.lexer.ParseError(
f"Line with id {n.identifier} appears twice. "
f"The other line is {old_node.filename}:{old_node.linenumber}",
n.filename, n.linenumber,).defer("duplicate_id")
self.default_translates[n.identifier] = n
self.file_translates[filename].append((label, n))
+1 -4
View File
@@ -442,10 +442,7 @@ screen = None # type: renpy.display.screen.ScreenDisplayable|None
class Wrapper(renpy.object.Object):
def __reduce__(self):
if PY2:
return bytes(self.name) # type: ignore
else:
return self.name
return self.name
def __init__(self, function, one=False, many=False, imagemap=False, replaces=False, style=None, **kwargs):
+21 -13
View File
@@ -2,14 +2,22 @@
# This builds out of date modules using the default C compiler, and then
# runs them.
set -e
export RENPY_CYTHON=cython
try () {
"$@" || exit -1
}
ROOT="$(dirname $(realpath $0))"
QUIET=${RENPY_QUIET- --quiet}
renpy_branch=$(git branch --show-current)
pushd "$ROOT/pygame_sdl2" >/dev/null
pygame_sdl2_branch=$(git branch --show-current)
popd >/dev/null
if [ "$renpy_branch" != "$pygame_sdl2_branch" ]; then
echo "warning: Ren'Py branch: $renpy_branch, pygame_sdl2 branch: $pygame_sdl2_branch"
fi
if [ -n "$RENPY_COVERAGE" ]; then
variant="renpy-coverage"
else
@@ -25,22 +33,20 @@ if [ -z "$VIRTUAL_ENV" ] ; then
exit 1
fi
PY_VERSION=$(python -c 'import sys; print(sys.version_info.major)')
if [ $PY_VERSION != "2" ]; then
BUILD_J="-j $(nproc)"
ADAPT_TO_SETUPTOOLS="--old-and-unmanageable"
fi
ROOT="$(dirname $(realpath $0))"
BUILD_J="-j $(nproc)"
ADAPT_TO_SETUPTOOLS="--old-and-unmanageable"
setup () {
pushd $1 >/dev/null
try python setup.py $QUIET \
python setup.py $QUIET \
build -b build/lib.$variant -t build/tmp.$variant $BUILD_J \
$RENPY_BUILD_ARGS install $ADAPT_TO_SETUPTOOLS
if [ -e install_headers.py ]; then
python install_headers.py $VIRTUAL_ENV
fi
popd >/dev/null
}
@@ -48,6 +54,8 @@ if [ -e "$ROOT/pygame_sdl2" ]; then
setup "$ROOT/pygame_sdl2/"
fi
if [ -e "$ROOT/cubism" ]; then
export CUBISM="$ROOT/cubism"
export CUBISM_PLATFORM=${CUBISM_PLATFORM:-linux/x86_64}
+1 -2
View File
@@ -2,5 +2,4 @@
cd $(dirname $(dirname $(readlink -f $0)))
nice lib/py2-linux-x86_64/python add.py --real "$@"
nice lib/py3-linux-x86_64/python add.py --real "$@"
lib/py3-linux-x86_64/python add.py --real "$@"
-2
View File
@@ -16,7 +16,5 @@ sleep 2
rpy pull
rpy build --python 3
rpy build --python 2
lib/py3-linux-x86_64/python distribute.py
lib/py2-linux-x86_64/python distribute.py
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
This searches Ren'Py for places where an inner function calls itself recursively.
When that happens, the function winds up as part of a reference cycle, and has to be
garbage collected manually.
"""
import ast
import pathlib
current_path : pathlib.Path = pathlib.Path()
results : set[tuple[str, str]] = set()
class FindRecursiveInnerFunction(ast.NodeVisitor):
def __init__(self, name):
self.name = name
def visit_Name(self, node):
if node.id == self.name:
results.add((current_path, self.name))
class FindInnerFunction(ast.NodeVisitor):
def visit_FunctionDef(self, node):
for i in node.body:
FindRecursiveInnerFunction(node.name).visit(i)
class FindFunctions(ast.NodeVisitor):
def visit_FunctionDef(self, node):
for i in node.body:
FindInnerFunction().visit(i)
def main():
global current_path
root = pathlib.Path(__file__).parent.parent
for fn in root.glob("renpy/**/*.py"):
current_path = fn
with open(fn) as f:
tree = ast.parse(f.read())
FindFunctions().visit(tree)
for fn, name in sorted(results):
print(f"{fn}: {name}")
if __name__ == '__main__':
main()
+34 -3
View File
@@ -4,10 +4,40 @@ Changelog (Ren'Py 7.x-)
*There is also a list of* :doc:`incompatible changes <incompatible>`
.. _renpy-8.4.0:
8.4.0
=====
Requirement and Dependency Changes
----------------------------------
Ren'Py now requires Windows 10 or later to run. This means that it will no longer run on Windows 7, 8, or 8.1.
Ren'Py is no longer built for 32-bit ARM linux. This drops support for the Raspberry Pi 3, and very old Chromebooks.
Ren'Py is still being built for 32-bit ARM Android.
Other Changes
-------------
The ``show expression`` statement has changed so that ``show expression "bg washington"`` is equivalent
to ``show bg washington``. Previously, the expression would be used as a tag, which would rarely be correct.
If a displayable is given instead of a string, a tag will be generated.
One the web platform, :var:`renpy.emscripten` is the emscripten module, making it available
without needing to import it. You should still check that :var:`renpy.emscripten` is true before using it.
When :var:`config.nearest_neighbor` is true, image fonts are scaled using nearest neighbor scaling,
rather than the default bilinear scaling.
The "Image Attributes" screen also indicates if transforms are applied to a layer, as it can be hard
to determine otherwise.
.. _renpy-8.3.3:
.. _renpy-7.8.3:
Fixes
-----
@@ -27,15 +57,16 @@ used, requiring the player to explicitly request the Steam Deck keyboard when re
Synchronized start of audio/video no longer occurs on movie channels unless explicitly requested.
Whe rolling back to a point where a looping :class:`Movie` was showing, the looping movie will be played again
When rolling back to a point where a looping :class:`Movie` was showing, the looping movie will be played again
if it had been stopped.
When :func:`renpy.set_audio_filter` is called with `immediate` false, the filter will be applied when
the queued file is played, rather than at some indeterminate time in the future.
When using text shaders to display text with outlines, Ren'Py will create psuedo-glyphs. These psuedo-glyphs
When using text shaders to display text with outlines, Ren'Py will create pseudo-glyphs. These pseudo-glyphs
cover the start and end of each line, and are used to ensure the outlines will be shown.
.. _renpy-8.3.2:
.. _renpy-7.8.2:
+31
View File
@@ -149,3 +149,34 @@ Debug Functions
---------------
.. include:: inc/debug
Launcher Customization
----------------------
It's possible to customize the Ren'Py launcher to select the files and directories
that are available to click on. To do this, create project.json with the lines below::
{
"renpy_launcher":
{
"open_directory":
{
"game": "game",
"base": ".",
"images": "game/images",
"audio": "game/audio",
"gui": "game/gui"
},
"edit_file":
{
"script.rpy": "game/script.rpy",
"options.rpy": "game/options.rpy",
"gui.rpy": "game/gui.rpy",
"screens.rpy": "game/screens.rpy"
}
}
}
If the file already exists, you'll want to edit in the renpy_launcher key and the lines below it.
You can then edit the dictionaries to change the available files and directories.
+15 -4
View File
@@ -12,12 +12,23 @@ features.
Incompatible changes to the GUI are documented at :ref:`gui-changes`, as
such changes only take effect when the GUI is regenerated.
Pending Deprecations
--------------------
.. _incompatible-8.4.0:
Ren'Py 7.8 is the last release to support Python 2.
8.4.0
-----
Ren'Py 8.4 will drop support for the original OpenGL renderer (gl1), and for Windows 7, 8, and 8.1.
**Show expression.** The ``show expression`` statement has been changed so that::
show expression "bg washington"
is exactly equivalent to:
show bg washington
Previously, this would use the expression itself as the tag. When the expression is not a string,
a unique tag is created for the show expression statement. This change can be reverted with::
define config.old_show_expression = True
.. _incompatible-8.3.0:
+3 -1
View File
@@ -64,7 +64,9 @@ it's running on.
.. var:: renpy.emscripten
Has a true value when running in the browser.
Has a true value when running in the browser. That value is specifically the
emscripten module that lets you interact with the browser, see :func:`renpy.emscripten.run_script`
and similar functions.
.. var:: renpy.mobile
File diff suppressed because it is too large Load Diff
+7 -22
View File
@@ -226,19 +226,20 @@ Javascript
----------
Ren'Py can run Javascript, using three functions in the ``emscripten``
module. This module is only present when running inside the web browser,
which can be tested using :var:`renpy.emscripten` - though you still need
to import the module before using it.
module. This module is available as :var:`renpy.emscripten` when
running on the web platform. When not running on the web platform,
renpy.emscripten is False.
.. function:: emscripten.run_script(script)
.. function:: renpy.emscripten.run_script(script)
Runs the given Javascript script. This does not return a result.
.. function:: emscripten.run_script_int(script)
.. function:: renpy.emscripten.run_script_int(script)
Runs the given Javascript script, and returns its result as an integer.
.. function:: emscripten.run_script_string(script)
.. function:: renpy.emscripten.run_script_string(script)
Runs the given Javascript script, and returns its result as a string.
@@ -251,22 +252,6 @@ and window.renpy_set functions. For the documentation of these functions, please
read ``web/renpy-pre.js``.
Bytecode Cache
--------------
In Ren'Py 8.1, loading may take longer than required, as Ren'Py may need
to compile the python into bytecode after downloading it. To avoid this, retrieve
the bytecode cache by:
* Opening the game in the browser. Hit f12 while the presplash is loading
to open the javascript console.
* In the javascript console, type ``downloadBytecode()``. This will cause
your browser to download ``bytecode-311.rpyb``.
* Place this in the ``game/cache`` directory of your game, and rebuild.
Hamburger Menu
--------------